[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/node.js.yml",
    "content": "# 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\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs\n\nname: PR test\n\non:\n  pull_request_target:\n    types: [assigned, opened, synchronize, reopened]\n    branches: [master]\n    paths: ['storefront/**']\n\njobs:\n  lint-storefront:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/\n        node-version: [20.x, 22.x]\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'yarn'\n          cache-dependency-path: 'storefront/yarn.lock'\n      - run: yarn install --frozen-lockfile\n        working-directory: storefront\n      - run: yarn lint\n        working-directory: storefront\n        env:\n          NODE_ENV: production\n          NEXT_PUBLIC_MEDUSA_BACKEND_URL: ${{ secrets.NEXT_PUBLIC_MEDUSA_BACKEND_URL }}\n          NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY }}\n          NEXT_PUBLIC_STRIPE_KEY: ${{ secrets.NEXT_PUBLIC_STRIPE_KEY }}\n          REVALIDATE_SECRET: ${{ secrets.REVALIDATE_SECRET }}\n          DISALLOW_ROBOTS: true\n          NEXT_PUBLIC_DEFAULT_REGION: us\n          NEXT_PUBLIC_FEATURE_SEARCH_ENABLED: false\n          NEXT_PUBLIC_BASE_URL: https://fashion-starter.agilo.com\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.agents/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Agilo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">Fashion E-commerce Starter for Medusa 2.0</h1>\n\n<video src=\"https://github.com/user-attachments/assets/1afe48e4-5a28-4aee-b4bd-e405701d3cc6\" controls=\"controls\" muted=\"muted\" playsinline=\"playsinline\"></video>\n\n<p align=\"center\">\n  <a href=\"https://www.figma.com/community/file/1494273775050024009\" target=\"_blank\">\n    <img src=\"https://img.shields.io/badge/Figma-Design_Template-F24E1E?style=for-the-badge&logo=figma&logoColor=white\" alt=\"Figma Design Template\" />\n  </a>\n</p>\n\nThe **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.\n\nThis 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.\n\n<h2>Table of Contents</h2>\n\n- [Features](#features)\n- [Roadmap](#roadmap)\n- [Screenshots](#screenshots)\n- [Prerequisites](#prerequisites)\n- [Quickstart](#quickstart)\n  - [Medusa](#medusa)\n  - [Storefront](#storefront)\n  - [Meilisearch](#meilisearch)\n\n## Features\n\n- **Sleek, Modern Design**: The storefront boasts a minimalist, contemporary design that perfectly reflects **Sofa Society's** commitment to modern aesthetics and sustainability.\n- **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.\n- **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.\n- **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.\n- **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**.\n- **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.\n- **Fully Responsive Design**: Optimized for mobile, tablet, and desktop devices, ensuring a smooth, consistent experience across all platforms.\n- **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.\n- **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.\n- **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.\n\n## Roadmap\n- [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).\n- [x] **Search**: Integration with Meilisearch for a powerful search experience.\n- [x] **404 Page**: Custom 404 page for a better user experience.\n- [x] **Account Management**: Allow customers to create accounts, view order history, and manage their personal information.\n- [x] **Cart Drawer**: Cart drawer that slides in from the side where customers can view and edit their cart items.\n- [x] **Email Templates**: Customizable email templates for order confirmation, shipping updates, and more.\n- [x] **Infinite Scroll Pagination**: Improve the product discovery experience with infinite scroll pagination on store and collection pages.\n- [x] **Resend Integration**: Integration with Resend for sending transactional emails.\n\n## Screenshots\n\n<details open=\"open\">\n<summary><strong style=\"font-size: 1.15rem\">Home</strong></summary>\n\n![Home Page](./media/home.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">About</strong></summary>\n\n![About Page](./media/about.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Inspiration</strong></summary>\n\n![Inspiration Page](./media/inspiration.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Collection</strong></summary>\n\n![Collection Page](./media/collection.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Store</strong></summary>\n\n![Store Page](./media/store.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Product</strong></summary>\n\n![Product Page](./media/product.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Cart</strong></summary>\n\n![Cart Page](./media/cart.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Checkout</strong></summary>\n\n![Checkout Page](./media/checkout.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Checkout Review</strong></summary>\n\n![Checkout Review Page](./media/checkout-review.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Order Confirmation</strong></summary>\n\n![Order Confirmation Page](./media/order-confirmation.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Admin - Edit Collection</strong></summary>\n\n![Admin - Edit Collection](./media/admin-collection.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Admin - Edit Product Type</strong></summary>\n\n![Admin - Edit Product Type](./media/admin-product-type.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Admin - Materials</strong></summary>\n\n![Admin - Materials](./media/admin-materials.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Admin - Colors</strong></summary>\n\n![Admin - Colors](./media/admin-colors.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Admin - Edit Color</strong></summary>\n\n![Admin - Edit Color](./media/admin-edit-color.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Admin - Product</strong></summary>\n\n![Admin - Product](./media/admin-product.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Admin - Product Missing Color</strong></summary>\n\n![Admin - Product Missing Color](./media/product-missing-color.jpeg)\n</details>\n\n<details>\n<summary><strong style=\"font-size: 1.15rem\">Admin - Product Add Missing Color</strong></summary>\n\n![Admin - Product Add Missing Color](./media/product-add-missing-color.jpeg)\n</details>\n\n## Prerequisites\n\n- Node >= 20\n- Yarn >= 3.5 for Medusa, Yarn v1 for Storefront\n- Docker and Docker Compose\n- Stripe account (for payments)\n- httpie\n\n## Quickstart\n\n```bash\ngit clone git@github.com:Agilo/fashion-starter.git\n```\n\n### Medusa\n\n```bash\ncd medusa\n\n# Create the .env file\ncp .env.template .env\n\n# Install dependencies\nyarn\n\n# Spin up the database and Redis\ndocker-compose up -d\n\n# Build the project\nyarn build\n\n# Run the migrations\nyarn medusa db:migrate\n\n# Seed the database\nyarn seed\n\n# Create an user\nyarn medusa user -e \"admin@medusa.local\" -p \"supersecret\"\n\n# Start the development server\nyarn dev\n```\n\nAt 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.\n\n### Storefront\n\n```bash\ncd storefront\n\n# Create the .env.local file\ncp .env.template .env.local\n\n# Install dependencies\nyarn\n\n# Start the development server\nyarn dev\n```\n\nYou should now be able to access the storefront at http://localhost:8000.\n\n### Meilisearch\n\n```bash\n# Get search api key\nhttp --auth \"yoursecretmasterkey\" --auth-type bearer GET http://localhost:7700/keys\n```\n\nYou 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`\n\n<a href=\"https://agilo.com\" target=\"_blank\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://github.com/user-attachments/assets/a4429448-a08a-4f5a-8195-2cea1416ca87\">\n    <img src=\"https://github.com/user-attachments/assets/772994f8-32c6-4b27-832f-2660f833fd78\">\n  </picture>\n</a>\n"
  },
  {
    "path": "medusa/.gitignore",
    "content": "/dist\n.env\n.DS_Store\n/uploads\n/node_modules\nyarn-error.log\n\n.idea\n\ncoverage\n\n!src/**\n\n./tsconfig.tsbuildinfo\npackage-lock.json\nmedusa-db.sql\nbuild\n.cache\n\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/sdks\n!.yarn/versions\n\n.medusa\n/static\n"
  },
  {
    "path": "medusa/.npmrc",
    "content": "node-linker=hoisted"
  },
  {
    "path": "medusa/.vscode/settings.json",
    "content": "{\n}"
  },
  {
    "path": "medusa/.yarnrc.yml",
    "content": "nodeLinker: node-modules\n"
  },
  {
    "path": "medusa/README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://www.medusajs.com\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://user-images.githubusercontent.com/59018053/229103275-b5e482bb-4601-46e6-8142-244f531cebdb.svg\">\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg\">\n    <img alt=\"Medusa logo\" src=\"https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg\">\n    </picture>\n  </a>\n</p>\n<h1 align=\"center\">\n  Medusa\n</h1>\n\n<h4 align=\"center\">\n  <a href=\"https://docs.medusajs.com\">Documentation</a> |\n  <a href=\"https://www.medusajs.com\">Website</a>\n</h4>\n\n<p align=\"center\">\n  Building blocks for digital commerce\n</p>\n<p align=\"center\">\n  <a href=\"https://github.com/medusajs/medusa/blob/master/CONTRIBUTING.md\">\n    <img src=\"https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat\" alt=\"PRs welcome!\" />\n  </a>\n    <a href=\"https://www.producthunt.com/posts/medusa\"><img src=\"https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Day-%23DA552E\" alt=\"Product Hunt\"></a>\n  <a href=\"https://discord.gg/xpCwq3Kfn8\">\n    <img src=\"https://img.shields.io/badge/chat-on%20discord-7289DA.svg\" alt=\"Discord Chat\" />\n  </a>\n  <a href=\"https://twitter.com/intent/follow?screen_name=medusajs\">\n    <img src=\"https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs\" alt=\"Follow @medusajs\" />\n  </a>\n</p>\n\n## Compatibility\n\nThis starter is compatible with versions >= 1.8.0 of `@medusajs/medusa`.\n\n## Getting Started\n\nVisit the [Quickstart Guide](https://docs.medusajs.com/create-medusa-app) to set up a server.\n\nVisit the [Docs](https://docs.medusajs.com/development/backend/prepare-environment) to learn more about our system requirements.\n\n## What is Medusa\n\nMedusa 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.\n\nLearn 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.\n\n## Roadmap, Upgrades & Plugins\n\nYou can view the planned, started and completed features in the [Roadmap discussion](https://github.com/medusajs/medusa/discussions/categories/roadmap).\n\nFollow the [Upgrade Guides](https://docs.medusajs.com/upgrade-guides/) to keep your Medusa project up-to-date.\n\nCheck out all [available Medusa plugins](https://medusajs.com/plugins/).\n\n## Community & Contributions\n\nThe 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.\n\nJoin our [Discord server](https://discord.com/invite/medusajs) to meet other community members.\n\n## Other channels\n\n- [GitHub Issues](https://github.com/medusajs/medusa/issues)\n- [Twitter](https://twitter.com/medusajs)\n- [LinkedIn](https://www.linkedin.com/company/medusajs)\n- [Medusa Blog](https://medusajs.com/blog/)\n"
  },
  {
    "path": "medusa/docker-compose.yml",
    "content": "services:\n  postgres:\n    image: postgres:16\n    ports:\n      - 5432:5432\n    environment:\n      POSTGRES_USER: postgres\n      POSTGRES_PASSWORD: postgres\n      POSTGRES_DB: medusa\n    volumes:\n      - medusa-postgres-data:/var/lib/postgresql/data\n\n  redis:\n    image: redis\n    ports:\n      - 6379:6379\n\n  minio:\n    image: minio/minio:RELEASE.2024-10-13T13-34-11Z\n    ports:\n      - 9090:9000\n      - 9001:9001\n    volumes:\n      - medusa-minio-data:/data\n    environment:\n      MINIO_ROOT_USER: medusaminio\n      MINIO_ROOT_PASSWORD: medusaminio\n    command: server /data --console-address \":9001\"\n    healthcheck:\n      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']\n      interval: 30s\n      timeout: 20s\n      retries: 3\n\n  createbuckets:\n    image: minio/mc:RELEASE.2024-10-08T09-37-26Z\n    depends_on:\n      minio:\n        condition: service_healthy\n    restart: on-failure\n    entrypoint: >\n      /bin/sh -c \"\n      /usr/bin/mc alias set myminio http://minio:9000 medusaminio medusaminio;\n      /usr/bin/mc mb myminio/medusa;\n      /usr/bin/mc anonymous set public myminio/medusa;\n      exit 0;\n      \"\n\n  meilisearch:\n    image: getmeili/meilisearch:v1.12\n    ports:\n      - 7700:7700\n    volumes:\n      - meili-data:/meili_data\n    environment:\n      MEILI_MASTER_KEY: ${MEILISEARCH_MASTER_KEY}\n\nvolumes:\n  medusa-postgres-data:\n  medusa-minio-data:\n  meili-data:\n"
  },
  {
    "path": "medusa/instrumentation.js",
    "content": "// Uncomment this file to enable instrumentation and observability using OpenTelemetry\n// Refer to the docs for installation instructions: https://docs.medusajs.com/v2/debugging-and-testing/instrumentation\n\n// const { registerOtel } = require(\"@medusajs/medusa\")\n// // If using an exporter other than Zipkin, require it here.\n// const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin')\n\n// // If using an exporter other than Zipkin, initialize it here.\n// const exporter = new ZipkinExporter({\n//   serviceName: 'my-medusa-project',\n// })\n\n// export function register() {\n//   registerOtel({\n//     serviceName: 'medusajs',\n//     // pass exporter\n//     exporter,\n//     instrument: {\n//       http: true,\n//       workflows: true,\n//       remoteQuery: true\n//     },\n//   })\n// }"
  },
  {
    "path": "medusa/integration-tests/http/README.md",
    "content": "# Integration Tests\n\nThe `medusa-test-utils` package provides utility functions to create integration tests for your API routes and workflows.\n\nFor example:\n\n```ts\nimport { medusaIntegrationTestRunner } from \"medusa-test-utils\"\n\nmedusaIntegrationTestRunner({\n  testSuite: ({ api, getContainer }) => {\n    describe(\"Custom endpoints\", () => {\n      describe(\"GET /store/custom\", () => {\n        it(\"returns correct message\", async () => {\n          const response = await api.get(\n            `/store/custom`\n          )\n  \n          expect(response.status).toEqual(200)\n          expect(response.data).toHaveProperty(\"message\")\n          expect(response.data.message).toEqual(\"Hello, World!\")\n        })\n      })\n    })\n  }\n})\n```\n\nLearn more in [this documentation](https://docs.medusajs.com/v2/debugging-and-testing/testing-tools/integration-tests)."
  },
  {
    "path": "medusa/integration-tests/http/health.spec.ts",
    "content": "import { medusaIntegrationTestRunner } from '@medusajs/test-utils';\njest.setTimeout(60 * 1000);\n\nmedusaIntegrationTestRunner({\n  inApp: true,\n  env: {},\n  testSuite: ({ api }) => {\n    describe('Ping', () => {\n      it('ping the server health endpoint', async () => {\n        const response = await api.get('/health');\n        expect(response.status).toEqual(200);\n      });\n    });\n  },\n});\n"
  },
  {
    "path": "medusa/jest.config.js",
    "content": "const { loadEnv } = require('@medusajs/utils')\nloadEnv('test', process.cwd())\n\nmodule.exports = {\n  transform: {\n    \"^.+\\\\.[jt]s$\": [\n      \"@swc/jest\",\n      {\n        jsc: {\n          parser: { syntax: \"typescript\", decorators: true },\n        },\n      },\n    ],\n  },\n  testEnvironment: \"node\",\n  moduleFileExtensions: [\"js\", \"ts\", \"json\"],\n  modulePathIgnorePatterns: [\"dist/\"],\n}\n\nif (process.env.TEST_TYPE === \"integration:http\") {\n  module.exports.testMatch = [\"**/integration-tests/http/*.spec.[jt]s\"]\n} else if (process.env.TEST_TYPE === \"integration:modules\") {\n  module.exports.testMatch = [\"**/src/modules/*/__tests__/**/*.[jt]s\"]\n} else if (process.env.TEST_TYPE === \"unit\") {\n  module.exports.testMatch = [\"**/src/**/__tests__/**/*.unit.spec.[jt]s\"]\n}"
  },
  {
    "path": "medusa/medusa-config.js",
    "content": "const { loadEnv, defineConfig } = require('@medusajs/framework/utils');\n\nloadEnv(process.env.NODE_ENV, process.cwd());\n\nmodule.exports = defineConfig({\n  admin: {\n    backendUrl:\n      process.env.BACKEND_URL ?? 'https://sofa-society-starter.medusajs.app',\n    storefrontUrl: process.env.STOREFRONT_URL,\n  },\n  projectConfig: {\n    databaseUrl: process.env.DATABASE_URL,\n    redisUrl: process.env.REDIS_URL,\n    http: {\n      storeCors: process.env.STORE_CORS,\n      adminCors: process.env.ADMIN_CORS,\n      authCors: process.env.AUTH_CORS,\n      jwtSecret: process.env.JWT_SECRET || 'supersecret',\n      cookieSecret: process.env.COOKIE_SECRET || 'supersecret',\n      jwtExpiresIn: process.env.JWT_EXPIRES_IN || '24h',\n    },\n  },\n  modules: [\n    {\n      resolve: '@medusajs/medusa/payment',\n      options: {\n        providers: [\n          {\n            id: 'stripe',\n            resolve: '@medusajs/medusa/payment-stripe',\n            options: {\n              apiKey: process.env.STRIPE_API_KEY,\n              webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,\n            },\n          },\n        ],\n      },\n    },\n    {\n      resolve: './src/modules/fashion',\n    },\n    {\n      resolve: '@medusajs/medusa/file',\n      options: {\n        providers: [\n          {\n            resolve: '@medusajs/medusa/file-s3',\n            id: 's3',\n            options: {\n              file_url: process.env.S3_FILE_URL,\n              access_key_id: process.env.S3_ACCESS_KEY_ID,\n              secret_access_key: process.env.S3_SECRET_ACCESS_KEY,\n              region: process.env.S3_REGION,\n              bucket: process.env.S3_BUCKET,\n              endpoint: process.env.S3_ENDPOINT,\n              additional_client_config: {\n                forcePathStyle:\n                  process.env.S3_FORCE_PATH_STYLE === 'true' ? true : undefined,\n              },\n            },\n          },\n        ],\n      },\n    },\n    {\n      resolve: '@medusajs/medusa/notification',\n      options: {\n        providers: [\n          {\n            resolve: './src/modules/resend',\n            id: 'resend',\n            options: {\n              channels: ['email'],\n              api_key: process.env.RESEND_API_KEY,\n              from: process.env.RESEND_FROM,\n              siteTitle: 'SofaSocietyCo.',\n              companyName: 'Sofa Society',\n              footerLinks: [\n                {\n                  url: 'https://agilo.com',\n                  label: 'Agilo',\n                },\n                {\n                  url: 'https://www.instagram.com/agiloltd/',\n                  label: 'Instagram',\n                },\n                {\n                  url: 'https://www.linkedin.com/company/agilo/',\n                  label: 'LinkedIn',\n                },\n              ],\n            },\n          },\n        ],\n      },\n    },\n    {\n      resolve: '@medusajs/medusa/event-bus-redis',\n      options: {\n        redisUrl: process.env.REDIS_URL,\n      },\n    },\n    {\n      resolve: '@medusajs/medusa/caching',\n      options: {\n        providers: [\n          {\n            resolve: '@medusajs/caching-redis',\n            id: 'caching-redis',\n            is_default: true,\n            options: {\n              redisUrl: process.env.REDIS_URL,\n            },\n          },\n        ],\n      },\n    },\n    {\n      resolve: '@medusajs/medusa/workflow-engine-redis',\n      options: {\n        redis: {\n          redisUrl: process.env.REDIS_URL,\n        },\n      },\n    },\n    {\n      resolve: '@medusajs/medusa/locking',\n      options: {\n        providers: [\n          {\n            resolve: '@medusajs/medusa/locking-redis',\n            id: 'locking-redis',\n            is_default: true,\n            options: {\n              redisUrl: process.env.REDIS_URL,\n            },\n          },\n        ],\n      },\n    },\n    {\n      resolve: './src/modules/meilisearch',\n      /**\n       * @type {import('./src/modules/meilisearch/types').MeiliSearchPluginOptions}\n       */\n      options: {\n        config: {\n          host:\n            process.env.MEILISEARCH_HOST ??\n            'https://fashion-starter-search.agilo.agency',\n          apiKey: process.env.MEILISEARCH_API_KEY,\n        },\n        settings: {\n          products: {\n            indexSettings: {\n              searchableAttributes: [\n                'title',\n                'subtitle',\n                'description',\n                'collection',\n                'categories',\n                'type',\n                'tags',\n                'variants',\n                'sku',\n              ],\n              displayedAttributes: [\n                'id',\n                'title',\n                'handle',\n                'subtitle',\n                'description',\n                'is_giftcard',\n                'status',\n                'thumbnail',\n                'collection',\n                'collection_handle',\n                'categories',\n                'categories_handle',\n                'type',\n                'tags',\n                'variants',\n                'sku',\n              ],\n            },\n            primaryKey: 'id',\n            /**\n             * @param {import('@medusajs/types').ProductDTO} product\n             */\n            transformer: (product) => {\n              return {\n                id: product.id,\n                title: product.title,\n                handle: product.handle,\n                subtitle: product.subtitle,\n                description: product.description,\n                is_giftcard: product.is_giftcard,\n                status: product.status,\n                thumbnail: product.images?.[0]?.url ?? null,\n                collection: product.collection.title,\n                collection_handle: product.collection.handle,\n                categories:\n                  product.categories?.map((category) => category.name) ?? [],\n                categories_handle:\n                  product.categories?.map((category) => category.handle) ?? [],\n                type: product.type?.value,\n                tags: product.tags.map((tag) => tag.value),\n                variants: product.variants.map((variant) => variant.title),\n                sku: product.variants\n                  .filter(\n                    (variant) => typeof variant.sku === 'string' && variant.sku,\n                  )\n                  .map((variant) => variant.sku),\n              };\n            },\n          },\n        },\n      },\n    },\n  ],\n  plugins: [\n    {\n      resolve: '@agilo/medusa-analytics-plugin',\n      options: {},\n    },\n  ],\n});\n"
  },
  {
    "path": "medusa/package.json",
    "content": "{\n  \"name\": \"fashion-starter-medusa\",\n  \"version\": \"2.0.0\",\n  \"description\": \"A starter for Medusa projects.\",\n  \"author\": \"Medusa (https://medusajs.com)\",\n  \"license\": \"MIT\",\n  \"keywords\": [\n    \"sqlite\",\n    \"postgres\",\n    \"typescript\",\n    \"ecommerce\",\n    \"headless\",\n    \"medusa\"\n  ],\n  \"scripts\": {\n    \"build\": \"medusa build\",\n    \"seed\": \"medusa exec ./src/scripts/seed.ts\",\n    \"start\": \"medusa start\",\n    \"dev\": \"medusa develop\",\n    \"emails:dev\": \"email dev --dir=src/modules/resend/emails\",\n    \"test:integration:http\": \"TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit\",\n    \"test:integration:modules\": \"TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit\",\n    \"test:unit\": \"TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit\"\n  },\n  \"dependencies\": {\n    \"@agilo/medusa-analytics-plugin\": \"^1.4.0\",\n    \"@medusajs/admin-sdk\": \"2.13.1\",\n    \"@medusajs/cli\": \"2.13.1\",\n    \"@medusajs/framework\": \"2.13.1\",\n    \"@medusajs/icons\": \"2.13.1\",\n    \"@medusajs/medusa\": \"2.13.1\",\n    \"@medusajs/types\": \"2.13.1\",\n    \"@medusajs/ui\": \"4.1.1\",\n    \"@react-email/components\": \"^1.0.7\",\n    \"@tanstack/react-query\": \"5.64.2\",\n    \"meilisearch\": \"0.55.0\",\n    \"posthog-node\": \"^5.24.15\",\n    \"react-dropzone\": \"^15.0.0\",\n    \"resend\": \"^6.9.2\"\n  },\n  \"devDependencies\": {\n    \"@medusajs/test-utils\": \"2.13.1\",\n    \"@react-email/preview-server\": \"^5.2.8\",\n    \"@swc/core\": \"^1.15.11\",\n    \"@swc/jest\": \"^0.2.39\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/node\": \"^22.19.11\",\n    \"@types/react\": \"^18.3.28\",\n    \"@types/react-dom\": \"^18.3.7\",\n    \"jest\": \"^29.7.0\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-email\": \"5.2.8\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"engines\": {\n    \"node\": \">=20\"\n  },\n  \"packageManager\": \"yarn@4.7.0\"\n}\n"
  },
  {
    "path": "medusa/src/admin/README.md",
    "content": "# Admin Customizations\n\nYou can extend the Medusa Admin to add widgets and new pages. Your customizations interact with API routes to provide merchants with custom functionalities.\n\n## Example: Create a Widget\n\nA widget is a React component that can be injected into an existing page in the admin dashboard.\n\nFor example, create the file `src/admin/widgets/product-widget.tsx` with the following content:\n\n```tsx title=\"src/admin/widgets/product-widget.tsx\"\nimport { defineWidgetConfig } from \"@medusajs/admin-sdk\"\n\n// The widget\nconst ProductWidget = () => {\n  return (\n    <div>\n      <h2>Product Widget</h2>\n    </div>\n  )\n}\n\n// The widget's configurations\nexport const config = defineWidgetConfig({\n  zone: \"product.details.after\",\n})\n\nexport default ProductWidget\n```\n\nThis inserts a widget with the text “Product Widget” at the end of a product’s details page."
  },
  {
    "path": "medusa/src/admin/components/EditMaterialDrawer.tsx",
    "content": "import * as React from 'react';\nimport { z } from 'zod';\nimport { Button, Drawer } from '@medusajs/ui';\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { Form } from './Form/Form';\nimport { InputField } from './Form/InputField';\n\nexport const materialFormSchema = z.object({\n  name: z.string(),\n});\n\nexport const EditMaterialDrawer: React.FC<{\n  id: string;\n  initialValues: z.infer<typeof materialFormSchema>;\n  children: React.ReactNode;\n}> = ({ id, initialValues, children }) => {\n  const queryClient = useQueryClient();\n  const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);\n  const updateMaterialMutation = useMutation({\n    mutationKey: ['fashion', 'update'],\n    mutationFn: async (values: z.infer<typeof materialFormSchema>) => {\n      return fetch(`/admin/fashion/${id}`, {\n        method: 'POST',\n        body: JSON.stringify(values),\n        credentials: 'include',\n      }).then((res) => res.json());\n    },\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        predicate: (query) => query.queryKey[0] === 'fashion',\n      });\n    },\n  });\n\n  return (\n    <Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>\n      <Drawer.Trigger asChild>{children}</Drawer.Trigger>\n      <Drawer.Content>\n        <Drawer.Header>\n          <Drawer.Title>Edit Material</Drawer.Title>\n        </Drawer.Header>\n        <Drawer.Body>\n          <Form\n            schema={materialFormSchema}\n            onSubmit={async (values) => {\n              await updateMaterialMutation.mutateAsync(values);\n              setIsDrawerOpen(false);\n            }}\n            formProps={{\n              id: `edit-material-${id}-form`,\n            }}\n            defaultValues={initialValues}\n          >\n            <InputField name=\"name\" label=\"Name\" />\n          </Form>\n        </Drawer.Body>\n        <Drawer.Footer>\n          <Drawer.Close asChild>\n            <Button variant=\"secondary\">Cancel</Button>\n          </Drawer.Close>\n          <Button\n            type=\"submit\"\n            form={`edit-material-${id}-form`}\n            isLoading={updateMaterialMutation.isPending}\n          >\n            Update\n          </Button>\n        </Drawer.Footer>\n      </Drawer.Content>\n    </Drawer>\n  );\n};\n"
  },
  {
    "path": "medusa/src/admin/components/Form/Form.tsx",
    "content": "import * as React from 'react';\nimport {\n  FormProvider,\n  useForm,\n  UseFormProps,\n  DefaultValues,\n  UseFormReturn,\n} from 'react-hook-form';\nimport { z } from 'zod';\nimport { zodResolver } from '@hookform/resolvers/zod';\n\nexport type FormProps<T extends z.ZodType<any, any>> = UseFormProps<\n  z.infer<T>\n> & {\n  schema: T;\n  onSubmit: (\n    values: z.infer<T>,\n    form: UseFormReturn<z.infer<T>>,\n  ) => void | Promise<void>;\n  defaultValues?: DefaultValues<z.infer<T>>;\n  children?: React.ReactNode;\n  formProps?: Omit<React.ComponentProps<'form'>, 'onSubmit'>;\n};\n\nexport const Form = <T extends z.ZodType<any, any>>({\n  schema,\n  onSubmit,\n  children,\n  formProps,\n  ...props\n}: FormProps<T>) => {\n  const form = useForm({\n    resolver: zodResolver(schema),\n    ...props,\n  });\n\n  const submitHandler = React.useCallback(\n    (values: z.infer<T>) => {\n      return onSubmit(values, form);\n    },\n    [onSubmit, form],\n  );\n\n  const onFormSubmit: React.FormEventHandler<HTMLFormElement> =\n    React.useCallback(\n      (event) => {\n        event.preventDefault();\n        event.stopPropagation();\n        form.handleSubmit(submitHandler)(event);\n      },\n      [form, submitHandler],\n    );\n\n  return (\n    <FormProvider {...form}>\n      <form {...formProps} onSubmit={onFormSubmit}>\n        <fieldset disabled={form.formState.isSubmitting}>{children}</fieldset>\n      </form>\n    </FormProvider>\n  );\n};\n"
  },
  {
    "path": "medusa/src/admin/components/Form/ImageField.tsx",
    "content": "import { Label, Button, clx } from '@medusajs/ui';\nimport { DropzoneProps, useDropzone } from 'react-dropzone';\nimport { useController, useFormContext } from 'react-hook-form';\nimport { z } from 'zod';\n\nimport { useAdminUploadImage } from '../../hooks/images';\n\nexport interface ImageFieldProps {\n  className?: string;\n  name: string;\n  label?: string;\n  dropzoneProps?: Omit<DropzoneProps, 'maxFiles'>;\n  dropzoneRootClassName?: string;\n  sizeRecommendation?: React.ReactNode;\n  isRequired?: boolean;\n}\n\nexport interface ImageFieldValue {\n  id: string;\n  url: string;\n}\n\nexport const imageFieldSchema = (params?: z.RawCreateParams) =>\n  z.object(\n    {\n      id: z.string(),\n      url: z.string().url(),\n    },\n    params,\n  );\n\nexport const ImageField: React.FC<ImageFieldProps> = ({\n  className,\n  name,\n  label,\n  dropzoneProps,\n  dropzoneRootClassName,\n  sizeRecommendation = '1200 x 1600 (3:4) recommended, up to 10MB each',\n  isRequired,\n}) => {\n  const form = useFormContext();\n  const { field, fieldState } = useController<{\n    __name__: {\n      id: string;\n      url: string;\n    };\n  }>({ name: name as '__name__' });\n  const uploadFileMutation = useAdminUploadImage({\n    onSuccess: (data) => {\n      field.onChange({\n        id: data.files[0].id,\n        url: data.files[0].url,\n      });\n    },\n    onError(error) {\n      form.setError(name, {\n        message: error.message,\n        type: 'upload_error',\n      });\n    },\n  });\n\n  const { getRootProps, getInputProps, open } = useDropzone({\n    accept: {\n      'image/*': ['.jpg', '.jpeg', '.png'],\n    },\n    ...dropzoneProps,\n    maxFiles: 1,\n    onDropAccepted(files) {\n      uploadFileMutation.mutate({\n        files,\n      });\n    },\n  });\n\n  return (\n    <div className={className}>\n      {typeof label !== 'undefined' && (\n        <Label htmlFor={name} className=\"block mb-1\">\n          {label}\n          {isRequired ? <span className=\"text-red-primary\">*</span> : ''}\n        </Label>\n      )}\n      <div\n        {...getRootProps({\n          className: clx(\n            'inter-base-regular text-grey-50 rounded-rounded border-grey-20 hover:border-violet-60 hover:text-grey-40 flex h-full w-full cursor-pointer select-none flex-col items-center justify-center border-2 border-dashed transition-colors',\n            dropzoneRootClassName,\n          ),\n        })}\n      >\n        <input {...getInputProps()} id={name} />\n        {field.value && typeof field.value !== 'string' ? (\n          <img\n            src={field.value.url}\n            className=\"w-full h-full object-contain rounded-rounded\"\n          />\n        ) : (\n          <div className=\"flex flex-col items-center justify-center\">\n            <p>\n              <span>\n                Drop your image here, or{' '}\n                <span className=\"text-violet-60\">click to browse</span>\n              </span>\n            </p>\n            {sizeRecommendation}\n          </div>\n        )}\n      </div>\n      {field.value && typeof field.value !== 'string' && (\n        <div className=\"mt-2 flex flex-row items-center justify-center gap-2\">\n          <Button\n            type=\"button\"\n            variant=\"secondary\"\n            onClick={() => {\n              field.onChange(null);\n            }}\n          >\n            Remove\n          </Button>\n          <Button\n            type=\"button\"\n            variant=\"secondary\"\n            onClick={() => {\n              open();\n            }}\n          >\n            Replace\n          </Button>\n        </div>\n      )}\n      {fieldState.error && (\n        <div className=\"text-red-primary text-sm mt-1\">\n          {fieldState.error.message}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "medusa/src/admin/components/Form/InputField.tsx",
    "content": "import { Input, Label, clx } from '@medusajs/ui';\nimport { useController, ControllerRenderProps } from 'react-hook-form';\n\nexport interface InputFieldProps {\n  className?: string;\n  name: string;\n  label?: string;\n  type?: React.ComponentProps<typeof Input>['type'];\n  labelProps?: React.ComponentProps<typeof Label>;\n  inputProps?: Omit<\n    React.ComponentProps<typeof Input>,\n    'name' | 'id' | 'type' | keyof ControllerRenderProps\n  >;\n  isRequired?: boolean;\n  suffix?: React.ReactNode;\n}\n\nexport const InputField: React.FC<InputFieldProps> = ({\n  className,\n  name,\n  label,\n  type,\n  labelProps,\n  inputProps,\n  isRequired,\n  suffix,\n}) => {\n  const { field, fieldState } = useController<{ __name__: string }, '__name__'>(\n    { name: name as '__name__' }\n  );\n\n  const inputEl = (\n    <Input\n      {...inputProps}\n      {...field}\n      value={field.value ?? ''}\n      id={name}\n      type={type}\n      aria-invalid={Boolean(fieldState.error)}\n      className={clx(\n        {\n          'pr-8':\n            Boolean(suffix) &&\n            (inputProps?.size === 'base' || !inputProps?.size),\n          'pr-7': Boolean(suffix) && inputProps?.size === 'small',\n        },\n        inputProps?.className\n      )}\n    />\n  );\n\n  return (\n    <div className={className}>\n      {typeof label !== 'undefined' && (\n        <Label\n          {...labelProps}\n          htmlFor={name}\n          className={clx('block mb-1', labelProps?.className)}\n        >\n          {label}\n          {isRequired ? <span className=\"text-red-primary\">*</span> : ''}\n        </Label>\n      )}\n      {suffix ? (\n        <div className=\"relative\">\n          {inputEl}\n          <div\n            className={clx(\n              'absolute bottom-0 right-0 flex items-center justify-center border-l',\n              {\n                'h-8 w-8': inputProps?.size === 'base' || !inputProps?.size,\n                'h-7 w-7': inputProps?.size === 'small',\n              }\n            )}\n          >\n            <div className=\"h-fit w-fit rounded-sm outline-none font-normal font-sans txt-medium text-fg-muted dark:text-fg-muted-dark pointer-events-none select-none\">\n              {suffix}\n            </div>\n          </div>\n        </div>\n      ) : (\n        inputEl\n      )}\n      {fieldState.error && (\n        <div className=\"text-red-primary text-sm mt-1\">\n          {fieldState.error.message}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "medusa/src/admin/components/Form/SelectField.tsx",
    "content": "import { Label, clx, Select } from '@medusajs/ui';\nimport { useController, ControllerRenderProps } from 'react-hook-form';\n\nexport interface SelectFieldProps {\n  className?: string;\n  name: string;\n  label?: string;\n  labelProps?: React.ComponentProps<typeof Label>;\n  selectProps?: Omit<\n    React.ComponentProps<typeof Select>,\n    'name' | 'id' | keyof ControllerRenderProps\n  >;\n  isRequired?: boolean;\n  children?: React.ReactNode;\n}\n\nexport const SelectField: React.FC<SelectFieldProps> = ({\n  className,\n  name,\n  label,\n  labelProps,\n  selectProps,\n  isRequired,\n  children,\n}) => {\n  const { field, fieldState } = useController<{ __name__: string }, '__name__'>(\n    { name: name as '__name__' },\n  );\n\n  return (\n    <div className={className}>\n      {typeof label !== 'undefined' && (\n        <Label\n          {...labelProps}\n          htmlFor={name}\n          className={clx('block mb-1', labelProps?.className)}\n        >\n          {label}\n          {isRequired ? <span className=\"text-red-primary\">*</span> : ''}\n        </Label>\n      )}\n      <Select\n        {...selectProps}\n        onValueChange={field.onChange}\n        onOpenChange={(open) => {\n          if (!open) {\n            field.onBlur();\n          }\n        }}\n        value={field.value || ''}\n        name={field.name}\n        required={isRequired}\n      >\n        {children}\n      </Select>\n      {fieldState.error && (\n        <div className=\"text-red-primary text-sm mt-1\">\n          {fieldState.error.message}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "medusa/src/admin/components/Form/SubmitButton.tsx",
    "content": "import { Button } from '@medusajs/ui';\nimport { useFormState } from 'react-hook-form';\n\nexport const SubmitButton: React.FC<React.ComponentProps<typeof Button>> = (\n  props\n) => {\n  const { isSubmitting } = useFormState();\n  return (\n    <Button\n      {...props}\n      type=\"submit\"\n      isLoading={isSubmitting || props.isLoading}\n      disabled={isSubmitting || props.disabled}\n    />\n  );\n};\n"
  },
  {
    "path": "medusa/src/admin/components/Form/TextareaField.tsx",
    "content": "import { Textarea, Label, clx } from '@medusajs/ui';\nimport { useController, ControllerRenderProps } from 'react-hook-form';\n\nexport interface TextareaFieldProps {\n  className?: string;\n  name: string;\n  label?: string;\n  labelProps?: React.ComponentProps<typeof Label>;\n  textareaProps?: Omit<\n    React.ComponentProps<typeof Textarea>,\n    'name' | 'id' | 'type' | keyof ControllerRenderProps\n  >;\n  isRequired?: boolean;\n}\n\nexport const TextareaField: React.FC<TextareaFieldProps> = ({\n  className,\n  name,\n  label,\n  labelProps,\n  textareaProps,\n  isRequired,\n}) => {\n  const { field, fieldState } = useController<{ __name__: string }, '__name__'>(\n    { name: name as '__name__' },\n  );\n\n  return (\n    <div className={className}>\n      {typeof label !== 'undefined' && (\n        <Label\n          {...labelProps}\n          htmlFor={name}\n          className={clx('block mb-1', labelProps?.className)}\n        >\n          {label}\n          {isRequired ? <span className=\"text-red-primary\">*</span> : ''}\n        </Label>\n      )}\n      <Textarea\n        {...textareaProps}\n        {...field}\n        value={field.value ?? ''}\n        id={name}\n        aria-invalid={Boolean(fieldState.error)}\n      />\n      {fieldState.error && (\n        <div className=\"text-red-primary text-sm mt-1\">\n          {fieldState.error.message}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "medusa/src/admin/components/QueryClientProvider.tsx",
    "content": "import * as React from 'react';\nimport {\n  QueryClient,\n  QueryClientProvider as TanstackQueryClientProvider,\n} from '@tanstack/react-query';\n\nconst queryClient = new QueryClient();\n\nexport const QueryClientProvider: React.FC<{ children: React.ReactNode }> = ({\n  children,\n}) => {\n  return (\n    <TanstackQueryClientProvider client={queryClient}>\n      {children}\n    </TanstackQueryClientProvider>\n  );\n};\n\nexport const withQueryClient = <P extends unknown = {}>(\n  Component: React.ComponentType<P>,\n) => {\n  return (props: P & JSX.IntrinsicAttributes) => (\n    <QueryClientProvider>\n      <Component {...props} />\n    </QueryClientProvider>\n  );\n};\n"
  },
  {
    "path": "medusa/src/admin/hooks/fashion.ts",
    "content": "import {\n  useMutation,\n  UseMutationOptions,\n  useQueryClient,\n} from '@tanstack/react-query';\n\nexport const useCreateMaterialMutation = (\n  options:\n    | Omit<\n        UseMutationOptions<\n          any,\n          Error,\n          {\n            name: string;\n          },\n          unknown\n        >,\n        'mutationKey' | 'mutationFn'\n      >\n    | undefined = undefined,\n) => {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationKey: ['fashion', 'create'],\n    mutationFn: async (values: { name: string }) => {\n      return fetch('/admin/fashion', {\n        method: 'POST',\n        body: JSON.stringify(values),\n        credentials: 'include',\n      }).then((res) => res.json());\n    },\n    ...options,\n    onSuccess: async (...args) => {\n      await queryClient.invalidateQueries({\n        predicate: (query) => query.queryKey[0] === 'fashion',\n      });\n\n      if (options?.onSuccess) {\n        return options.onSuccess(...args);\n      }\n    },\n  });\n};\n\nexport const useCreateColorMutation = (\n  material_id: string,\n  options:\n    | Omit<\n        UseMutationOptions<\n          any,\n          Error,\n          { name: string; hex_code: string },\n          unknown\n        >,\n        'mutationKey' | 'mutationFn'\n      >\n    | undefined = undefined,\n) => {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationKey: ['fashion', material_id, 'colors', 'create'],\n    mutationFn: async (values: { name: string; hex_code: string }) => {\n      return fetch(`/admin/fashion/${material_id}/colors`, {\n        method: 'POST',\n        body: JSON.stringify(values),\n        credentials: 'include',\n      }).then((res) => res.json());\n    },\n    ...options,\n    onSuccess: async (...args) => {\n      await queryClient.invalidateQueries({\n        predicate: (query) => query.queryKey[0] === 'fashion',\n      });\n\n      if (options?.onSuccess) {\n        return options.onSuccess(...args);\n      }\n    },\n  });\n};\n"
  },
  {
    "path": "medusa/src/admin/hooks/images.ts",
    "content": "import { HttpTypes } from '@medusajs/framework/types';\nimport { UseMutationOptions, useMutation } from '@tanstack/react-query';\n\nconst getFileBase64EncodedContent = (file: File) => {\n  return new Promise<string>((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      resolve(\n        (reader.result as string).replace('data:', '').replace(/^.+,/, ''),\n      );\n    };\n    reader.onerror = reject;\n    reader.readAsDataURL(file);\n  });\n};\n\nconst createPayload = async (payload: HttpTypes.AdminUploadFile) => {\n  if (payload instanceof FileList) {\n    const formData = new FormData();\n    for (const file of payload) {\n      formData.append('files', file);\n    }\n    return formData;\n  }\n\n  if (payload.files.every((f) => f instanceof File)) {\n    const formData = new FormData();\n    for (const file of payload.files) {\n      formData.append('files', file);\n    }\n    return formData;\n  }\n\n  const obj: {\n    files: {\n      name: string;\n      content: string;\n    }[];\n  } = {\n    files: [],\n  };\n\n  for (const file of payload.files) {\n    if (file instanceof File) {\n      obj.files.push({\n        name: file.name,\n        content: await getFileBase64EncodedContent(file),\n      });\n    } else {\n      obj.files.push(file);\n    }\n  }\n\n  return JSON.stringify(obj);\n};\n\nexport const useAdminUploadImage = (\n  options?: UseMutationOptions<\n    HttpTypes.AdminFileListResponse,\n    Error,\n    HttpTypes.AdminUploadFile\n  >,\n) => {\n  return useMutation<\n    HttpTypes.AdminFileListResponse,\n    Error,\n    HttpTypes.AdminUploadFile\n  >({\n    mutationKey: ['admin-upload-image'],\n    mutationFn: async (payload) => {\n      const res = await fetch(`/admin/uploads`, {\n        method: 'POST',\n        body: await createPayload(payload),\n        credentials: 'include',\n      });\n\n      if (!res.ok) {\n        throw new Error(res.statusText);\n      }\n\n      return res.json();\n    },\n    ...options,\n  });\n};\n"
  },
  {
    "path": "medusa/src/admin/routes/fashion/[id]/page.tsx",
    "content": "import * as React from 'react';\nimport { z } from 'zod';\nimport { useParams } from 'react-router-dom';\nimport {\n  Container,\n  Heading,\n  Text,\n  IconButton,\n  Table,\n  Button,\n  Drawer,\n  DropdownMenu,\n  Prompt,\n  Switch,\n  Label,\n  Kbd,\n} from '@medusajs/ui';\nimport {\n  PencilSquare,\n  EllipsisHorizontal,\n  Trash,\n  ArrowPath,\n} from '@medusajs/icons';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { useSearchParams } from 'react-router-dom';\n\nimport type { MaterialModelType } from '../../../../modules/fashion/models/material';\nimport { ColorModelType } from '../../../../modules/fashion/models/color';\nimport { useCreateColorMutation } from '../../../hooks/fashion';\nimport { Form } from '../../../components/Form/Form';\nimport { InputField } from '../../../components/Form/InputField';\nimport { EditMaterialDrawer } from '../../../components/EditMaterialDrawer';\nimport { withQueryClient } from '../../../components/QueryClientProvider';\n\nconst colorFormSchema = z.object({\n  name: z.string().min(1),\n  hex_code: z.string().min(7).max(7),\n});\n\nconst EditColorDrawer: React.FC<{\n  materialId: string;\n  id: string;\n  initialValues: z.infer<typeof colorFormSchema>;\n  children: React.ReactNode;\n}> = ({ materialId, id, initialValues, children }) => {\n  const queryClient = useQueryClient();\n  const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);\n  const updateColorMutation = useMutation({\n    mutationKey: ['fashion', materialId, 'colors', id, 'update'],\n    mutationFn: async (values: z.infer<typeof colorFormSchema>) => {\n      return fetch(`/admin/fashion/${materialId}/colors/${id}`, {\n        method: 'POST',\n        body: JSON.stringify(values),\n        credentials: 'include',\n      }).then((res) => res.json());\n    },\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        predicate: (query) => query.queryKey[0] === 'fashion',\n      });\n    },\n  });\n\n  return (\n    <Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>\n      <Drawer.Trigger asChild>{children}</Drawer.Trigger>\n      <Drawer.Content>\n        <Drawer.Header>\n          <Drawer.Title>Edit Color</Drawer.Title>\n        </Drawer.Header>\n        <Drawer.Body>\n          <Form\n            schema={colorFormSchema}\n            onSubmit={async (values) => {\n              await updateColorMutation.mutateAsync(values);\n              setIsDrawerOpen(false);\n            }}\n            formProps={{\n              id: `edit-color-${id}-form`,\n            }}\n            defaultValues={initialValues}\n          >\n            <div className=\"flex flex-col gap-4\">\n              <InputField name=\"name\" label=\"Name\" />\n              <InputField\n                name=\"hex_code\"\n                label=\"Hex Code\"\n                type=\"color\"\n                inputProps={{\n                  className: 'max-w-8',\n                }}\n              />\n            </div>\n          </Form>\n        </Drawer.Body>\n        <Drawer.Footer>\n          <Drawer.Close asChild>\n            <Button variant=\"secondary\">Cancel</Button>\n          </Drawer.Close>\n          <Button\n            type=\"submit\"\n            form={`edit-color-${id}-form`}\n            isLoading={updateColorMutation.isPending}\n          >\n            Update\n          </Button>\n        </Drawer.Footer>\n      </Drawer.Content>\n    </Drawer>\n  );\n};\n\nconst DeleteColorPrompt: React.FC<{\n  materialId: string;\n  id: string;\n  name: string;\n  children: React.ReactNode;\n}> = ({ materialId, name, id, children }) => {\n  const queryClient = useQueryClient();\n  const [isPromptOpen, setIsPromptOpen] = React.useState(false);\n  const deleteColorMutation = useMutation({\n    mutationKey: ['fashion', materialId, 'colors', id, 'delete'],\n    mutationFn: async () => {\n      return fetch(`/admin/fashion/${materialId}/colors/${id}`, {\n        method: 'DELETE',\n        credentials: 'include',\n      }).then((res) => res.json());\n    },\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        predicate: (query) => query.queryKey[0] === 'fashion',\n      });\n\n      setIsPromptOpen(false);\n    },\n  });\n\n  return (\n    <Prompt open={isPromptOpen} onOpenChange={setIsPromptOpen}>\n      <Prompt.Trigger asChild>{children}</Prompt.Trigger>\n      <Prompt.Content>\n        <Prompt.Header>\n          <Prompt.Title>Delete {name} color?</Prompt.Title>\n          <Prompt.Description>\n            Are you sure you want to delete the color {name}?\n          </Prompt.Description>\n        </Prompt.Header>\n        <Prompt.Footer>\n          <Prompt.Cancel>Cancel</Prompt.Cancel>\n          <Prompt.Action\n            onClick={() => {\n              deleteColorMutation.mutate();\n            }}\n          >\n            Delete\n          </Prompt.Action>\n        </Prompt.Footer>\n      </Prompt.Content>\n    </Prompt>\n  );\n};\n\nconst RestoreColorPrompt: React.FC<{\n  materialId: string;\n  id: string;\n  name: string;\n  children: React.ReactNode;\n}> = ({ materialId, name, id, children }) => {\n  const queryClient = useQueryClient();\n  const [isPromptOpen, setIsPromptOpen] = React.useState(false);\n  const restoreColorMutation = useMutation({\n    mutationKey: ['fashion', materialId, 'colors', id, 'restore'],\n    mutationFn: async () => {\n      return fetch(`/admin/fashion/${materialId}/colors/${id}/restore`, {\n        method: 'POST',\n        credentials: 'include',\n      }).then((res) => res.json());\n    },\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        predicate: (query) => query.queryKey[0] === 'fashion',\n      });\n\n      setIsPromptOpen(false);\n    },\n  });\n\n  return (\n    <Prompt\n      open={isPromptOpen}\n      onOpenChange={setIsPromptOpen}\n      variant=\"confirmation\"\n    >\n      <Prompt.Trigger asChild>{children}</Prompt.Trigger>\n      <Prompt.Content>\n        <Prompt.Header>\n          <Prompt.Title>Restore {name} color?</Prompt.Title>\n          <Prompt.Description>\n            Are you sure you want to restore the color {name}?\n          </Prompt.Description>\n        </Prompt.Header>\n        <Prompt.Footer>\n          <Prompt.Cancel>Cancel</Prompt.Cancel>\n          <Prompt.Action\n            onClick={() => {\n              restoreColorMutation.mutate();\n            }}\n          >\n            Restore\n          </Prompt.Action>\n        </Prompt.Footer>\n      </Prompt.Content>\n    </Prompt>\n  );\n};\n\nconst MaterialColors: React.FC<{ materialId: string }> = ({ materialId }) => {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const page = Number(searchParams.get('page')) || 1;\n  const setPage = React.useCallback(\n    (page: number) => {\n      setSearchParams((prev) => {\n        const next = new URLSearchParams(prev);\n        next.set('page', page.toString());\n        return next;\n      });\n    },\n    [setSearchParams]\n  );\n  const deleted = searchParams.has('deleted');\n  const toggleDeleted = React.useCallback(() => {\n    setSearchParams((prev) => {\n      const next = new URLSearchParams(prev);\n\n      if (prev.has('page')) {\n        next.delete('page');\n      }\n\n      if (!prev.has('deleted')) {\n        next.set('deleted', '');\n      } else {\n        next.delete('deleted');\n      }\n\n      return next;\n    });\n  }, [setSearchParams]);\n\n  const [isCreateModalOpen, setIsCreateModalOpen] = React.useState(false);\n\n  const { data, isLoading, isError, isSuccess } = useQuery({\n    queryKey: ['fashion', materialId, 'colors', deleted, page],\n    queryFn: async () => {\n      return fetch(\n        `/admin/fashion/${materialId}/colors?page=${page}${\n          deleted ? '&deleted=true' : ''\n        }`,\n        {\n          credentials: 'include',\n        }\n      ).then(\n        (res) =>\n          res.json() as Promise<{\n            colors: ColorModelType[];\n            count: number;\n            page: number;\n            last_page: number;\n          }>\n      );\n    },\n  });\n\n  const createColorMutation = useCreateColorMutation(materialId);\n\n  return (\n    <div className=\"-px-6\">\n      <div className=\"px-6 flex flex-row gap-6 justify-between items-center mb-4\">\n        <Heading level=\"h2\">Colors</Heading>\n        <div className=\"flex flex-row gap-4\">\n          <div className=\"flex items-center gap-x-2\">\n            <Switch\n              id=\"deleted-flag\"\n              checked={deleted}\n              onClick={() => {\n                toggleDeleted();\n              }}\n            />\n            <Label htmlFor=\"deleted-flag\">Show Deleted</Label>\n          </div>\n          <Drawer open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>\n            <Drawer.Trigger asChild>\n              <Button variant=\"secondary\" size=\"small\">\n                Create\n              </Button>\n            </Drawer.Trigger>\n            <Drawer.Content>\n              <Drawer.Header>\n                <Drawer.Title>Create Color</Drawer.Title>\n              </Drawer.Header>\n              <Drawer.Body>\n                <Form\n                  schema={colorFormSchema}\n                  onSubmit={async (values) => {\n                    await createColorMutation.mutateAsync(values);\n                    setIsCreateModalOpen(false);\n                  }}\n                  formProps={{\n                    id: 'create-color-form',\n                  }}\n                >\n                  <div className=\"flex flex-col gap-4\">\n                    <InputField name=\"name\" label=\"Name\" />\n                    <InputField\n                      name=\"hex_code\"\n                      label=\"Hex Code\"\n                      type=\"color\"\n                      inputProps={{\n                        className: 'max-w-8',\n                      }}\n                    />\n                  </div>\n                </Form>\n              </Drawer.Body>\n              <Drawer.Footer>\n                <Drawer.Close asChild>\n                  <Button variant=\"secondary\">Cancel</Button>\n                </Drawer.Close>\n                <Button\n                  type=\"submit\"\n                  form=\"create-color-form\"\n                  isLoading={createColorMutation.isPending}\n                >\n                  Create\n                </Button>\n              </Drawer.Footer>\n            </Drawer.Content>\n          </Drawer>\n        </div>\n      </div>\n      <Table>\n        <Table.Header>\n          <Table.Row>\n            <Table.HeaderCell>Name</Table.HeaderCell>\n            <Table.HeaderCell>Hex Code</Table.HeaderCell>\n            <Table.HeaderCell>&nbsp;</Table.HeaderCell>\n          </Table.Row>\n        </Table.Header>\n        <Table.Body>\n          {isLoading && (\n            <Table.Row>\n              {/* @ts-ignore */}\n              <Table.Cell colSpan={3}>\n                <Text>Loading...</Text>\n              </Table.Cell>\n            </Table.Row>\n          )}\n          {isError && (\n            <Table.Row>\n              {/* @ts-ignore */}\n              <Table.Cell colSpan={3}>\n                <Text>Error loading colors</Text>\n              </Table.Cell>\n            </Table.Row>\n          )}\n          {isSuccess && data.colors.length === 0 && (\n            <Table.Row>\n              {/* @ts-ignore */}\n              <Table.Cell colSpan={3}>\n                <Text>No colors found</Text>\n              </Table.Cell>\n            </Table.Row>\n          )}\n          {isSuccess &&\n            data.colors.length > 0 &&\n            data.colors.map((color) => (\n              <Table.Row key={color.id}>\n                <Table.Cell>{color.name}</Table.Cell>\n                <Table.Cell>\n                  <Kbd className=\"flex flex-row gap-1 items-center font-mono\">\n                    <div\n                      className=\"w-3 h-3 rounded-full\"\n                      style={{ backgroundColor: `${color.hex_code}` }}\n                    />\n                    {color.hex_code}\n                  </Kbd>\n                </Table.Cell>\n                <Table.Cell className=\"text-right\">\n                  <DropdownMenu>\n                    <DropdownMenu.Trigger asChild>\n                      <IconButton>\n                        <EllipsisHorizontal />\n                      </IconButton>\n                    </DropdownMenu.Trigger>\n                    <DropdownMenu.Content>\n                      <DropdownMenu.Item asChild>\n                        <EditColorDrawer\n                          materialId={materialId}\n                          id={color.id}\n                          initialValues={color}\n                        >\n                          <Button\n                            variant=\"transparent\"\n                            className=\"flex flex-row gap-2 items-center w-full justify-start\"\n                          >\n                            <PencilSquare className=\"text-fg-subtle dark:text-fg-subtle-dark\" />\n                            Edit\n                          </Button>\n                        </EditColorDrawer>\n                      </DropdownMenu.Item>\n                      <DropdownMenu.Separator />\n                      {color.deleted_at ? (\n                        <DropdownMenu.Item asChild>\n                          <RestoreColorPrompt\n                            materialId={materialId}\n                            id={color.id}\n                            name={color.name}\n                          >\n                            <Button\n                              variant=\"transparent\"\n                              className=\"flex flex-row gap-2 items-center w-full justify-start\"\n                            >\n                              <ArrowPath className=\"text-fg-subtle dark:text-fg-subtle-dark\" />\n                              Restore\n                            </Button>\n                          </RestoreColorPrompt>\n                        </DropdownMenu.Item>\n                      ) : (\n                        <DropdownMenu.Item asChild>\n                          <DeleteColorPrompt\n                            materialId={materialId}\n                            id={color.id}\n                            name={color.name}\n                          >\n                            <Button\n                              variant=\"transparent\"\n                              className=\"flex flex-row gap-2 items-center w-full justify-start\"\n                            >\n                              <Trash className=\"text-fg-subtle dark:text-fg-subtle-dark\" />\n                              Delete\n                            </Button>\n                          </DeleteColorPrompt>\n                        </DropdownMenu.Item>\n                      )}\n                    </DropdownMenu.Content>\n                  </DropdownMenu>\n                </Table.Cell>\n              </Table.Row>\n            ))}\n        </Table.Body>\n      </Table>\n      <Table.Pagination\n        className=\"pb-0\"\n        count={data?.count || 0}\n        pageSize={20}\n        pageIndex={page - 1}\n        pageCount={data?.last_page ?? 1}\n        canPreviousPage={page > 1}\n        canNextPage={page < (data?.last_page ?? 1)}\n        previousPage={() => setPage(Math.max(1, page - 1))}\n        nextPage={() => setPage(Math.min(page + 1, data?.last_page ?? 1))}\n      />\n    </div>\n  );\n};\n\nconst MaterialPage = () => {\n  const { id } = useParams();\n  const { data, isLoading, isError, isSuccess } = useQuery({\n    queryKey: ['fashion', id],\n    queryFn: async () => {\n      const res = await fetch(`/admin/fashion/${id}`, {\n        credentials: 'include',\n      });\n      return res.json() as Promise<MaterialModelType>;\n    },\n  });\n\n  if (!id) {\n    return null;\n  }\n\n  return (\n    <Container className=\"px-0\">\n      {isLoading && <Text>Loading...</Text>}\n      {isError && <Text>Error loading material</Text>}\n      {isSuccess && (\n        <>\n          <div className=\"px-6 flex flex-row gap-6 justify-between items-center mb-4\">\n            <div className=\"flex flex-row gap-3\">\n              <Heading level=\"h2\">{data?.name}</Heading>\n              <EditMaterialDrawer id={id} initialValues={data}>\n                <IconButton size=\"xsmall\" variant=\"transparent\">\n                  <PencilSquare />\n                </IconButton>\n              </EditMaterialDrawer>\n            </div>\n          </div>\n        </>\n      )}\n      <hr className=\"mb-6\" />\n      <MaterialColors materialId={id} />\n    </Container>\n  );\n};\n\nexport default withQueryClient(MaterialPage);\n"
  },
  {
    "path": "medusa/src/admin/routes/fashion/page.tsx",
    "content": "import * as React from 'react';\nimport { defineRouteConfig } from '@medusajs/admin-sdk';\nimport {\n  Swatch,\n  PencilSquare,\n  EllipsisHorizontal,\n  Trash,\n  ArrowPath,\n} from '@medusajs/icons';\nimport {\n  Container,\n  Heading,\n  Table,\n  Button,\n  IconButton,\n  Text,\n  Drawer,\n  DropdownMenu,\n  Prompt,\n  Switch,\n  Label,\n} from '@medusajs/ui';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { useSearchParams, Link } from 'react-router-dom';\n\nimport { MaterialModelType } from '../../../modules/fashion/models/material';\nimport { useCreateMaterialMutation } from '../../hooks/fashion';\nimport { Form } from '../../components/Form/Form';\nimport { InputField } from '../../components/Form/InputField';\nimport {\n  EditMaterialDrawer,\n  materialFormSchema,\n} from '../../components/EditMaterialDrawer';\nimport { withQueryClient } from '../../components/QueryClientProvider';\n\nconst DeleteMaterialPrompt: React.FC<{\n  id: string;\n  name: string;\n  children: React.ReactNode;\n}> = ({ id, name, children }) => {\n  const queryClient = useQueryClient();\n  const [isPromptOpen, setIsPromptOpen] = React.useState(false);\n  const deleteMaterialMutation = useMutation({\n    mutationKey: ['fashion', id, 'delete'],\n    mutationFn: async () => {\n      return fetch(`/admin/fashion/${id}`, {\n        method: 'DELETE',\n        credentials: 'include',\n      }).then((res) => res.json());\n    },\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        predicate: (query) => query.queryKey[0] === 'fashion',\n      });\n\n      setIsPromptOpen(false);\n    },\n  });\n\n  return (\n    <Prompt open={isPromptOpen} onOpenChange={setIsPromptOpen}>\n      <Prompt.Trigger asChild>{children}</Prompt.Trigger>\n      <Prompt.Content>\n        <Prompt.Header>\n          <Prompt.Title>Delete {name} material?</Prompt.Title>\n          <Prompt.Description>\n            Are you sure you want to delete the material {name}?\n          </Prompt.Description>\n        </Prompt.Header>\n        <Prompt.Footer>\n          <Prompt.Cancel>Cancel</Prompt.Cancel>\n          <Prompt.Action\n            onClick={() => {\n              deleteMaterialMutation.mutate();\n            }}\n          >\n            Delete\n          </Prompt.Action>\n        </Prompt.Footer>\n      </Prompt.Content>\n    </Prompt>\n  );\n};\n\nconst RestoreMaterialPrompt: React.FC<{\n  id: string;\n  name: string;\n  children: React.ReactNode;\n}> = ({ id, name, children }) => {\n  const queryClient = useQueryClient();\n  const [isPromptOpen, setIsPromptOpen] = React.useState(false);\n  const restoreMaterialMutation = useMutation({\n    mutationKey: ['fashion', id, 'restore'],\n    mutationFn: async () => {\n      return fetch(`/admin/fashion/${id}/restore`, {\n        method: 'POST',\n        credentials: 'include',\n      }).then((res) => res.json());\n    },\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        predicate: (query) => query.queryKey[0] === 'fashion',\n      });\n\n      setIsPromptOpen(false);\n    },\n  });\n\n  return (\n    <Prompt open={isPromptOpen} onOpenChange={setIsPromptOpen}>\n      <Prompt.Trigger asChild>{children}</Prompt.Trigger>\n      <Prompt.Content>\n        <Prompt.Header>\n          <Prompt.Title>Restore {name} material?</Prompt.Title>\n          <Prompt.Description>\n            Are you sure you want to restore the material {name}?\n          </Prompt.Description>\n        </Prompt.Header>\n        <Prompt.Footer>\n          <Prompt.Cancel>Cancel</Prompt.Cancel>\n          <Prompt.Action\n            onClick={() => {\n              restoreMaterialMutation.mutate();\n            }}\n          >\n            Restore\n          </Prompt.Action>\n        </Prompt.Footer>\n      </Prompt.Content>\n    </Prompt>\n  );\n};\n\nconst FashionPage = () => {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const page = Number(searchParams.get('page')) || 1;\n  const setPage = React.useCallback(\n    (page: number) => {\n      setSearchParams((prev) => {\n        const next = new URLSearchParams(prev);\n        next.set('page', page.toString());\n        return next;\n      });\n    },\n    [setSearchParams]\n  );\n  const deleted = searchParams.has('deleted');\n  const toggleDeleted = React.useCallback(() => {\n    setSearchParams((prev) => {\n      const next = new URLSearchParams(prev);\n\n      if (prev.has('page')) {\n        next.delete('page');\n      }\n\n      if (!prev.has('deleted')) {\n        next.set('deleted', '');\n      } else {\n        next.delete('deleted');\n      }\n      return next;\n    });\n  }, [setSearchParams]);\n  const [isCreateModalOpen, setIsCreateModalOpen] = React.useState(false);\n\n  const { data, isLoading, isError, isSuccess } = useQuery({\n    queryKey: ['fashion', deleted, page],\n    queryFn: async () => {\n      return fetch(\n        `/admin/fashion?page=${page}${deleted ? '&deleted=true' : ''}`,\n        {\n          credentials: 'include',\n        }\n      ).then(\n        (res) =>\n          res.json() as Promise<{\n            materials: MaterialModelType[];\n            count: number;\n            page: number;\n            last_page: number;\n          }>\n      );\n    },\n  });\n\n  const createMaterialMutation = useCreateMaterialMutation();\n\n  return (\n    <Container className=\"px-0\">\n      <div className=\"px-6 flex flex-row gap-6 justify-between items-center mb-4\">\n        <Heading level=\"h2\">Materials</Heading>\n        <div className=\"flex flex-row gap-4\">\n          <div className=\"flex items-center gap-x-2\">\n            <Switch\n              id=\"deleted-flag\"\n              checked={deleted}\n              onClick={() => {\n                toggleDeleted();\n              }}\n            />\n            <Label htmlFor=\"deleted-flag\">Show Deleted</Label>\n          </div>\n          <Drawer open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>\n            <Drawer.Trigger asChild>\n              <Button variant=\"secondary\" size=\"small\">\n                Create\n              </Button>\n            </Drawer.Trigger>\n            <Drawer.Content>\n              <Drawer.Header>\n                <Drawer.Title>Create Material</Drawer.Title>\n              </Drawer.Header>\n              <Drawer.Body>\n                <Form\n                  schema={materialFormSchema}\n                  onSubmit={async (values) => {\n                    await createMaterialMutation.mutateAsync(values);\n                    setIsCreateModalOpen(false);\n                  }}\n                  formProps={{\n                    id: 'create-material-form',\n                  }}\n                >\n                  <InputField name=\"name\" label=\"Name\" />\n                </Form>\n              </Drawer.Body>\n              <Drawer.Footer>\n                <Drawer.Close asChild>\n                  <Button variant=\"secondary\">Cancel</Button>\n                </Drawer.Close>\n                <Button\n                  type=\"submit\"\n                  form=\"create-material-form\"\n                  isLoading={createMaterialMutation.isPending}\n                >\n                  Create\n                </Button>\n              </Drawer.Footer>\n            </Drawer.Content>\n          </Drawer>\n        </div>\n      </div>\n      <Table>\n        <Table.Header>\n          <Table.Row>\n            <Table.HeaderCell>Name</Table.HeaderCell>\n            <Table.HeaderCell>&nbsp;</Table.HeaderCell>\n          </Table.Row>\n        </Table.Header>\n        <Table.Body>\n          {isLoading && (\n            <Table.Row>\n              {/* @ts-ignore */}\n              <Table.Cell colSpan={2}>\n                <Text>Loading...</Text>\n              </Table.Cell>\n            </Table.Row>\n          )}\n          {isError && (\n            <Table.Row>\n              {/* @ts-ignore */}\n              <Table.Cell colSpan={2}>\n                <Text>Error loading materials</Text>\n              </Table.Cell>\n            </Table.Row>\n          )}\n          {isSuccess && data.materials.length === 0 && (\n            <Table.Row>\n              {/* @ts-ignore */}\n              <Table.Cell colSpan={2}>\n                <Text>No materials found</Text>\n              </Table.Cell>\n            </Table.Row>\n          )}\n          {isSuccess &&\n            data.materials.length > 0 &&\n            data.materials.map((material) => (\n              <Table.Row key={material.id}>\n                <Table.Cell>\n                  <Link to={`/fashion/${material.id}`}>{material.name}</Link>\n                </Table.Cell>\n                <Table.Cell className=\"text-right\">\n                  <DropdownMenu>\n                    <DropdownMenu.Trigger asChild>\n                      <IconButton>\n                        <EllipsisHorizontal />\n                      </IconButton>\n                    </DropdownMenu.Trigger>\n                    <DropdownMenu.Content>\n                      <DropdownMenu.Item asChild>\n                        <EditMaterialDrawer\n                          id={material.id}\n                          initialValues={material}\n                        >\n                          <Button\n                            variant=\"transparent\"\n                            className=\"flex flex-row gap-2 items-center w-full justify-start\"\n                          >\n                            <PencilSquare className=\"text-fg-subtle dark:text-fg-subtle-dark\" />\n                            Edit\n                          </Button>\n                        </EditMaterialDrawer>\n                      </DropdownMenu.Item>\n                      <DropdownMenu.Separator />\n                      {material.deleted_at ? (\n                        <DropdownMenu.Item asChild>\n                          <RestoreMaterialPrompt\n                            id={material.id}\n                            name={material.name}\n                          >\n                            <Button\n                              variant=\"transparent\"\n                              className=\"flex flex-row gap-2 items-center w-full justify-start\"\n                            >\n                              <ArrowPath className=\"text-fg-subtle dark:text-fg-subtle-dark\" />\n                              Restore\n                            </Button>\n                          </RestoreMaterialPrompt>\n                        </DropdownMenu.Item>\n                      ) : (\n                        <DropdownMenu.Item asChild>\n                          <DeleteMaterialPrompt\n                            id={material.id}\n                            name={material.name}\n                          >\n                            <Button\n                              variant=\"transparent\"\n                              className=\"flex flex-row gap-2 items-center w-full justify-start\"\n                            >\n                              <Trash className=\"text-fg-subtle dark:text-fg-subtle-dark\" />\n                              Delete\n                            </Button>\n                          </DeleteMaterialPrompt>\n                        </DropdownMenu.Item>\n                      )}\n                    </DropdownMenu.Content>\n                  </DropdownMenu>\n                </Table.Cell>\n              </Table.Row>\n            ))}\n        </Table.Body>\n      </Table>\n      <Table.Pagination\n        className=\"pb-0\"\n        count={data?.count || 0}\n        pageSize={20}\n        pageIndex={page - 1}\n        pageCount={data?.last_page ?? 1}\n        canPreviousPage={page > 1}\n        canNextPage={page < (data?.last_page ?? 1)}\n        previousPage={() => setPage(Math.max(1, page - 1))}\n        nextPage={() => setPage(Math.min(page + 1, data?.last_page ?? 1))}\n      />\n    </Container>\n  );\n};\n\nexport default withQueryClient(FashionPage);\n\nexport const config = defineRouteConfig({\n  label: 'Materials & Colors',\n  icon: Swatch,\n});\n"
  },
  {
    "path": "medusa/src/admin/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\".\"]\n}\n"
  },
  {
    "path": "medusa/src/admin/widgets/collection-details.tsx",
    "content": "import * as React from 'react';\nimport { defineWidgetConfig } from '@medusajs/admin-sdk';\nimport { DetailWidgetProps, AdminCollection } from '@medusajs/framework/types';\nimport { Container, Heading, Button, Drawer, Text } from '@medusajs/ui';\nimport { PencilSquare } from '@medusajs/icons';\nimport { z } from 'zod';\n\nimport { ImageField, imageFieldSchema } from '../components/Form/ImageField';\nimport { Form } from '../components/Form/Form';\nimport { TextareaField } from '../components/Form/TextareaField';\nimport { InputField } from '../components/Form/InputField';\n\nconst detailsFormSchema = z.object({\n  image: imageFieldSchema().optional(),\n  description: z.string().optional(),\n  collection_page_image: imageFieldSchema().optional(),\n  collection_page_heading: z.string().optional(),\n  collection_page_content: z.string().optional(),\n  product_page_heading: z.string().optional(),\n  product_page_image: imageFieldSchema().optional(),\n  product_page_wide_image: imageFieldSchema().optional(),\n  product_page_cta_image: imageFieldSchema().optional(),\n  product_page_cta_heading: z.string().optional(),\n  product_page_cta_link: z.string().optional(),\n});\n\nconst UpdateDetailsDrawer: React.FC<{\n  children: React.ReactNode;\n  isOpen: boolean;\n  onOpenChange: (open: boolean) => void;\n  id: string;\n  title: React.ReactNode;\n  initialValue: z.infer<typeof detailsFormSchema>;\n  onSave: (values: z.infer<typeof detailsFormSchema>) => void;\n}> = ({ children, isOpen, onOpenChange, id, title, initialValue, onSave }) => {\n  return (\n    <Drawer open={isOpen} onOpenChange={onOpenChange}>\n      <Drawer.Trigger asChild>{children}</Drawer.Trigger>\n      <Drawer.Content className=\"max-h-full\">\n        <Drawer.Header>\n          <Drawer.Title>{title}</Drawer.Title>\n        </Drawer.Header>\n        <Drawer.Body className=\"p-4 overflow-auto\">\n          <Form\n            schema={detailsFormSchema}\n            onSubmit={async (values) => {\n              await fetch(`/admin/custom/collections/${id}/details`, {\n                method: 'POST',\n                body: JSON.stringify(values),\n                credentials: 'include',\n              });\n\n              onSave(values);\n            }}\n            defaultValues={initialValue}\n            formProps={{\n              id: `edit-collection-${id}-fields`,\n            }}\n          >\n            <div className=\"flex flex-col gap-4\">\n              <ImageField\n                name=\"image\"\n                label=\"Image\"\n                dropzoneRootClassName=\"h-60\"\n              />\n              <TextareaField name=\"description\" label=\"Description\" />\n              <ImageField\n                name=\"collection_page_image\"\n                label=\"Collection page image\"\n                dropzoneRootClassName=\"h-60\"\n              />\n              <InputField\n                name=\"collection_page_heading\"\n                label=\"Collection page heading\"\n              />\n              <TextareaField\n                name=\"collection_page_content\"\n                label=\"Collection page content\"\n              />\n              <InputField\n                name=\"product_page_heading\"\n                label=\"Product page heading\"\n              />\n              <ImageField\n                name=\"product_page_image\"\n                label=\"Product page image\"\n                dropzoneRootClassName=\"h-60\"\n              />\n              <ImageField\n                name=\"product_page_wide_image\"\n                label=\"Product page wide image\"\n                dropzoneRootClassName=\"h-60\"\n              />\n              <ImageField\n                name=\"product_page_cta_image\"\n                label=\"Product page CTA image\"\n                dropzoneRootClassName=\"h-60\"\n              />\n              <InputField\n                name=\"product_page_cta_heading\"\n                label=\"Product page CTA heading\"\n              />\n              <InputField\n                name=\"product_page_cta_link\"\n                label=\"Product page CTA link label\"\n              />\n            </div>\n          </Form>\n        </Drawer.Body>\n        <Drawer.Footer>\n          <Drawer.Close asChild>\n            <Button variant=\"secondary\">Cancel</Button>\n          </Drawer.Close>\n          <Button type=\"submit\" form={`edit-collection-${id}-fields`}>\n            Save\n          </Button>\n        </Drawer.Footer>\n      </Drawer.Content>\n    </Drawer>\n  );\n};\n\nconst CollectionDetailsWidget = ({\n  data,\n}: DetailWidgetProps<AdminCollection>) => {\n  const [isEditModalOpen, setIsModalOpen] = React.useState(false);\n  const [details, setDetails] = React.useState<z.infer<\n    typeof detailsFormSchema\n  > | null>(null);\n\n  React.useEffect(() => {\n    fetch(`/admin/custom/collections/${data.id}/details`, {\n      credentials: 'include',\n    })\n      .then((res) => res.json())\n      .then((json) => {\n        setDetails(json);\n      })\n      .catch((e) => {\n        console.error(e);\n      });\n  }, [data.id]);\n\n  return (\n    <Container className=\"divide-y p-0\">\n      <div className=\"flex items-center justify-between px-6 py-4\">\n        <Heading>Details</Heading>\n        {details !== null && (\n          <UpdateDetailsDrawer\n            isOpen={isEditModalOpen}\n            onOpenChange={setIsModalOpen}\n            title=\"Update collection details\"\n            id={data.id}\n            initialValue={details}\n            onSave={(value) => {\n              setDetails(value);\n              setIsModalOpen(false);\n            }}\n          >\n            <Button\n              variant=\"transparent\"\n              size=\"small\"\n              className=\"text-fg-muted dark:text-fg-muted-dark hover:text-fg-subtle dark:hover:text-fg-subtle-dark\"\n              onClick={(event) => {\n                event.preventDefault();\n                setIsModalOpen(true);\n              }}\n            >\n              <PencilSquare /> Edit\n            </Button>\n          </UpdateDetailsDrawer>\n        )}\n      </div>\n      <div className=\"text-fg-subtle dark:text-fg-subtle-dark grid grid-cols-2 items-center px-6 py-4\">\n        {details === null ? (\n          <Text>Loading...</Text>\n        ) : (\n          <div className=\"flex flex-col gap-2\">\n            {typeof details.image?.url === 'string' && (\n              <div>\n                <img\n                  src={details.image.url}\n                  className=\"max-h-60 max-w-none w-auto\"\n                />\n              </div>\n            )}\n            {(details.description?.length ?? 0) > 0 && (\n              <Text>{details.description}</Text>\n            )}\n\n            {typeof details.image?.url !== 'string' && !details.description && (\n              <Text>No details available</Text>\n            )}\n\n            <Heading>Collection Page</Heading>\n\n            {typeof details.collection_page_image?.url === 'string' && (\n              <div>\n                <img\n                  src={details.collection_page_image.url}\n                  className=\"max-h-60 max-w-none w-auto\"\n                />\n              </div>\n            )}\n            {(details.collection_page_heading?.length ?? 0) > 0 && (\n              <Text>{details.collection_page_heading}</Text>\n            )}\n            {(details.collection_page_content?.length ?? 0) > 0 && (\n              <Text>{details.collection_page_content}</Text>\n            )}\n\n            {typeof details.collection_page_image?.url !== 'string' &&\n              !details.collection_page_heading &&\n              !details.collection_page_content && (\n                <Text>Collection page details not entered</Text>\n              )}\n\n            <Heading>Product Page</Heading>\n\n            {typeof details.product_page_heading?.length === 'string' && (\n              <Text>{details.product_page_heading}</Text>\n            )}\n\n            {typeof details.product_page_image?.url === 'string' && (\n              <div>\n                <img\n                  src={details.product_page_image.url}\n                  className=\"max-h-60 max-w-none w-auto\"\n                />\n              </div>\n            )}\n\n            {typeof details.product_page_wide_image?.url === 'string' && (\n              <div>\n                <img\n                  src={details.product_page_wide_image.url}\n                  className=\"max-h-60 max-w-none w-auto\"\n                />\n              </div>\n            )}\n\n            {typeof details.product_page_cta_image?.url === 'string' && (\n              <div>\n                <img\n                  src={details.product_page_cta_image.url}\n                  className=\"max-h-60 max-w-none w-auto\"\n                />\n              </div>\n            )}\n\n            {(details.product_page_cta_heading?.length ?? 0) > 0 && (\n              <Text>{details.product_page_cta_heading}</Text>\n            )}\n\n            {(details.product_page_cta_link?.length ?? 0) > 0 && (\n              <Text>{details.product_page_cta_link}</Text>\n            )}\n\n            {typeof details.product_page_heading?.length !== 'string' &&\n              typeof details.product_page_image?.url !== 'string' &&\n              typeof details.product_page_wide_image?.url !== 'string' &&\n              typeof details.product_page_cta_image?.url !== 'string' &&\n              !details.product_page_cta_heading &&\n              !details.product_page_cta_link && (\n                <Text>Product page details not entered</Text>\n              )}\n          </div>\n        )}\n      </div>\n    </Container>\n  );\n};\n\nexport const config = defineWidgetConfig({\n  zone: 'product_collection.details.after',\n});\n\nexport default CollectionDetailsWidget;\n"
  },
  {
    "path": "medusa/src/admin/widgets/product-fashion.tsx",
    "content": "import * as React from 'react';\nimport { defineWidgetConfig } from '@medusajs/admin-sdk';\nimport { DetailWidgetProps, AdminProduct } from '@medusajs/framework/types';\nimport {\n  Container,\n  Heading,\n  Text,\n  Button,\n  Drawer,\n  IconButton,\n} from '@medusajs/ui';\nimport { ArrowPath, PlusMini } from '@medusajs/icons';\nimport { z } from 'zod';\nimport { useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { MaterialModelType } from '../../modules/fashion/models/material';\n\nimport { Form } from '../components/Form/Form';\nimport { withQueryClient } from '../components/QueryClientProvider';\nimport {\n  useCreateColorMutation,\n  useCreateMaterialMutation,\n} from '../hooks/fashion';\nimport { InputField } from '../components/Form/InputField';\n\n// const SelectColorField: React.FC<{\n//   name: string;\n// }> = ({ name }) => {\n//   const materialsQuery = useInfiniteQuery({\n//     queryKey: ['fashion'],\n//     queryFn: async ({ pageParam = 1, signal }) => {\n//       const res = await fetch(`/admin/fashion?page=${pageParam}`, {\n//         credentials: 'include',\n//         signal,\n//       });\n\n//       return res.json() as Promise<{\n//         materials: MaterialModelType[];\n//         count: number;\n//         page: number;\n//         last_page: number;\n//       }>;\n//     },\n//     initialPageParam: 1,\n//     getNextPageParam: (lastPage) => {\n//       return lastPage.page < lastPage.last_page ? lastPage.page + 1 : undefined;\n//     },\n//     getPreviousPageParam: (firstPage) => {\n//       return firstPage.page > 1 ? firstPage.page - 1 : undefined;\n//     },\n//   });\n\n//   return (\n//     <SelectField name={name}>\n//       <Select.Trigger>\n//         <Select.Value placeholder=\"Select color\" />\n//       </Select.Trigger>\n//       <Select.Content>\n//         {materialsQuery.isSuccess &&\n//           materialsQuery.data.pages.map((materialsPageData) =>\n//             materialsPageData.materials.map((material) => (\n//               <Select.Group key={material.id}>\n//                 <Select.Label>{material.name}</Select.Label>\n//                 {material.colors.map((color) => (\n//                   <Select.Item key={color.id} value={color.id}>\n//                     {color.name}\n//                   </Select.Item>\n//                 ))}\n//               </Select.Group>\n//             )),\n//           )}\n//         {materialsQuery.isSuccess && materialsQuery.hasNextPage && (\n//           <Select.Item\n//             key={'load-more'}\n//             value=\"load-more\"\n//             onClick={(event) => {\n//               event.preventDefault();\n\n//               if (materialsQuery.isFetchingNextPage) {\n//                 return;\n//               }\n\n//               materialsQuery.fetchNextPage();\n//             }}\n//           >\n//             {materialsQuery.isFetchingNextPage ? 'Loading...' : 'Load more'}\n//           </Select.Item>\n//         )}\n//       </Select.Content>\n//     </SelectField>\n//   );\n// };\n\nconst addColorFormSchema = z.object({\n  name: z.string().min(1),\n  hex_code: z.string().min(7).max(7),\n});\n\nconst AddColorDrawer: React.FC<{\n  materialId: string;\n  name: string;\n  children: React.ReactNode;\n}> = ({ materialId, name, children }) => {\n  const queryClient = useQueryClient();\n  const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);\n  const createColorMutation = useCreateColorMutation(materialId, {\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        predicate: (query) =>\n          query.queryKey.length >= 3 &&\n          query.queryKey[0] === 'product' &&\n          query.queryKey[2] === 'fashion',\n      });\n      setIsDrawerOpen(false);\n    },\n  });\n\n  return (\n    <Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>\n      <Drawer.Trigger asChild>{children}</Drawer.Trigger>\n      <Drawer.Content>\n        <Drawer.Header>\n          <Drawer.Title>Add new color</Drawer.Title>\n        </Drawer.Header>\n        <Drawer.Body className=\"p-4\">\n          <Form\n            schema={addColorFormSchema}\n            onSubmit={async (values) => {\n              createColorMutation.mutate(values);\n            }}\n            defaultValues={{\n              name,\n            }}\n            formProps={{\n              id: `material-${materialId}-add-color-${name\n                .toLowerCase()\n                .replace(/[^\\w]/g, '-')}`,\n            }}\n          >\n            <div className=\"flex flex-col gap-4\">\n              <fieldset disabled>\n                <InputField name=\"name\" label=\"Name\" />\n              </fieldset>\n              <InputField\n                name=\"hex_code\"\n                label=\"Hex code\"\n                type=\"color\"\n                inputProps={{\n                  className: 'max-w-8',\n                }}\n              />\n            </div>\n          </Form>\n        </Drawer.Body>\n        <Drawer.Footer>\n          <Drawer.Close asChild>\n            <Button variant=\"secondary\">Cancel</Button>\n          </Drawer.Close>\n          <Button\n            type=\"submit\"\n            form={`material-${materialId}-add-color-${name\n              .toLowerCase()\n              .replace(/[^\\w]/g, '-')}`}\n            isLoading={createColorMutation.isPending}\n            disabled={createColorMutation.isPending}\n          >\n            Save\n          </Button>\n        </Drawer.Footer>\n      </Drawer.Content>\n    </Drawer>\n  );\n};\n\nconst ProductFashionWidget = withQueryClient(\n  ({ data }: DetailWidgetProps<AdminProduct>) => {\n    const productFashion = useQuery({\n      queryKey: ['product', data.id, 'fashion'],\n      queryFn: async ({ signal }) => {\n        const res = await fetch(`/admin/products/${data.id}/fashion`, {\n          credentials: 'include',\n          signal,\n        });\n        return res.json() as Promise<{\n          missing_materials: string[];\n          materials: (MaterialModelType & { missing_colors: string[] })[];\n        }>;\n      },\n    });\n    const createMaterialMutation = useCreateMaterialMutation({\n      onSuccess: () => {\n        productFashion.refetch();\n      },\n    });\n\n    const materialsData = [\n      ...(productFashion.data?.missing_materials ?? []),\n      ...(productFashion.data?.materials ?? []),\n    ].sort((a, b) => {\n      const aName = typeof a === 'string' ? a : a.name;\n      const bName = typeof b === 'string' ? b : b.name;\n\n      return aName.localeCompare(bName);\n    });\n\n    return (\n      <Container className=\"divide-y p-0\">\n        <div className=\"flex flex-row items-center justify-between px-6 py-4 gap-6\">\n          <Heading>Materials &amp; Colors</Heading>\n          <IconButton\n            variant=\"transparent\"\n            className=\"text-fg-muted dark:text-fg-muted-dark hover:text-fg-subtle dark:hover:text-fg-subtle-dark\"\n            onClick={(event) => {\n              event.preventDefault();\n              productFashion.refetch();\n            }}\n            disabled={productFashion.isFetching}\n            isLoading={productFashion.isFetching}\n          >\n            <ArrowPath />\n          </IconButton>\n        </div>\n        <div className=\"text-fg-subtle dark:text-fg-subtle-dark px-6 py-4\">\n          {productFashion.isLoading ? (\n            <Text>Loading...</Text>\n          ) : productFashion.isError ? (\n            <Text>Error loading product materials</Text>\n          ) : productFashion.isSuccess &&\n            productFashion.data &&\n            !materialsData.length ? (\n            <Text>No product variants with Material option</Text>\n          ) : productFashion.isSuccess && productFashion.data ? (\n            <div className=\"flex flex-col gap-8\">\n              {materialsData.map((material) => (\n                <div\n                  key={typeof material === 'string' ? material : material.id}\n                  className=\"flex flex-col gap-1\"\n                >\n                  <Text>\n                    <strong\n                      className={\n                        typeof material === 'string'\n                          ? 'border-b border-dashed border-button-danger dark:border-button-danger-dark'\n                          : undefined\n                      }\n                    >\n                      {typeof material === 'string' ? material : material.name}\n                    </strong>\n                  </Text>\n                  {typeof material === 'string' ? (\n                    <Button\n                      variant=\"secondary\"\n                      onClick={(event) => {\n                        event.preventDefault();\n                        createMaterialMutation.mutate({\n                          name: material,\n                        });\n                      }}\n                    >\n                      Create material\n                    </Button>\n                  ) : (\n                    <div className=\"flex flex-row gap-4\">\n                      {material.colors.map((color) => (\n                        <div\n                          key={color.id}\n                          className=\"flex flex-col items-center gap-1\"\n                        >\n                          <div\n                            style={{ backgroundColor: color.hex_code }}\n                            className=\"w-10 h-10 border-2 border-grayscale-40 rounded-full\"\n                          />\n                          <Text>{color.name}</Text>\n                        </div>\n                      ))}\n                      {material.missing_colors.map((color) => (\n                        <div\n                          key={color}\n                          className=\"flex flex-col items-center gap-1\"\n                        >\n                          <AddColorDrawer materialId={material.id} name={color}>\n                            <IconButton\n                              variant=\"transparent\"\n                              className=\"w-10 h-10 bg-grayscale-20 border-2 border-dashed border-button-danger dark:border-button-danger-dark rounded-full\"\n                            >\n                              <PlusMini />\n                            </IconButton>\n                          </AddColorDrawer>\n                          {/* <div className=\"w-10 h-10 bg-grayscale-20 border-2 border-dashed border-button-danger dark:border-button-danger-dark rounded-full\" /> */}\n                          <Text>{color}</Text>\n                        </div>\n                      ))}\n                    </div>\n                  )}\n                </div>\n              ))}\n            </div>\n          ) : (\n            <Text>No fashion details set</Text>\n          )}\n        </div>\n      </Container>\n    );\n  }\n);\n\nexport const config = defineWidgetConfig({\n  zone: 'product.details.side.before',\n});\n\nexport default ProductFashionWidget;\n"
  },
  {
    "path": "medusa/src/admin/widgets/product-type-details.tsx",
    "content": "import * as React from 'react';\nimport { defineWidgetConfig } from '@medusajs/admin-sdk';\nimport { DetailWidgetProps, AdminCollection } from '@medusajs/framework/types';\nimport { Container, Heading, Button, Drawer, Text } from '@medusajs/ui';\nimport { PencilSquare } from '@medusajs/icons';\nimport { z } from 'zod';\nimport { ImageField, imageFieldSchema } from '../components/Form/ImageField';\nimport { Form } from '../components/Form/Form';\n\nconst detailsFormSchema = z.object({\n  image: imageFieldSchema().optional(),\n});\n\nconst UpdateDetailsDrawer: React.FC<{\n  children: React.ReactNode;\n  isOpen: boolean;\n  onOpenChange: (open: boolean) => void;\n  id: string;\n  title: React.ReactNode;\n  initialValue: z.infer<typeof detailsFormSchema>;\n  onSave: (values: z.infer<typeof detailsFormSchema>) => void;\n}> = ({ children, isOpen, onOpenChange, id, title, initialValue, onSave }) => {\n  return (\n    <Drawer open={isOpen} onOpenChange={onOpenChange}>\n      <Drawer.Trigger asChild>{children}</Drawer.Trigger>\n      <Drawer.Content>\n        <Drawer.Header>\n          <Drawer.Title>{title}</Drawer.Title>\n        </Drawer.Header>\n        <Drawer.Body className=\"p-4\">\n          <Form\n            schema={detailsFormSchema}\n            onSubmit={async (values) => {\n              await fetch(`/admin/custom/product-types/${id}/details`, {\n                method: 'POST',\n                body: JSON.stringify(values),\n                credentials: 'include',\n              });\n\n              onSave(values);\n            }}\n            defaultValues={initialValue}\n            formProps={{\n              id: `edit-product-type-${id}-fields`,\n            }}\n          >\n            <div className=\"flex flex-col gap-4\">\n              <ImageField\n                name=\"image\"\n                label=\"Image\"\n                dropzoneRootClassName=\"h-60\"\n              />\n            </div>\n          </Form>\n        </Drawer.Body>\n        <Drawer.Footer>\n          <Drawer.Close asChild>\n            <Button variant=\"secondary\">Cancel</Button>\n          </Drawer.Close>\n          <Button type=\"submit\" form={`edit-product-type-${id}-fields`}>\n            Save\n          </Button>\n        </Drawer.Footer>\n      </Drawer.Content>\n    </Drawer>\n  );\n};\n\nconst ProductTypeDetailsWidget = ({\n  data,\n}: DetailWidgetProps<AdminCollection>) => {\n  const [isEditModalOpen, setIsModalOpen] = React.useState(false);\n  const [details, setDetails] = React.useState<z.infer<\n    typeof detailsFormSchema\n  > | null>(null);\n\n  React.useEffect(() => {\n    fetch(`/admin/custom/product-types/${data.id}/details`, {\n      credentials: 'include',\n    })\n      .then((res) => res.json())\n      .then((json) => {\n        setDetails(json);\n      })\n      .catch((e) => {\n        console.error(e);\n      });\n  }, [data.id]);\n\n  return (\n    <Container className=\"divide-y p-0\">\n      <div className=\"flex items-center justify-between px-6 py-4\">\n        <Heading>Details</Heading>\n        {details !== null && (\n          <UpdateDetailsDrawer\n            isOpen={isEditModalOpen}\n            onOpenChange={setIsModalOpen}\n            title=\"Update description\"\n            id={data.id}\n            initialValue={details}\n            onSave={(value) => {\n              setDetails(value);\n              setIsModalOpen(false);\n            }}\n          >\n            <Button\n              variant=\"transparent\"\n              size=\"small\"\n              className=\"text-fg-muted dark:text-fg-muted-dark hover:text-fg-subtle dark:hover:text-fg-subtle-dark\"\n              onClick={(event) => {\n                event.preventDefault();\n                setIsModalOpen(true);\n              }}\n            >\n              <PencilSquare /> Edit\n            </Button>\n          </UpdateDetailsDrawer>\n        )}\n      </div>\n      <div className=\"text-fg-subtle dark:text-fg-subtle-dark grid grid-cols-2 items-center px-6 py-4\">\n        {details === null ? (\n          <Text>Loading...</Text>\n        ) : (\n          <div className=\"flex flex-col gap-2\">\n            {typeof details.image?.url === 'string' ? (\n              <div>\n                <img\n                  src={details.image.url}\n                  className=\"max-h-60 max-w-none w-auto\"\n                />\n              </div>\n            ) : (\n              <Text>No image</Text>\n            )}\n          </div>\n        )}\n      </div>\n    </Container>\n  );\n};\n\nexport const config = defineWidgetConfig({\n  zone: 'product_type.details.after',\n});\n\nexport default ProductTypeDetailsWidget;\n"
  },
  {
    "path": "medusa/src/api/README.md",
    "content": "# Custom API Routes\n\nAn API Route is a REST API endpoint.\n\nAn API Route is created in a TypeScript or JavaScript file under the `/src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`.\n\nFor example, to create a `GET` API Route at `/store/hello-world`, create the file `src/api/store/hello-world/route.ts` with the following content:\n\n```ts\nimport type { MedusaRequest, MedusaResponse } from \"@medusajs/medusa\";\n\nexport async function GET(req: MedusaRequest, res: MedusaResponse) {\n  res.json({\n    message: \"Hello world!\",\n  });\n}\n```\n\n## Supported HTTP methods\n\nThe file based routing supports the following HTTP methods:\n\n- GET\n- POST\n- PUT\n- PATCH\n- DELETE\n- OPTIONS\n- HEAD\n\nYou can define a handler for each of these methods by exporting a function with the name of the method in the paths `route.ts` file.\n\nFor example:\n\n```ts\nimport type { MedusaRequest, MedusaResponse } from \"@medusajs/medusa\";\n\nexport async function GET(req: MedusaRequest, res: MedusaResponse) {\n  // Handle GET requests\n}\n\nexport async function POST(req: MedusaRequest, res: MedusaResponse) {\n  // Handle POST requests\n}\n\nexport async function PUT(req: MedusaRequest, res: MedusaResponse) {\n  // Handle PUT requests\n}\n```\n\n## Parameters\n\nTo create an API route that accepts a path parameter, create a directory within the route's path whose name is of the format `[param]`.\n\nFor example, if you want to define a route that takes a `productId` parameter, you can do so by creating a file called `/api/products/[productId]/route.ts`:\n\n```ts\nimport type {\n  MedusaRequest,\n  MedusaResponse,\n} from \"@medusajs/medusa\"\n\nexport async function GET(req: MedusaRequest, res: MedusaResponse) {\n  const { productId } = req.params;\n\n  res.json({\n    message: `You're looking for product ${productId}`\n  })\n}\n```\n\nTo create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`.\n\nFor example, if you want to define a route that takes both a `productId` and a `variantId` parameter, you can do so by creating a file called `/api/products/[productId]/variants/[variantId]/route.ts`.\n\n## Using the container\n\nThe Medusa container is available on `req.scope`. Use it to access modules' main services and other registered resources:\n\n```ts\nimport type {\n  MedusaRequest,\n  MedusaResponse,\n} from \"@medusajs/medusa\"\nimport { IProductModuleService } from \"@medusajs/framework/types\"\nimport { Modules } from \"@medusajs/framework/utils\"\n\nexport const GET = async (\n  req: MedusaRequest,\n  res: MedusaResponse\n) => {\n  const productModuleService: IProductModuleService =\n    req.scope.resolve(Modules.PRODUCT)\n\n  const [, count] = await productModuleService.listAndCount()\n\n  res.json({\n    count,\n  })\n}\n```\n\n## Middleware\n\nYou can apply middleware to your routes by creating a file called `/api/middlewares.ts`. This file must export a configuration object with what middleware you want to apply to which routes.\n\nFor example, if you want to apply a custom middleware function to the `/store/custom` route, you can do so by adding the following to your `/api/middlewares.ts` file:\n\n```ts\nimport { defineMiddlewares } from \"@medusajs/medusa\"\nimport type {\n  MedusaRequest,\n  MedusaResponse,\n  MedusaNextFunction,\n} from \"@medusajs/medusa\";\n\nasync function logger(\n  req: MedusaRequest,\n  res: MedusaResponse,\n  next: MedusaNextFunction\n) {\n  console.log(\"Request received\");\n  next();\n}\n\nexport default defineMiddlewares({\n  routes: [\n    {\n      matcher: \"/store/custom\",\n      middlewares: [logger],\n    },\n  ],\n})\n```\n\nThe `matcher` property can be either a string or a regular expression. The `middlewares` property accepts an array of middleware functions.\n"
  },
  {
    "path": "medusa/src/api/admin/custom/collections/[collectionId]/details/route.ts",
    "content": "import { Modules } from '@medusajs/framework/utils';\nimport { MedusaRequest, MedusaResponse } from '@medusajs/framework';\nimport { z } from '@medusajs/framework/zod';\n\nconst collectionFieldsMetadataSchema = z.object({\n  image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n  description: z.string().optional(),\n  collection_page_image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n  collection_page_heading: z.string().optional(),\n  collection_page_content: z.string().optional(),\n  product_page_heading: z.string().optional(),\n  product_page_image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n  product_page_wide_image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n  product_page_cta_image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n  product_page_cta_heading: z.string().optional(),\n  product_page_cta_link: z.string().optional(),\n});\n\nexport async function GET(\n  req: MedusaRequest,\n  res: MedusaResponse,\n): Promise<void> {\n  const { collectionId } = req.params;\n  const productService = req.scope.resolve(Modules.PRODUCT);\n  const collection =\n    await productService.retrieveProductCollection(collectionId);\n\n  const parsed = collectionFieldsMetadataSchema.safeParse(\n    collection.metadata ?? {},\n  );\n\n  res.json({\n    image: parsed.success && parsed.data.image ? parsed.data.image : null,\n    description:\n      parsed.success && parsed.data.description ? parsed.data.description : '',\n    collection_page_image:\n      parsed.success && parsed.data.collection_page_image\n        ? parsed.data.collection_page_image\n        : null,\n    collection_page_heading:\n      parsed.success && parsed.data.collection_page_heading\n        ? parsed.data.collection_page_heading\n        : '',\n    collection_page_content:\n      parsed.success && parsed.data.collection_page_content\n        ? parsed.data.collection_page_content\n        : '',\n    product_page_heading:\n      parsed.success && parsed.data.product_page_heading\n        ? parsed.data.product_page_heading\n        : '',\n    product_page_image:\n      parsed.success && parsed.data.product_page_image\n        ? parsed.data.product_page_image\n        : null,\n    product_page_wide_image:\n      parsed.success && parsed.data.product_page_wide_image\n        ? parsed.data.product_page_wide_image\n        : null,\n    product_page_cta_image:\n      parsed.success && parsed.data.product_page_cta_image\n        ? parsed.data.product_page_cta_image\n        : null,\n    product_page_cta_heading:\n      parsed.success && parsed.data.product_page_cta_heading\n        ? parsed.data.product_page_cta_heading\n        : '',\n    product_page_cta_link:\n      parsed.success && parsed.data.product_page_cta_link\n        ? parsed.data.product_page_cta_link\n        : '',\n  });\n}\n\nexport async function POST(\n  req: MedusaRequest<typeof collectionFieldsMetadataSchema>,\n  res: MedusaResponse,\n): Promise<void> {\n  const { collectionId } = req.params;\n  const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;\n  const customFields = collectionFieldsMetadataSchema.parse(body);\n\n  const productService = req.scope.resolve(Modules.PRODUCT);\n  const collection =\n    await productService.retrieveProductCollection(collectionId);\n\n  const updatedCollection = await productService.updateProductCollections(\n    collectionId,\n    {\n      metadata: {\n        ...collection.metadata,\n        ...customFields,\n      },\n    },\n  );\n\n  res.json(updatedCollection);\n}\n"
  },
  {
    "path": "medusa/src/api/admin/custom/index-products/route.ts",
    "content": "import {\n  AuthenticatedMedusaRequest,\n  MedusaResponse,\n} from '@medusajs/framework';\nimport { indexProductsWorkflow } from '../../../../workflows/index-products';\n\nexport async function POST(\n  req: AuthenticatedMedusaRequest,\n  res: MedusaResponse,\n): Promise<void> {\n  const result = await indexProductsWorkflow(req.scope).run();\n\n  res.json(result);\n}\n"
  },
  {
    "path": "medusa/src/api/admin/custom/product-types/[productTypeId]/details/route.ts",
    "content": "import { Modules } from '@medusajs/framework/utils';\nimport { MedusaRequest, MedusaResponse } from '@medusajs/framework';\nimport { z } from '@medusajs/framework/zod';\n\nconst productTypeFieldsMetadataSchema = z.object({\n  image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n});\n\nexport async function GET(\n  req: MedusaRequest,\n  res: MedusaResponse,\n): Promise<void> {\n  const { productTypeId } = req.params;\n  const productService = req.scope.resolve(Modules.PRODUCT);\n  const productType = await productService.retrieveProductType(productTypeId);\n\n  const parsed = productTypeFieldsMetadataSchema.safeParse(\n    productType.metadata ?? {},\n  );\n\n  res.json({\n    image: parsed.success && parsed.data.image ? parsed.data.image : null,\n  });\n}\n\nexport async function POST(\n  req: MedusaRequest,\n  res: MedusaResponse,\n): Promise<void> {\n  const { productTypeId } = req.params;\n  const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;\n  const customFields = productTypeFieldsMetadataSchema.parse(body);\n\n  const productService = req.scope.resolve(Modules.PRODUCT);\n  const productType = await productService.retrieveProductType(productTypeId);\n\n  const updatedProductType = await productService.updateProductTypes(\n    productTypeId,\n    {\n      metadata: {\n        ...productType.metadata,\n        ...customFields,\n      },\n    },\n  );\n\n  res.json(updatedProductType);\n}\n"
  },
  {
    "path": "medusa/src/api/admin/fashion/[id]/colors/[colorId]/restore/route.ts",
    "content": "import { MedusaRequest, MedusaResponse } from '@medusajs/framework';\nimport FashionModuleService from '../../../../../../../modules/fashion/service';\nimport { FASHION_MODULE } from '../../../../../../../modules/fashion';\n\nexport const POST = async (req: MedusaRequest, res: MedusaResponse) => {\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  await fashionModuleService.retrieveMaterial(req.params.id, {\n    withDeleted: true,\n  });\n\n  await fashionModuleService.restoreColors(req.params.colorId);\n\n  const color = await fashionModuleService.retrieveColor(req.params.colorId, {\n    withDeleted: true,\n  });\n\n  res.status(200).json(color);\n};\n"
  },
  {
    "path": "medusa/src/api/admin/fashion/[id]/colors/[colorId]/route.ts",
    "content": "import { MedusaRequest, MedusaResponse } from '@medusajs/framework';\nimport { z } from '@medusajs/framework/zod';\nimport FashionModuleService from '../../../../../../modules/fashion/service';\nimport { FASHION_MODULE } from '../../../../../../modules/fashion';\n\nexport const GET = async (req: MedusaRequest, res: MedusaResponse) => {\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  await fashionModuleService.retrieveMaterial(req.params.id, {\n    withDeleted: true,\n  });\n\n  const color = await fashionModuleService.retrieveColor(req.params.colorId, {\n    withDeleted: true,\n  });\n\n  res.status(200).json(color);\n};\n\nconst colorsUpdateBodySchema = z.object({\n  name: z.string().min(1),\n  hex_code: z\n    .string()\n    .min(1)\n    .transform((val) => val.toUpperCase())\n    .refine((val) => /^#([A-F0-9]{6}|[A-F0-9]{3})$/.test(val), {\n      message: 'Invalid hex code',\n    }),\n});\n\nexport const POST = async (req: MedusaRequest, res: MedusaResponse) => {\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  await fashionModuleService.retrieveMaterial(req.params.id, {\n    withDeleted: true,\n  });\n\n  const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;\n  const validatedData = colorsUpdateBodySchema.parse(body);\n\n  const color = await fashionModuleService.updateColors({\n    ...validatedData,\n    id: req.params.colorId,\n  });\n\n  res.status(200).json(color);\n};\n\nexport const DELETE = async (req: MedusaRequest, res: MedusaResponse) => {\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  await fashionModuleService.retrieveMaterial(req.params.id, {\n    withDeleted: true,\n  });\n\n  await fashionModuleService.softDeleteColors(req.params.colorId);\n\n  const color = await fashionModuleService.retrieveColor(req.params.colorId, {\n    withDeleted: true,\n  });\n\n  res.status(200).json(color);\n};\n"
  },
  {
    "path": "medusa/src/api/admin/fashion/[id]/colors/route.ts",
    "content": "import { z } from '@medusajs/framework/zod';\nimport { MedusaRequest, MedusaResponse } from '@medusajs/framework';\nimport FashionModuleService from '../../../../../modules/fashion/service';\nimport { FASHION_MODULE } from '../../../../../modules/fashion';\n\nconst colorsListQuerySchema = z.object({\n  page: z.coerce.number().min(1).optional().default(1),\n  deleted: z.coerce.boolean().optional().default(false),\n});\n\nexport const GET = async (req: MedusaRequest, res: MedusaResponse) => {\n  const { page, deleted } = colorsListQuerySchema.parse(req.query);\n\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  const [colors, count] = await fashionModuleService.listAndCountColors(\n    deleted\n      ? {\n          deleted_at: { $lte: new Date() },\n          material_id: req.params.id,\n        }\n      : {\n          material_id: req.params.id,\n        },\n    {\n      skip: 20 * (page - 1),\n      take: 20,\n      withDeleted: deleted,\n    },\n  );\n\n  const last_page = Math.ceil(count / 20);\n\n  res.status(200).json({ colors, count, page, last_page });\n};\n\nconst colorsCreateBodySchema = z.object({\n  name: z.string().min(1),\n  hex_code: z.string().min(7).max(7),\n});\n\nexport const POST = async (req: MedusaRequest, res: MedusaResponse) => {\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;\n  const validatedData = colorsCreateBodySchema.parse(body);\n\n  const color = await fashionModuleService.createColors({\n    ...validatedData,\n    material_id: req.params.id,\n  });\n\n  res.status(200).json(color);\n};\n"
  },
  {
    "path": "medusa/src/api/admin/fashion/[id]/restore/route.ts",
    "content": "import { MedusaRequest, MedusaResponse } from '@medusajs/framework';\nimport FashionModuleService from '../../../../../modules/fashion/service';\nimport { FASHION_MODULE } from '../../../../../modules/fashion';\n\nexport const POST = async (req: MedusaRequest, res: MedusaResponse) => {\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  await fashionModuleService.restoreMaterials(req.params.id);\n\n  const material = await fashionModuleService.retrieveMaterial(req.params.id, {\n    relations: ['colors'],\n    withDeleted: true,\n  });\n\n  res.status(200).json(material);\n};\n"
  },
  {
    "path": "medusa/src/api/admin/fashion/[id]/route.ts",
    "content": "import { MedusaRequest, MedusaResponse } from '@medusajs/framework';\nimport { z } from '@medusajs/framework/zod';\nimport FashionModuleService from '../../../../modules/fashion/service';\nimport { FASHION_MODULE } from '../../../../modules/fashion';\n\nexport const GET = async (req: MedusaRequest, res: MedusaResponse) => {\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  const material = await fashionModuleService.retrieveMaterial(req.params.id, {\n    relations: ['colors'],\n    withDeleted: true,\n  });\n\n  res.status(200).json(material);\n};\n\nconst updateMaterialBodySchema = z.object({\n  name: z.string().min(1),\n});\n\nexport const POST = async (req: MedusaRequest, res: MedusaResponse) => {\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;\n  const validatedData = updateMaterialBodySchema.parse(body);\n\n  const material = await fashionModuleService.updateMaterials({\n    ...validatedData,\n    id: req.params.id,\n  });\n\n  res.status(200).json(material);\n};\n\nexport const DELETE = async (req: MedusaRequest, res: MedusaResponse) => {\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  await fashionModuleService.softDeleteMaterials(req.params.id);\n\n  const material = await fashionModuleService.retrieveMaterial(req.params.id, {\n    relations: ['colors'],\n    withDeleted: true,\n  });\n\n  res.status(200).json(material);\n};\n"
  },
  {
    "path": "medusa/src/api/admin/fashion/route.ts",
    "content": "import { z } from '@medusajs/framework/zod';\nimport { MedusaRequest, MedusaResponse } from '@medusajs/framework';\nimport FashionModuleService from '../../../modules/fashion/service';\nimport { FASHION_MODULE } from '../../../modules/fashion';\n\nconst materialsListQuerySchema = z.object({\n  page: z.coerce.number().min(1).optional().default(1),\n  deleted: z.coerce.boolean().optional().default(false),\n});\n\nexport const GET = async (req: MedusaRequest, res: MedusaResponse) => {\n  const { page, deleted } = materialsListQuerySchema.parse(req.query);\n\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  const [materials, count] = await fashionModuleService.listAndCountMaterials(\n    deleted\n      ? {\n          deleted_at: { $lte: new Date() },\n        }\n      : undefined,\n    {\n      skip: 20 * (page - 1),\n      take: 20,\n      withDeleted: deleted,\n      relations: ['colors'],\n    },\n  );\n\n  const last_page = Math.ceil(count / 20);\n\n  res.status(200).json({ materials, count, page, last_page });\n};\n\nconst createMaterialBodySchema = z.object({\n  name: z.string().min(1),\n});\n\nexport const POST = async (req: MedusaRequest, res: MedusaResponse) => {\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;\n  const validatedData = createMaterialBodySchema.parse(body);\n\n  const material = await fashionModuleService.createMaterials(validatedData);\n\n  res.status(201).json(material);\n};\n"
  },
  {
    "path": "medusa/src/api/admin/products/[id]/fashion/route.ts",
    "content": "import { MedusaRequest, MedusaResponse } from '@medusajs/framework';\nimport { Modules } from '@medusajs/framework/utils';\nimport { IProductModuleService } from '@medusajs/framework/types';\nimport { FASHION_MODULE } from '../../../../../modules/fashion';\nimport FashionModuleService from '../../../../../modules/fashion/service';\n\nexport const GET = async (req: MedusaRequest, res: MedusaResponse) => {\n  const productModuleService: IProductModuleService = req.scope.resolve(\n    Modules.PRODUCT,\n  );\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  const product = await productModuleService.retrieveProduct(req.params.id, {\n    relations: ['options', 'variants', 'variants.options'],\n  });\n\n  const materialOption = product.options.find(\n    (option) => option.title === 'Material',\n  );\n  const colorOption = product.options.find(\n    (option) => option.title === 'Color',\n  );\n\n  const materialsAndColorsNamesTree = new Map<string, string[]>();\n\n  for (const productVariant of product.variants) {\n    const materialName = productVariant.options.find(\n      (option) => option.option_id === materialOption.id,\n    )?.value;\n\n    if (!materialName) {\n      continue;\n    }\n\n    const colorNames = productVariant.options\n      .filter((option) => option.option_id === colorOption.id)\n      .map((option) => option.value);\n\n    if (!materialsAndColorsNamesTree.has(materialName)) {\n      materialsAndColorsNamesTree.set(materialName, colorNames);\n    } else {\n      const existingColorNames = materialsAndColorsNamesTree.get(materialName);\n\n      materialsAndColorsNamesTree.set(\n        materialName,\n        Array.from(new Set([...existingColorNames, ...colorNames])),\n      );\n    }\n  }\n\n  const materials = await fashionModuleService.listMaterials(\n    {\n      name: Array.from(materialsAndColorsNamesTree.keys()),\n    },\n    {\n      relations: ['colors'],\n    },\n  );\n\n  res.status(200).json({\n    missing_materials: Array.from(materialsAndColorsNamesTree.keys()).filter(\n      (materialName) =>\n        materials.every((material) => material.name !== materialName),\n    ),\n    materials: materials.map((material) => ({\n      ...material,\n      colors: material.colors.filter((color) =>\n        materialsAndColorsNamesTree.get(material.name).includes(color.name),\n      ),\n      missing_colors: materialsAndColorsNamesTree\n        .get(material.name)\n        .filter((colorName) =>\n          material.colors.every((color) => color.name !== colorName),\n        ),\n    })),\n  });\n};\n"
  },
  {
    "path": "medusa/src/api/middlewares.ts",
    "content": "import { defineMiddlewares } from '@medusajs/medusa';\nimport { adminProductTypeRoutesMiddlewares } from './store/custom/product-types/middlewares';\nimport { authenticate } from '@medusajs/framework';\n\nexport default defineMiddlewares([\n  ...adminProductTypeRoutesMiddlewares,\n  {\n    method: 'ALL',\n    matcher: '/store/custom/customer/*',\n    middlewares: [authenticate('customer', ['session', 'bearer'])],\n  },\n]);\n"
  },
  {
    "path": "medusa/src/api/store/custom/customer/send-welcome-email/route.ts",
    "content": "import {\n  AuthenticatedMedusaRequest,\n  MedusaResponse,\n} from '@medusajs/framework';\nimport emitCustomerWelcomeEvent from '../../../../../workflows/emit-customer-welcome-event';\n\nexport const POST = async (\n  req: AuthenticatedMedusaRequest,\n  res: MedusaResponse,\n) => {\n  const customerId = req.auth_context.actor_id;\n\n  await emitCustomerWelcomeEvent(req.scope).run({\n    input: {\n      id: customerId,\n    },\n  });\n\n  res.status(200).json({ success: true });\n};\n"
  },
  {
    "path": "medusa/src/api/store/custom/fashion/[productHandle]/route.ts",
    "content": "import { MedusaRequest, MedusaResponse } from '@medusajs/framework';\nimport { Modules } from '@medusajs/framework/utils';\nimport { IProductModuleService } from '@medusajs/framework/types';\nimport { FASHION_MODULE } from '../../../../../modules/fashion';\nimport FashionModuleService from '../../../../../modules/fashion/service';\n\nexport const GET = async (req: MedusaRequest, res: MedusaResponse) => {\n  const productModuleService: IProductModuleService = req.scope.resolve(\n    Modules.PRODUCT,\n  );\n  const fashionModuleService: FashionModuleService =\n    req.scope.resolve(FASHION_MODULE);\n\n  const [product] = await productModuleService.listProducts(\n    {\n      handle: req.params.productHandle,\n    },\n    {\n      relations: ['options', 'variants', 'variants.options'],\n      take: 1,\n    },\n  );\n\n  const materialOption = product.options.find(\n    (option) => option.title === 'Material',\n  );\n  const colorOption = product.options.find(\n    (option) => option.title === 'Color',\n  );\n\n  if (!materialOption || !colorOption) {\n    res.status(200).json({\n      materials: [],\n    });\n    return;\n  }\n\n  const materialsAndColorsNamesTree = new Map<string, string[]>();\n\n  for (const productVariant of product.variants) {\n    const materialName = productVariant.options.find(\n      (option) => option.option_id === materialOption.id,\n    )?.value;\n\n    if (!materialName) {\n      continue;\n    }\n\n    const colorNames = productVariant.options\n      .filter((option) => option.option_id === colorOption.id)\n      .map((option) => option.value);\n\n    if (!materialsAndColorsNamesTree.has(materialName)) {\n      materialsAndColorsNamesTree.set(materialName, colorNames);\n    } else {\n      const existingColorNames = materialsAndColorsNamesTree.get(materialName);\n\n      materialsAndColorsNamesTree.set(\n        materialName,\n        Array.from(new Set([...existingColorNames, ...colorNames])),\n      );\n    }\n  }\n\n  const materials = await fashionModuleService.listMaterials(\n    {\n      name: Array.from(materialsAndColorsNamesTree.keys()),\n    },\n    {\n      relations: ['colors'],\n    },\n  );\n\n  res.status(200).json({\n    materials: materials.map((material) => ({\n      id: material.id,\n      name: material.name,\n      colors: material.colors\n        .filter((color) =>\n          materialsAndColorsNamesTree.get(material.name).includes(color.name),\n        )\n        .map((color) => ({\n          id: color.id,\n          name: color.name,\n          hex_code: color.hex_code,\n        })),\n    })),\n  });\n};\n"
  },
  {
    "path": "medusa/src/api/store/custom/product-types/[id]/route.ts",
    "content": "import { refetchProductType } from '../helpers';\nimport { AdminGetProductTypeParamsType } from '../validators';\nimport { ProductTypeDTO } from '@medusajs/framework/types';\nimport {\n  AuthenticatedMedusaRequest,\n  MedusaResponse,\n} from '@medusajs/framework';\n\nexport const GET = async (\n  req: AuthenticatedMedusaRequest<AdminGetProductTypeParamsType>,\n  res: MedusaResponse\n) => {\n  const productType = await refetchProductType(\n    req.params.id,\n    req.scope,\n    req.remoteQueryConfig.fields as (keyof ProductTypeDTO)[],\n  );\n\n  res.status(200).json({ product_type: productType });\n};\n"
  },
  {
    "path": "medusa/src/api/store/custom/product-types/helpers.ts",
    "content": "import { MedusaContainer, ProductTypeDTO } from \"@medusajs/framework/types\"\n\nexport const refetchProductType = async (\n  productTypeId: string,\n  scope: MedusaContainer,\n  fields: (keyof ProductTypeDTO)[]\n) => {\n  const query = scope.resolve(\"query\")\n  const { data: [ productType ] } = await query.graph({\n    entity: \"product_type\",\n    filters: { id: productTypeId },\n    fields,\n  })\n\n  return productType \n}\n"
  },
  {
    "path": "medusa/src/api/store/custom/product-types/middlewares.ts",
    "content": "import * as QueryConfig from './query-config';\nimport {\n  MiddlewareRoute,\n  validateAndTransformQuery,\n} from '@medusajs/framework/http';\nimport {\n  AdminGetProductTypeParams,\n  AdminGetProductTypesParams,\n} from './validators';\n\nexport const adminProductTypeRoutesMiddlewares: MiddlewareRoute[] = [\n  {\n    method: ['GET'],\n    matcher: '/store/custom/product-types',\n    middlewares: [\n      validateAndTransformQuery(\n        AdminGetProductTypesParams,\n        QueryConfig.listProductTypesTransformQueryConfig,\n      ),\n    ],\n  },\n  {\n    method: ['GET'],\n    matcher: '/store/custom/product-types/:id',\n    middlewares: [\n      validateAndTransformQuery(\n        AdminGetProductTypeParams,\n        QueryConfig.retrieveProductTypeTransformQueryConfig,\n      ),\n    ],\n  },\n];\n"
  },
  {
    "path": "medusa/src/api/store/custom/product-types/query-config.ts",
    "content": "export const defaultAdminProductTypeFields = [\n  \"id\",\n  \"value\",\n  \"created_at\",\n  \"updated_at\",\n]\n\nexport const retrieveProductTypeTransformQueryConfig = {\n  defaults: defaultAdminProductTypeFields,\n  isList: false,\n}\n\nexport const listProductTypesTransformQueryConfig = {\n  ...retrieveProductTypeTransformQueryConfig,\n  defaultLimit: 20,\n  isList: true,\n}\n"
  },
  {
    "path": "medusa/src/api/store/custom/product-types/route.ts",
    "content": "import { HttpTypes, ProductTypeDTO } from '@medusajs/framework/types';\nimport {\n  AuthenticatedMedusaRequest,\n  MedusaResponse,\n} from '@medusajs/framework';\n\nexport const GET = async (\n  req: AuthenticatedMedusaRequest<HttpTypes.AdminProductTypeListParams>,\n  res: MedusaResponse,\n) => {\n  const query = req.scope.resolve(\"query\")\n  const { data: productTypes, metadata } = await query.graph({\n    entity: \"product_types\",\n    filters: req.filterableFields,\n    fields: req.remoteQueryConfig.fields as (keyof ProductTypeDTO)[],\n    pagination: req.remoteQueryConfig.pagination\n  })\n\n  res.json({\n    product_types: productTypes,\n    count: metadata.count,\n    offset: metadata.skip,\n    limit: metadata.take,\n  });\n};\n"
  },
  {
    "path": "medusa/src/api/store/custom/product-types/validators.ts",
    "content": "import {\n  createSelectParams,\n  createFindParams,\n  createOperatorMap,\n} from '@medusajs/medusa/api/utils/validators';\nimport { z } from '@medusajs/framework/zod';\n\nexport type AdminGetProductTypeParamsType = z.infer<\n  typeof AdminGetProductTypeParams\n>;\nexport const AdminGetProductTypeParams = createSelectParams();\n\nexport type AdminGetProductTypesParamsType = z.infer<\n  typeof AdminGetProductTypesParams\n>;\nexport const AdminGetProductTypesParams = createFindParams({\n  limit: 10,\n  offset: 0,\n}).merge(\n  z.object({\n    q: z.string().optional(),\n    id: z.union([z.string(), z.array(z.string())]).optional(),\n    value: z.union([z.string(), z.array(z.string())]).optional(),\n    // TODO: To be added in next iteration\n    // discount_condition_id: z.string().nullish(),\n    created_at: createOperatorMap().optional(),\n    updated_at: createOperatorMap().optional(),\n    deleted_at: createOperatorMap().optional(),\n    $and: z.lazy(() => AdminGetProductTypesParams.array()).optional(),\n    $or: z.lazy(() => AdminGetProductTypesParams.array()).optional(),\n  }),\n);\n"
  },
  {
    "path": "medusa/src/api/store/custom/stripe/get-payment-method/[id]/route.ts",
    "content": "import { MedusaResponse, MedusaStoreRequest } from \"@medusajs/framework\";\nimport Stripe from \"stripe\";\n\nconst stripe = new Stripe(process.env.STRIPE_API_KEY);\n\nexport const GET = async (req: MedusaStoreRequest, res: MedusaResponse) => {\n  const { id } = req.params;\n\n  const paymentMethod = await stripe.paymentMethods.retrieve(id);\n  res.status(200).json(paymentMethod);\n};\n"
  },
  {
    "path": "medusa/src/api/store/custom/stripe/set-payment-method/route.ts",
    "content": "import { MedusaResponse, MedusaStoreRequest } from \"@medusajs/framework\";\nimport { IPaymentModuleService } from \"@medusajs/framework/types\";\nimport { Modules } from \"@medusajs/framework/utils\";\nimport Stripe from \"stripe\";\n\nconst stripe = new Stripe(process.env.STRIPE_API_KEY);\n\nexport const POST = async (\n  req: MedusaStoreRequest<{\n    session_id: string;\n    token: string;\n  }>,\n  res: MedusaResponse\n) => {\n  const paymentModuleService: IPaymentModuleService = req.scope.resolve(\n    Modules.PAYMENT\n  );\n\n  const session = await paymentModuleService.retrievePaymentSession(\n    req.body.session_id\n  );\n\n  if (!req.body.token) {\n    await paymentModuleService.updatePaymentSession({\n      ...session,\n      data: {\n        ...session.data,\n        payment_method_id: null,\n      },\n    });\n    res.status(200).json({ success: true });\n  }\n\n  const paymentMethod = await stripe.paymentMethods.create({\n    type: \"card\",\n    card: { token: req.body.token },\n  });\n\n  await stripe.paymentIntents.update(session.data.id as string, {\n    payment_method: paymentMethod.id,\n  });\n  await paymentModuleService.updatePaymentSession({\n    ...session,\n    data: {\n      ...session.data,\n      payment_method_id: paymentMethod.id,\n    },\n  });\n  res.status(200).json({ success: true });\n};\n"
  },
  {
    "path": "medusa/src/jobs/README.md",
    "content": "# Custom scheduled jobs\n\nA scheduled job is a function executed at a specified interval of time in the background of your Medusa application.\n\nA scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory.\n\nFor example, create the file `src/jobs/hello-world.ts` with the following content:\n\n```ts\nimport {\n  IProductModuleService,\n  MedusaContainer\n} from \"@medusajs/framework/types\";\nimport { Modules } from \"@medusajs/framework/utils\";\n\nexport default async function myCustomJob(container: MedusaContainer) {\n  const productService: IProductModuleService = container.resolve(Modules.PRODUCT)\n\n  const products = await productService.listAndCountProducts();\n\n  // Do something with the products\n}\n\nexport const config = {\n  name: \"daily-product-report\",\n  schedule: \"0 0 * * *\", // Every day at midnight\n};\n```\n\nA scheduled job file must export:\n\n- The function to be executed whenever it’s time to run the scheduled job.\n- A configuration object defining the job. It has three properties:\n  - `name`: a unique name for the job.\n  - `schedule`: a [cron expression](https://crontab.guru/).\n  - `numberOfExecutions`: an optional integer, specifying how many times the job will execute before being removed\n\nThe `handler` is a function that accepts one parameter, `container`, which is a `MedusaContainer` instance used to resolve services.\n"
  },
  {
    "path": "medusa/src/links/README.md",
    "content": "# Module Links\n\nA module link forms an association between two data models of different modules, while maintaining module isolation.\n\nFor example:\n\n```ts\nimport HelloModule from \"../modules/hello\"\nimport ProductModule from \"@medusajs/medusa/product\"\nimport { defineLink } from \"@medusajs/framework/utils\"\n\nexport default defineLink(\n  ProductModule.linkable.product,\n  HelloModule.linkable.myCustom\n)\n```\n\nThis defines a link between the Product Module's `product` data model and the Hello Module (custom module)'s `myCustom` data model.\n\nLearn more about links in [this documentation](https://docs.medusajs.com/v2/advanced-development/modules/module-links)"
  },
  {
    "path": "medusa/src/modules/README.md",
    "content": "# Custom Module\n\nA module is a package of reusable functionalities. It can be integrated into your Medusa application without affecting the overall system.\n\nTo create a module:\n\n## 1. Create a Service\n\nA module must define a service. A service is a TypeScript or JavaScript class holding methods related to a business logic or commerce functionality.\n\nFor example, create the file `src/modules/hello/service.ts` with the following content:\n\n```ts title=\"src/modules/hello/service.ts\"\nexport default class HelloModuleService {\n  getMessage() {\n    return \"Hello, world!\"\n  }\n}\n```\n\n## 2. Export Module Definition\n\nA module must have an `index.ts` file in its root directory that exports its definition. The definition specifies the main service of the module.\n\nFor example, create the file `src/modules/hello.index.ts` with the following content:\n\n```ts title=\"src/modules/hello.index.ts\" highlights={[[\"4\", \"\", \"The main service of the module.\"]]}\nimport HelloModuleService from \"./service\"\nimport { Module } from \"@medusajs/framework/utils\"\n\nexport const HELLO_MODULE = \"helloModuleService\"\n\nexport default Module(HELLO_MODULE, {\n  service: HelloModuleService,\n})\n```\n\n## 3. Add Module to Configurations\n\nThe last step is to add the module in Medusa’s configurations.\n\nIn `medusa-config.js`, add the module to the `modules` object:\n\n```js title=\"medusa-config.js\"\nimport { HELLO_MODULE } from \"./src/modules/hello\"\n\nmodule.exports = defineConfig({\n  // ...\n  modules: {\n    [HELLO_MODULE]: {\n      resolve: \"./modules/hello\",\n    },\n  },\n})\n```\n\nIts key (`helloModuleService` or `HELLO_MODULE`) is the name of the module’s main service. It will be registered in the Medusa container with that name.\n\n## Use Module\n\nYou can resolve the main service of the module in other resources, such as an API route:\n\n```ts\nimport { MedusaRequest, MedusaResponse } from \"@medusajs/medusa\"\nimport HelloModuleService from \"../../../modules/hello/service\"\nimport { HELLO_MODULE } from \"../../../modules/hello\"\n\nexport async function GET(\n  req: MedusaRequest,\n  res: MedusaResponse\n): Promise<void> {\n  const helloModuleService: HelloModuleService = req.scope.resolve(\n    HELLO_MODULE\n  )\n\n  res.json({\n    message: helloModuleService.getMessage(),\n  })\n}\n```\n"
  },
  {
    "path": "medusa/src/modules/fashion/index.ts",
    "content": "import { Module } from '@medusajs/framework/utils';\nimport FashionModuleService from './service';\n\nexport const FASHION_MODULE = 'fashionModuleService';\n\nexport default Module(FASHION_MODULE, {\n  service: FashionModuleService,\n});\n"
  },
  {
    "path": "medusa/src/modules/fashion/migrations/.snapshot-medusa.json",
    "content": "{\n  \"namespaces\": [\n    \"public\"\n  ],\n  \"name\": \"public\",\n  \"tables\": [\n    {\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": false,\n          \"mappedType\": \"text\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": false,\n          \"mappedType\": \"text\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamptz\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": false,\n          \"length\": 6,\n          \"default\": \"now()\",\n          \"mappedType\": \"datetime\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamptz\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": false,\n          \"length\": 6,\n          \"default\": \"now()\",\n          \"mappedType\": \"datetime\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"timestamptz\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": true,\n          \"length\": 6,\n          \"mappedType\": \"datetime\"\n        }\n      },\n      \"name\": \"material\",\n      \"schema\": \"public\",\n      \"indexes\": [\n        {\n          \"keyName\": \"material_pkey\",\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"composite\": false,\n          \"primary\": true,\n          \"unique\": true\n        }\n      ],\n      \"checks\": [],\n      \"foreignKeys\": {}\n    },\n    {\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": false,\n          \"mappedType\": \"text\"\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": false,\n          \"mappedType\": \"text\"\n        },\n        \"hex_code\": {\n          \"name\": \"hex_code\",\n          \"type\": \"text\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": false,\n          \"mappedType\": \"text\"\n        },\n        \"material_id\": {\n          \"name\": \"material_id\",\n          \"type\": \"text\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": false,\n          \"mappedType\": \"text\"\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamptz\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": false,\n          \"length\": 6,\n          \"default\": \"now()\",\n          \"mappedType\": \"datetime\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamptz\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": false,\n          \"length\": 6,\n          \"default\": \"now()\",\n          \"mappedType\": \"datetime\"\n        },\n        \"deleted_at\": {\n          \"name\": \"deleted_at\",\n          \"type\": \"timestamptz\",\n          \"unsigned\": false,\n          \"autoincrement\": false,\n          \"primary\": false,\n          \"nullable\": true,\n          \"length\": 6,\n          \"mappedType\": \"datetime\"\n        }\n      },\n      \"name\": \"color\",\n      \"schema\": \"public\",\n      \"indexes\": [\n        {\n          \"keyName\": \"IDX_color_material_id\",\n          \"columnNames\": [],\n          \"composite\": false,\n          \"primary\": false,\n          \"unique\": false,\n          \"expression\": \"CREATE INDEX IF NOT EXISTS \\\"IDX_color_material_id\\\" ON \\\"color\\\" (material_id) WHERE deleted_at IS NULL\"\n        },\n        {\n          \"keyName\": \"color_pkey\",\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"composite\": false,\n          \"primary\": true,\n          \"unique\": true\n        }\n      ],\n      \"checks\": [],\n      \"foreignKeys\": {\n        \"color_material_id_foreign\": {\n          \"constraintName\": \"color_material_id_foreign\",\n          \"columnNames\": [\n            \"material_id\"\n          ],\n          \"localTableName\": \"public.color\",\n          \"referencedColumnNames\": [\n            \"id\"\n          ],\n          \"referencedTableName\": \"public.material\",\n          \"updateRule\": \"cascade\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "medusa/src/modules/fashion/migrations/Migration20241002190028.ts",
    "content": "import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20241002190028 extends Migration {\n\n  async up(): Promise<void> {\n    this.addSql('create table if not exists \"material\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"material_pkey\" primary key (\"id\"));');\n\n    this.addSql('create table if not exists \"color\" (\"id\" text not null, \"name\" text not null, \"hex_code\" text not null, \"material_id\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"color_pkey\" primary key (\"id\"));');\n    this.addSql('CREATE INDEX IF NOT EXISTS \"IDX_color_material_id\" ON \"color\" (material_id) WHERE deleted_at IS NULL;');\n\n    this.addSql('alter table if exists \"color\" add constraint \"color_material_id_foreign\" foreign key (\"material_id\") references \"material\" (\"id\") on update cascade;');\n  }\n\n  async down(): Promise<void> {\n    this.addSql('alter table if exists \"color\" drop constraint if exists \"color_material_id_foreign\";');\n\n    this.addSql('drop table if exists \"material\" cascade;');\n\n    this.addSql('drop table if exists \"color\" cascade;');\n  }\n\n}\n"
  },
  {
    "path": "medusa/src/modules/fashion/models/color.ts",
    "content": "import { model } from '@medusajs/framework/utils';\nimport { InferTypeOf } from '@medusajs/framework/types';\nimport Material from './material';\n\nconst Color = model.define('color', {\n  id: model.id().primaryKey(),\n  name: model.text(),\n  hex_code: model.text(),\n  material: model.belongsTo(() => Material, {\n    mappedBy: 'colors',\n  }),\n});\n\nexport type ColorModelType = InferTypeOf<typeof Color>;\n\nexport default Color;\n"
  },
  {
    "path": "medusa/src/modules/fashion/models/material.ts",
    "content": "import { model } from '@medusajs/framework/utils';\nimport { InferTypeOf } from '@medusajs/framework/types';\nimport Color from './color';\n\nconst Material = model.define('material', {\n  id: model.id().primaryKey(),\n  name: model.text(),\n  colors: model.hasMany(() => Color),\n});\n\nexport type MaterialModelType = InferTypeOf<typeof Material>;\n\nexport default Material;\n"
  },
  {
    "path": "medusa/src/modules/fashion/service.ts",
    "content": "import { MedusaService } from '@medusajs/framework/utils';\nimport Material from './models/material';\nimport Color from './models/color';\n\nexport default class FashionModuleService extends MedusaService({\n  Material,\n  Color,\n}) {}\n"
  },
  {
    "path": "medusa/src/modules/meilisearch/index.ts",
    "content": "import { Module } from '@medusajs/utils';\nimport Loader from './loader';\nimport { MeiliSearchService } from './service';\n\nexport default Module('meilisearchService', {\n  service: MeiliSearchService,\n  loaders: [Loader],\n});\n"
  },
  {
    "path": "medusa/src/modules/meilisearch/loader.ts",
    "content": "import { LoaderOptions } from '@medusajs/types';\nimport { MeiliSearchService } from './service';\nimport { MeiliSearchPluginOptions } from './types';\nimport { asValue } from 'awilix';\n\nexport default async ({\n  container,\n  options,\n}: LoaderOptions<MeiliSearchPluginOptions>): Promise<void> => {\n  if (!options) {\n    throw new Error('Missing meilisearch configuration');\n  }\n\n  const meilisearchService: MeiliSearchService = new MeiliSearchService(\n    container,\n    options,\n  );\n\n  container.register({\n    meilisearchService: asValue(meilisearchService),\n  });\n\n  if (options.settings) {\n    await Promise.all(\n      Object.entries(options.settings).map(([indexName, indexSettings]) =>\n        meilisearchService.updateSettings(indexName, indexSettings),\n      ),\n    );\n  }\n};\n"
  },
  {
    "path": "medusa/src/modules/meilisearch/service.ts",
    "content": "import { SearchTypes } from '@medusajs/types';\nimport { SearchUtils } from '@medusajs/utils';\n// @ts-ignore\nimport { MeiliSearch, MeiliSearchApiError, Settings } from 'meilisearch';\nimport { MeiliSearchPluginOptions } from './types';\nimport { logger } from '@medusajs/framework';\n\nexport class MeiliSearchService extends SearchUtils.AbstractSearchService {\n  static identifier = 'meilisearch';\n\n  isDefault = false;\n\n  protected readonly client: MeiliSearch;\n\n  constructor(container: any, options: MeiliSearchPluginOptions) {\n    super(container, options);\n\n    if (process.env.NODE_ENV !== 'development') {\n      if (!options.config?.apiKey) {\n        throw Error(\n          'MeiliSearch API key is required for production environments.',\n        );\n      }\n    }\n\n    if (!options.config?.host) {\n      throw Error(\n        'MeiliSearch host is required. Please provide a host in the configuration.',\n      );\n    }\n\n    this.client = new MeiliSearch(options.config);\n  }\n\n  async createIndex(\n    indexName: string,\n    options: Record<string, unknown> = { primaryKey: 'id' },\n  ) {\n    return this.client.createIndex(indexName, options);\n  }\n\n  getIndex(indexName: string) {\n    return this.client.index(indexName);\n  }\n\n  async addDocuments(\n    indexName: string,\n    documents: Record<string, any>[],\n    type: string,\n  ) {\n    const indexSetting = this.options.settings?.[indexName];\n    const transformer = indexSetting?.transformer ?? ((doc: any) => doc);\n    const primaryKey = indexSetting?.primaryKey ?? 'id';\n\n    return this.client\n      .index(indexName)\n      .addDocuments(documents.map(transformer), { primaryKey });\n  }\n\n  async replaceDocuments(\n    indexName: string,\n    documents: Record<string, any>[],\n    type: string,\n  ) {\n    return this.addDocuments(indexName, documents, type);\n  }\n\n  async deleteDocument(indexName: string, documentId: string) {\n    return this.client.index(indexName).deleteDocument(documentId);\n  }\n\n  async deleteAllDocuments(indexName: string) {\n    return this.client.index(indexName).deleteAllDocuments();\n  }\n\n  async search(indexName: string, query: string, options: Record<string, any>) {\n    const { paginationOptions, filter, additionalOptions } = options;\n\n    return this.client\n      .index(indexName)\n      .search(query, { filter, ...paginationOptions, ...additionalOptions });\n  }\n\n  async updateSettings(\n    indexName: string,\n    settings: SearchTypes.IndexSettings & { indexSettings: Settings },\n  ) {\n    const indexSettings = settings.indexSettings ?? {};\n\n    try {\n      await this.client.getIndex(indexName);\n    } catch (error) {\n      if (\n        error instanceof MeiliSearchApiError &&\n        error.cause?.code === 'index_not_found'\n      ) {\n        await this.createIndex(indexName, {\n          primaryKey: settings.primaryKey ?? 'id',\n        });\n      } else {\n        logger.error(error);\n        throw error;\n      }\n    }\n\n    return this.client.index(indexName).updateSettings(indexSettings);\n  }\n}\n"
  },
  {
    "path": "medusa/src/modules/meilisearch/types.ts",
    "content": "import { SearchTypes } from '@medusajs/types';\n// @ts-ignore\nimport type { Config, Settings } from 'meilisearch';\n\nexport interface MeiliSearchPluginOptions {\n  /**\n   * MeiliSearch client configuration\n   */\n  config: Config;\n\n  /**\n   * MeiliSearch index settings\n   */\n  settings?: Record<\n    string,\n    SearchTypes.IndexSettings & { indexSettings: Settings }\n  >;\n}\n"
  },
  {
    "path": "medusa/src/modules/resend/emails/auth-email-confirm.tsx",
    "content": "// External packages\nimport { Text, Heading, Button } from '@react-email/components';\n\n// Components\nimport EmailLayout, { EmailLayoutProps } from './components/EmailLayout';\n\nexport default function AuthEmailConfirm({\n  ...emailLayoutProps\n}: EmailLayoutProps) {\n  return (\n    <EmailLayout {...emailLayoutProps}>\n      <Heading className=\"text-2xl mt-0 mb-11 font-medium\">\n        Verify your email\n      </Heading>\n      <Text className=\"text-md !mb-10\">\n        Hey Jovana, thanks for registering for an account on Sofa Society!\n      </Text>\n      <Text className=\"text-md !mb-10\">\n        Before we get started, we just need to confirm that this is you.\n        <br />\n        Click below to verify your email address:\n      </Text>\n      <Button className=\"inline-flex items-center rounded-xs justify-center transition-colors bg-black text-white h-10 px-6\">\n        Verify email\n      </Button>\n    </EmailLayout>\n  );\n}\n"
  },
  {
    "path": "medusa/src/modules/resend/emails/auth-forgot-password.tsx",
    "content": "// External components\nimport { Text, Heading, Button } from '@react-email/components';\n\n// Types\nimport { CustomerDTO } from '@medusajs/framework/types';\n\n// Components\nimport EmailLayout, { EmailLayoutProps } from './components/EmailLayout';\n\ntype Props = {\n  customer: Pick<CustomerDTO, 'id' | 'email' | 'first_name' | 'last_name'>;\n  token: string;\n};\n\nexport default function AuthPasswordForgotResetEmail({\n  customer,\n  token,\n  ...emailLayoutProps\n}: Props & EmailLayoutProps) {\n  return (\n    <EmailLayout {...emailLayoutProps}>\n      <Heading className=\"text-2xl mt-0 mb-10 font-medium\">\n        Reset your password\n      </Heading>\n      <Text className=\"text-md !mb-10\">\n        We received a request to reset your Sofa Society account password. Click\n        below to set a new password:\n      </Text>\n      <Button\n        href={`${\n          process.env.STOREFRONT_URL || 'http://localhost:8000'\n        }/auth/forgot-password/reset?email=${encodeURIComponent(\n          customer.email,\n        )}&token=${encodeURIComponent(token)}`}\n        className=\"inline-flex items-center rounded-xs justify-center transition-colors bg-black text-white h-10 px-6 mb-10\"\n      >\n        Reset password\n      </Button>\n      <Text className=\"text-md text-grayscale-500 m-0\">\n        If you didn&apos;t request this change, please ignore this email, and\n        your current password will remain unchanged.\n      </Text>\n    </EmailLayout>\n  );\n}\n\nAuthPasswordForgotResetEmail.PreviewProps = {\n  customer: {\n    id: '1',\n    email: 'example@medusa.local',\n    first_name: 'John',\n    last_name: 'Doe',\n  },\n  token: '1234567789012345677890',\n} satisfies Props;\n"
  },
  {
    "path": "medusa/src/modules/resend/emails/auth-password-reset.tsx",
    "content": "// External components\nimport { Text, Heading, Button } from '@react-email/components';\n\n// Types\nimport { CustomerDTO } from '@medusajs/framework/types';\n\n// Components\nimport EmailLayout, { EmailLayoutProps } from './components/EmailLayout';\n\ntype Props = {\n  customer: Pick<CustomerDTO, 'id' | 'email' | 'first_name' | 'last_name'>;\n  token: string;\n};\n\nexport default function AuthPasswordResetEmail({\n  customer,\n  token,\n  ...emailLayoutProps\n}: Props & EmailLayoutProps) {\n  return (\n    <EmailLayout {...emailLayoutProps}>\n      <Heading className=\"text-2xl mt-0 mb-10 font-medium\">\n        Reset your password\n      </Heading>\n      <Text className=\"text-md !mb-10\">\n        We received a request to reset your Sofa Society account password. Click\n        below to set a new password:\n      </Text>\n      <Button\n        href={`${\n          process.env.STOREFRONT_URL || 'http://localhost:8000'\n        }/auth/reset-password?email=${encodeURIComponent(\n          customer.email,\n        )}&token=${encodeURIComponent(token)}`}\n        className=\"inline-flex items-center rounded-xs justify-center bg-black text-white h-10 px-6 mb-10\"\n      >\n        Reset password\n      </Button>\n      <Text className=\"text-md text-grayscale-500 m-0\">\n        If you didn&apos;t request this change, please ignore this email, and\n        your current password will remain unchanged.\n      </Text>\n    </EmailLayout>\n  );\n}\n\nAuthPasswordResetEmail.PreviewProps = {\n  customer: {\n    id: '1',\n    email: 'example@medusa.local',\n    first_name: 'John',\n    last_name: 'Doe',\n  },\n  token: '1234567789012345677890',\n} satisfies Props;\n"
  },
  {
    "path": "medusa/src/modules/resend/emails/components/EmailLayout.tsx",
    "content": "// External packages\nimport {\n  Body,\n  Column,\n  Container,\n  Font,\n  Head,\n  Hr,\n  Html,\n  Link,\n  Row,\n  Section,\n  Text,\n  Tailwind,\n} from '@react-email/components';\n\n// Google Font API is used to load the Mona Sans font\n// You can find other variants here: https://webfonts.googleapis.com/v1/webfonts?capability=WOFF2&family=Mona%20Sans&subset=latin-ext&key=[YOUR_API_KEY]\n\nexport type EmailLayoutProps = {\n  siteTitle?: string;\n  companyName?: string;\n  footerLinks?: {\n    url: string;\n    label: string;\n  }[];\n};\n\nexport default function EmailLayout(\n  props: {\n    children: React.ReactNode;\n  } & EmailLayoutProps\n) {\n  return (\n    <Html>\n      <Head>\n        <Font\n          fontFamily=\"Mona Sans\"\n          fallbackFontFamily={['Arial', 'Helvetica', 'Verdana', 'sans-serif']}\n          webFont={{\n            url: 'https://fonts.gstatic.com/s/monasans/v1/o-0mIpQmx24alC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A99Y41P6zHtY.woff2',\n            format: 'woff2',\n          }}\n          fontWeight={400}\n          fontStyle=\"normal\"\n        />\n        <Font\n          fontFamily=\"Mona Sans\"\n          fallbackFontFamily={['Arial', 'Helvetica', 'Verdana', 'sans-serif']}\n          webFont={{\n            url: 'https://fonts.gstatic.com/s/monasans/v1/o-0kIpQmx24alC5A4PNr4C5OaxRsfNNlKbCePevHtVtX57DGjDU1QDce6VLYyWtY1rI.woff2',\n            format: 'woff2',\n          }}\n          fontWeight={400}\n          fontStyle=\"italic\"\n        />\n        <Font\n          fontFamily=\"Mona Sans\"\n          fallbackFontFamily={['Arial', 'Helvetica', 'Verdana', 'sans-serif']}\n          webFont={{\n            url: 'https://fonts.gstatic.com/s/monasans/v1/o-0mIpQmx24alC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyAjBN9Y41P6zHtY.woff2',\n            format: 'woff2',\n          }}\n          fontWeight={600}\n          fontStyle=\"normal\"\n        />\n        <Font\n          fontFamily=\"Mona Sans\"\n          fallbackFontFamily={['Arial', 'Helvetica', 'Verdana', 'sans-serif']}\n          webFont={{\n            url: 'https://fonts.gstatic.com/s/monasans/v1/o-0kIpQmx24alC5A4PNr4C5OaxRsfNNlKbCePevHtVtX57DGjDU1QOkZ6VLYyWtY1rI.woff2',\n            format: 'woff2',\n          }}\n          fontWeight={600}\n          fontStyle=\"italic\"\n        />\n        <Font\n          fontFamily=\"Mona Sans\"\n          fallbackFontFamily={['Arial', 'Helvetica', 'Verdana', 'sans-serif']}\n          webFont={{\n            url: 'https://fonts.gstatic.com/s/monasans/v1/o-0mIpQmx24alC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyAaBN9Y41P6zHtY.woff2',\n            format: 'woff2',\n          }}\n          fontWeight={700}\n          fontStyle=\"normal\"\n        />\n        <Font\n          fontFamily=\"Mona Sans\"\n          fallbackFontFamily={['Arial', 'Helvetica', 'Verdana', 'sans-serif']}\n          webFont={{\n            url: 'https://fonts.gstatic.com/s/monasans/v1/o-0kIpQmx24alC5A4PNr4C5OaxRsfNNlKbCePevHtVtX57DGjDU1QNAZ6VLYyWtY1rI.woff2',\n            format: 'woff2',\n          }}\n          fontWeight={700}\n          fontStyle=\"italic\"\n        />\n      </Head>\n      <Tailwind\n        config={{\n          theme: {\n            fontFamily: {\n              sans: 'Mona Sans',\n            },\n            extend: {\n              spacing: {\n                18: '4.5rem',\n                22: '5.5rem',\n              },\n              colors: {\n                grayscale: {\n                  500: '#808080',\n                  200: '#D1D1D1',\n                  100: '#E7E7E7',\n                  50: '#F4F4F4',\n                },\n              },\n              borderRadius: {\n                xs: '4px',\n                sm: '16px',\n              },\n              maxWidth: {\n                37: '9.25rem',\n                228: '57rem',\n              },\n              fontSize: {\n                '3xl': ['3.5rem', '1.5'],\n                '2xl': ['3rem', '1.5'],\n                xl: ['2.5rem', '1.5'],\n                lg: ['1.75rem', '1.5'],\n                md: ['1.5rem', '1.5'],\n                sm: ['1.125rem', '1.5'],\n                base: ['1rem', '1.5'],\n                xs: ['0.75rem', '1.5'],\n              },\n            },\n          },\n        }}\n      >\n        <Body className=\"bg-grayscale-50 font-normal\">\n          <Container className=\"bg-white py-18 px-22 rounded-sm max-w-228 w-full\">\n            <Link\n              href={process.env.STOREFRONT_URL || 'http://localhost:8000'}\n              className=\"text-lg mb-18 inline-block text-black\"\n            >\n              {props.siteTitle || 'SofaSocietyCo.'}\n            </Link>\n            {props.children}\n            <Hr className=\"mt-20 mb-8\" />\n            <Section className=\"gap-4 text-grayscale-500\">\n              <Row>\n                <Column className=\"w-full\">\n                  <Link\n                    href={process.env.STOREFRONT_URL || 'http://localhost:8000'}\n                    className=\"text-lg text-grayscale-500\"\n                  >\n                    {props.siteTitle || 'SofaSocietyCo.'}\n                  </Link>\n                  <Text className=\"text-xs m-0\">\n                    &copy; {new Date().getFullYear()},{' '}\n                    {props.companyName || 'Sofa Society'}\n                  </Text>\n                </Column>\n                {props.footerLinks && props.footerLinks.length > 0 && (\n                  <Column valign=\"top\">\n                    <Row>\n                      {props.footerLinks.map((link, index) => (\n                        <Column className=\"px-2\" key={index}>\n                          <Link href={link.url} className=\"text-grayscale-500\">\n                            {link.label}\n                          </Link>\n                        </Column>\n                      ))}\n                    </Row>\n                  </Column>\n                )}\n              </Row>\n            </Section>\n          </Container>\n        </Body>\n      </Tailwind>\n    </Html>\n  );\n}\n"
  },
  {
    "path": "medusa/src/modules/resend/emails/index.ts",
    "content": "import AuthPasswordForgotResetEmail from \"./auth-forgot-password\";\nimport AuthPasswordResetEmail from \"./auth-password-reset\";\nimport OrderPlacedEmail from \"./order-placed\";\nimport WelcomeEmail from \"./welcome\";\n\n// TODO: we should be able to use notification data in subjects too\nexport const subjects = {\n  \"auth-password-reset\": \"Reset your password\",\n  \"order-placed\": \"Your order has been placed\",\n  \"customer-welcome\": \"Welcome to Sofa Society!\",\n  \"auth-forgot-password\": \"Reset your password\",\n};\n\nexport default {\n  \"auth-password-reset\": AuthPasswordResetEmail,\n  \"order-placed\": OrderPlacedEmail,\n  \"customer-welcome\": WelcomeEmail,\n  \"auth-forgot-password\": AuthPasswordForgotResetEmail,\n};\n"
  },
  {
    "path": "medusa/src/modules/resend/emails/order-placed.tsx",
    "content": "// External packages\nimport { Fragment } from 'react';\nimport {\n  Text,\n  Column,\n  Heading,\n  Img,\n  Row,\n  Section,\n  Link,\n  Hr,\n} from '@react-email/components';\nimport { HttpTypes } from '@medusajs/framework/types';\nimport EmailLayout, { EmailLayoutProps } from './components/EmailLayout';\n\nexport type OrderPlacedEmailProps = {\n  order: Pick<\n    HttpTypes.AdminOrder,\n    | 'currency_code'\n    | 'email'\n    | 'shipping_total'\n    | 'subtotal'\n    | 'total'\n    | 'tax_total'\n  > & {\n    shipping_address:\n      | (Pick<\n          HttpTypes.AdminOrderAddress,\n          | 'first_name'\n          | 'last_name'\n          | 'address_1'\n          | 'address_2'\n          | 'city'\n          | 'postal_code'\n          | 'province'\n          | 'phone'\n        > & {\n          country?: Pick<\n            HttpTypes.AdminRegionCountry,\n            'iso_2' | 'name' | 'display_name'\n          >;\n        })\n      | null;\n    billing_address:\n      | (Pick<\n          HttpTypes.AdminOrderAddress,\n          | 'first_name'\n          | 'last_name'\n          | 'address_1'\n          | 'address_2'\n          | 'city'\n          | 'postal_code'\n          | 'province'\n          | 'phone'\n        > & {\n          country?: Pick<\n            HttpTypes.AdminRegionCountry,\n            'iso_2' | 'name' | 'display_name'\n          >;\n        })\n      | null;\n    items: Pick<\n      HttpTypes.AdminOrder['items'][number],\n      | 'id'\n      | 'thumbnail'\n      | 'product_title'\n      | 'variant_title'\n      | 'total'\n      | 'quantity'\n      | 'variant_option_values'\n    >[];\n  };\n} & EmailLayoutProps;\n\nexport default function OrderPlacedEmail({\n  order,\n  ...emailLayoutProps\n}: OrderPlacedEmailProps) {\n  const formatter = new Intl.NumberFormat([], {\n    style: 'currency',\n    currencyDisplay: 'narrowSymbol',\n    currency: order.currency_code,\n  });\n\n  return (\n    <EmailLayout {...emailLayoutProps}>\n      <Heading className=\"text-2xl font-medium mt-0 mb-10\">\n        Order confirmation\n      </Heading>\n      <Text className=\"text-md !mb-6\">\n        We are pleased to confirm that your order has been successfully placed\n        and will be processed shortly. Your order number is #100002.\n      </Text>\n      <Text className=\"text-md !mb-6\">\n        You&apos;ll receive another update once your order is shipped. For any\n        questions, feel free to contact us at info@sofasociety.com.\n      </Text>\n      <Text className=\"text-md !mb-20\">Thank you for shopping with us!</Text>\n      <Section className=\"mb-6\">\n        <Row>\n          <Column className=\"border border-solid p-4 border-grayscale-200 rounded-xs\">\n            <Text className=\"text-grayscale-500 !mt-0 !mb-8\">\n              Delivery Address\n            </Text>\n            <Text className=\"m-0 leading-tight\">\n              {[\n                order.shipping_address.first_name,\n                order.shipping_address.last_name,\n              ]\n                .filter(Boolean)\n                .join(' ')}\n            </Text>\n            <Text className=\"m-0 leading-tight\">\n              {[\n                order.shipping_address.address_1,\n                order.shipping_address.address_2,\n                [\n                  order.shipping_address.postal_code,\n                  order.shipping_address.city,\n                ]\n                  .filter(Boolean)\n                  .join(' '),\n                order.shipping_address.province,\n                order.shipping_address.country.display_name,\n              ]\n                .filter(Boolean)\n                .join(', ')}\n            </Text>\n            {order.shipping_address.phone && (\n              <Text className=\"m-0 leading-tight\">\n                {order.shipping_address.phone}\n              </Text>\n            )}\n          </Column>\n          <Column className=\"w-8\" />\n          <Column className=\"border border-solid p-4 border-grayscale-200 rounded-xs\">\n            <Text className=\"text-grayscale-500 !mt-0 !mb-8\">\n              Billing Address\n            </Text>\n            <Text className=\"m-0 leading-tight\">\n              {[\n                order.billing_address.first_name,\n                order.billing_address.last_name,\n              ]\n                .filter(Boolean)\n                .join(' ')}\n            </Text>\n            <Text className=\"m-0 leading-tight\">\n              {[\n                order.billing_address.address_1,\n                order.billing_address.address_2,\n                [order.billing_address.postal_code, order.billing_address.city]\n                  .filter(Boolean)\n                  .join(' '),\n                order.billing_address.province,\n                order.billing_address.country.display_name,\n              ]\n                .filter(Boolean)\n                .join(', ')}\n            </Text>\n            {order.billing_address.phone && (\n              <Text className=\"m-0 leading-tight\">\n                {order.billing_address.phone}\n              </Text>\n            )}\n          </Column>\n        </Row>\n      </Section>\n      <Section className=\"border border-solid border-grayscale-200 rounded-xs px-4 mb-6\">\n        {order.items.map((item, index) => {\n          return (\n            <Fragment key={item.id}>\n              {index > 0 && (\n                <Hr className=\"border-t border-solid border-grayscale-100 m-0\" />\n              )}\n              <Row className=\"py-4\">\n                <Column>\n                  {!!item.thumbnail && (\n                    <Link href=\"/\">\n                      <Img\n                        src={item.thumbnail}\n                        alt={item.product_title}\n                        className=\"aspect-[3/4] object-cover max-w-37 float-left\"\n                      />\n                    </Link>\n                  )}\n                </Column>\n                <Column className=\"w-full pl-8 relative\" valign=\"top\">\n                  <Text className=\"text-md !mt-0 !mb-2\">\n                    {item.product_title}\n                  </Text>\n                  <Section className=\"mb-1\">\n                    {Object.entries(item.variant_option_values).flatMap(\n                      ([key, value]) =>\n                        typeof value === 'string' ? (\n                          <Row key={key}>\n                            <Column className=\"flex\">\n                              <Text className=\"text-grayscale-500 m-0 text-xs\">\n                                {key}:\n                              </Text>\n                              <Text className=\"m-0 text-xs ml-2\">{value}</Text>\n                            </Column>\n                          </Row>\n                        ) : (\n                          []\n                        ),\n                    )}\n                    <Row className=\"absolute bottom-0\">\n                      <Column className=\"flex\">\n                        <Text className=\"text-grayscale-500 m-0 text-xs\">\n                          Quantity:\n                        </Text>\n                        <Text className=\"m-0 text-xs ml-2\">\n                          {item.quantity}\n                        </Text>\n                      </Column>\n                    </Row>\n                  </Section>\n                </Column>\n                <Column valign=\"bottom\">\n                  <Text className=\"m-0 text-md\">\n                    {formatter.format(item.total)}\n                  </Text>\n                </Column>\n              </Row>\n            </Fragment>\n          );\n        })}\n      </Section>\n      <Section className=\"border border-solid border-grayscale-200 rounded-xs p-4\">\n        <Row>\n          <Column className=\"w-1/2 flex items-center\" valign=\"top\">\n            <Img\n              src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAEySURBVHgB7ZbLccIwEIbXz/GMD3EJSgekgigdkAqSzkIHSSrAVAAdWB3A0eMn/4IBY+DGYg76ZtZr2Rrvb2klLZHFMjLO8EEURbqua0UCeJ5n8jxP6ZoABFYI/IvbCcliIOQDQsyZgCAIMjgF2ziO80f3J4FN2rZV8KuyLN+Ob3zf1xDQhmGYJYDkSBBnzbHiON6NtMsX/LHqOqQbQHLw6P7zTVEUJwFjMroAv99AgmjkwQ8JghjvdEsAUOjwTQ9kKGABm5EsXzB9VQAyNEN2zkgQTLHGKB/bdhVYAVbAGAJeaCgA69J0fkr7c1sEPuoRY3cKcnXEvl+QLGlfDRlsSCkJ0DTNFN9OYAYb3uuZAC7J0GHeVSxiIPjKdd3Pi5LsAFdHvQLlrvBUV1WVksXyTGwBvHxnj9a95poAAAAASUVORK5CYII=\"\n              alt=\"Credit card\"\n              width=\"16\"\n              height=\"16\"\n            />\n            <Text className=\"m-0 ml-2\">Payment</Text>\n          </Column>\n          <Column className=\"w-1/2\">\n            <Section>\n              <Row className=\"mb-2\">\n                <Column className=\"flex\">\n                  <Text className=\"text-grayscale-500 m-0 text-base\">\n                    Subtotal\n                  </Text>\n                  <Text className=\"m-0 text-base ml-auto\">\n                    {formatter.format(order.subtotal)}\n                  </Text>\n                </Column>\n              </Row>\n              <Row className=\"mb-6\">\n                <Column className=\"flex\">\n                  <Text className=\"text-grayscale-500 m-0 text-base\">\n                    Shipping\n                  </Text>\n                  <Text className=\"m-0 text-base ml-auto\">\n                    {formatter.format(order.shipping_total)}\n                  </Text>\n                </Column>\n              </Row>\n              <Row>\n                <Column className=\"flex\">\n                  <Text className=\"m-0 text-md\">Total</Text>\n                  <Text className=\"m-0 text-md ml-auto\">\n                    {formatter.format(order.total)}\n                  </Text>\n                </Column>\n              </Row>\n              <Row>\n                <Column className=\"flex\">\n                  <Text className=\"text-grayscale-500 m-0 text-xs\">\n                    Including\n                  </Text>\n                  <Text className=\"m-0 text-xs text-grayscale-500 ml-1\">\n                    {formatter.format(order.tax_total)} tax\n                  </Text>\n                </Column>\n              </Row>\n            </Section>\n          </Column>\n        </Row>\n      </Section>\n    </EmailLayout>\n  );\n}\n\nOrderPlacedEmail.PreviewProps = {\n  order: {\n    currency_code: 'EUR',\n    email: 'example@medusa.local',\n    shipping_address: {\n      first_name: 'John',\n      last_name: 'Doe',\n      address_1: '1234 Main St',\n      address_2: 'Apt 1',\n      city: 'Los Angeles',\n      postal_code: '90001',\n      country: {\n        iso_2: 'US',\n        name: 'United States',\n        display_name: 'United States',\n      },\n      phone: '+1 123 456 7890',\n      province: 'California',\n    },\n    billing_address: {\n      first_name: 'John',\n      last_name: 'Doe',\n      address_1: '1234 Main St',\n      address_2: 'Apt 1',\n      city: 'Los Angeles',\n      postal_code: '90001',\n      country: {\n        iso_2: 'US',\n        name: 'United States',\n        display_name: 'United States',\n      },\n      phone: '+1 123 456 7890',\n      province: 'California',\n    },\n    items: [\n      {\n        id: '1',\n        thumbnail:\n          'https://fashion-starter-demo.s3.eu-central-1.amazonaws.com/belime-estate-01JAR3JYD68D1YYR0BN7HHMAZE.png',\n        product_title: 'Belime Estate',\n        variant_title: 'Linen / Red',\n        total: 1500,\n        quantity: 1,\n        variant_option_values: {\n          Material: 'Linen',\n          Color: 'Red',\n        },\n      },\n    ],\n    shipping_total: 100,\n    subtotal: 1400,\n    total: 1500,\n    tax_total: 100,\n  },\n} satisfies OrderPlacedEmailProps;\n"
  },
  {
    "path": "medusa/src/modules/resend/emails/order-update.tsx",
    "content": "// External packages\nimport { Text, Heading, Button } from '@react-email/components';\n\n// Types\nimport { CustomerDTO, OrderDTO } from '@medusajs/framework/types';\n\n// Components\nimport EmailLayout, { EmailLayoutProps } from './components/EmailLayout';\n\ntype Props = {\n  customer: Pick<CustomerDTO, 'id' | 'email' | 'first_name' | 'last_name'>;\n  order: Pick<OrderDTO, 'id' | 'display_id'>;\n};\n\nexport default function OrderUpdateEmail({\n  customer,\n  order,\n  ...emailLayoutProps\n}: Props & EmailLayoutProps) {\n  return (\n    <EmailLayout {...emailLayoutProps}>\n      <Heading className=\"text-2xl mt-0 mb-10 font-medium\">\n        Shipping update\n      </Heading>\n      <Text className=\"text-md !mb-8\">\n        Great news! Your order #{order.display_id} is now on its way to you.\n        <br />\n        Here are the shipping details.\n      </Text>\n      <Text className=\"text-md !mb-10\">\n        You can track your package by clicking below:\n      </Text>\n      <Button\n        href={`${\n          process.env.STOREFRONT_URL || 'http://localhost:8000'\n        }/account/my-orders/${order.id}`}\n        className=\"inline-flex items-center rounded-xs justify-center bg-black text-white h-10 px-6 mb-10\"\n      >\n        Order details\n      </Button>\n      <Text className=\"text-md m-0\">\n        Thank you for choosing Sofa Society. We&apos;re excited for your new\n        sofa to find its home with you!\n      </Text>\n    </EmailLayout>\n  );\n}\n\nOrderUpdateEmail.PreviewProps = {\n  customer: {\n    id: '1',\n    email: 'example@medusa.local',\n    first_name: 'John',\n    last_name: 'Doe',\n  },\n  order: {\n    id: 'order_01JCNYH6VADAK90W7CBSPV5BT6',\n    display_id: 1,\n  },\n} satisfies Props;\n"
  },
  {
    "path": "medusa/src/modules/resend/emails/welcome.tsx",
    "content": "// External packages\nimport { Text, Heading, Row, Column } from '@react-email/components';\nimport { CustomerDTO } from '@medusajs/framework/types';\n\n// Components\nimport EmailLayout, { EmailLayoutProps } from './components/EmailLayout';\n\nconst UnorderedList: React.FC<{\n  children?: React.ReactNode;\n  className?: string;\n}> = ({ children, className }) => {\n  return (\n    <Row className={['align-top', className].filter(Boolean).join(' ')}>\n      <Column className=\"pl-6\">{children}</Column>\n    </Row>\n  );\n};\n\nconst UnorderedListItem: React.FC<{\n  children?: React.ReactNode;\n  className?: string;\n  textClassName?: string;\n}> = ({ children, className, textClassName }) => {\n  return (\n    <ul\n      role=\"presentation\"\n      className={['list-disc mt-0 mb-0 p-0', className]\n        .filter(Boolean)\n        .join(' ')}\n    >\n      <li role=\"listitem\" className=\"m-0 p-0\">\n        <span className={textClassName}>{children}</span>\n      </li>\n    </ul>\n  );\n};\n\ntype Props = {\n  customer: Pick<CustomerDTO, 'id' | 'email' | 'first_name' | 'last_name'>;\n};\n\nexport default function WelcomeEmail({\n  customer,\n  ...emailLayoutProps\n}: Props & EmailLayoutProps) {\n  return (\n    <EmailLayout {...emailLayoutProps}>\n      <Heading className=\"text-2xl mt-0 mb-10 font-medium\">\n        Welcome to Sofa Society!\n      </Heading>\n      <Text className=\"text-md !mb-8\">\n        Welcome to Sofa Society! We're excited to have you join our community of\n        comfort enthusiasts. With our carefully crafted sofas, you&apos;re just\n        steps away from adding elegance and coziness to your living space.\n      </Text>\n      <Text className=\"text-md font-semibold !mb-8\">\n        As a new member, here&apos;s what you can expect:\n      </Text>\n      <UnorderedList className=\"mb-8\">\n        <UnorderedListItem className=\"text-md\">\n          Premium, high-quality sofas in a range of styles and materials\n        </UnorderedListItem>\n        <UnorderedListItem className=\"text-md\">\n          Dedicated customer support ready to assist you\n        </UnorderedListItem>\n        <UnorderedListItem className=\"text-md\">\n          Exclusive offers and early access to new collections\n        </UnorderedListItem>\n        <UnorderedListItem className=\"text-md\">\n          Explore our collections and find the sofa that suits your style!\n        </UnorderedListItem>\n      </UnorderedList>\n      <Text className=\"text-md\">\n        Best wishes,\n        <br />\n        The Sofa Society Team\n      </Text>\n    </EmailLayout>\n  );\n}\n\nWelcomeEmail.PreviewProps = {\n  customer: {\n    id: '1',\n    email: 'example@medusa.local',\n    first_name: 'John',\n    last_name: 'Doe',\n  },\n} satisfies Props;\n"
  },
  {
    "path": "medusa/src/modules/resend/index.ts",
    "content": "import { ModuleProvider, Modules } from '@medusajs/framework/utils';\nimport ResendNotificationProviderService from './service';\n\nexport default ModuleProvider(Modules.NOTIFICATION, {\n  services: [ResendNotificationProviderService],\n});\n"
  },
  {
    "path": "medusa/src/modules/resend/service.tsx",
    "content": "import { AbstractNotificationProviderService } from '@medusajs/framework/utils';\nimport { Logger } from '@medusajs/medusa';\nimport {\n  ProviderSendNotificationDTO,\n  ProviderSendNotificationResultsDTO,\n} from '@medusajs/types';\nimport { Resend } from 'resend';\nimport emails, { subjects } from './emails';\nimport type { EmailLayoutProps } from './emails/components/EmailLayout';\n\ntype InjectedDependencies = {\n  logger: Logger;\n};\n\nexport default class ResendNotificationProviderService extends AbstractNotificationProviderService {\n  public static identifier = 'resend';\n  private resendClient: Resend;\n  private from: string;\n  private layoutOptions?: EmailLayoutProps;\n  private logger: Logger;\n\n  constructor({ logger }: InjectedDependencies, options: unknown) {\n    super();\n\n    if (\n      typeof options !== 'object' ||\n      options === null ||\n      !('api_key' in options) ||\n      typeof options.api_key !== 'string' ||\n      !('from' in options) ||\n      typeof options.from !== 'string'\n    ) {\n      throw new Error(\n        `Invalid options provided to Resend module. Expected { api_key: string, from: string }`,\n      );\n    }\n\n    const layoutOptions: EmailLayoutProps = {};\n\n    if ('siteTitle' in options && typeof options.siteTitle === 'string') {\n      layoutOptions.siteTitle = options.siteTitle;\n    }\n\n    if ('companyName' in options && typeof options.companyName === 'string') {\n      layoutOptions.companyName = options.companyName;\n    }\n\n    if ('footerLinks' in options) {\n      if (\n        !Array.isArray(options.footerLinks) ||\n        !options.footerLinks.every(\n          (l) => typeof l.url === 'string' && typeof l.label === 'string',\n        )\n      ) {\n        this.logger.warn(\n          `Invalid footer links provided to Resend module. Expected an array of { url: string, label: string } objects.`,\n        );\n      } else {\n        layoutOptions.footerLinks = options.footerLinks;\n      }\n    }\n\n    this.resendClient = new Resend(options.api_key);\n    this.from = options.from;\n    this.logger = logger;\n    this.layoutOptions = layoutOptions;\n  }\n\n  async send(\n    notification: ProviderSendNotificationDTO,\n  ): Promise<ProviderSendNotificationResultsDTO> {\n    const Template = emails[notification.template];\n    const subject = subjects[notification.template] || '';\n\n    if (!Template) {\n      this.logger.error(\n        `Couldn't find an email template for ${\n          notification.template\n        }. The valid options are ${Object.keys(emails).join(', ')}`,\n      );\n      return {};\n    }\n\n    if (!subject) {\n      this.logger.warn(\n        `No subject found for template ${notification.template}. Please add a subject to the emails file.`,\n      );\n    }\n\n    const { data, error } = await this.resendClient.emails.send({\n      from: this.from,\n      to: [notification.to],\n      subject,\n      react: <Template {...this.layoutOptions} {...notification.data} />,\n    });\n\n    if (error) {\n      this.logger.error(`Failed to send email`, error);\n      return {};\n    }\n\n    return { id: data.id };\n  }\n}\n"
  },
  {
    "path": "medusa/src/scripts/README.md",
    "content": "# Custom CLI Script\n\nA custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run as a CLI tool.\n\n## How to Create a Custom CLI Script?\n\nTo create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function.\n\nFor example, create the file `src/scripts/my-script.ts` with the following content:\n\n```ts title=\"src/scripts/my-script.ts\"\nimport { \n  ExecArgs,\n  IProductModuleService\n} from \"@medusajs/framework/types\"\nimport { Modules } from \"@medusajs/framework/utils\"\n\nexport default async function myScript ({\n  container\n}: ExecArgs) {\n  const productModuleService: IProductModuleService = \n    container.resolve(Modules.PRODUCT)\n\n  const [, count] = await productModuleService.listAndCount()\n\n  console.log(`You have ${count} product(s)`)\n}\n```\n\nThe function receives as a parameter an object having a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application.\n\n---\n\n## How to Run Custom CLI Script?\n\nTo run the custom CLI script, run the `exec` command:\n\n```bash\nnpx medusa exec ./src/scripts/my-script.ts\n```\n\n---\n\n## Custom CLI Script Arguments\n\nYour script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property.\n\nFor example:\n\n```ts\nimport { ExecArgs } from \"@medusajs/framework/types\"\n\nexport default async function myScript ({\n  args\n}: ExecArgs) {\n  console.log(`The arguments you passed: ${args}`)\n}\n```\n\nThen, pass the arguments in the `exec` command after the file path:\n\n```bash\nnpx medusa exec ./src/scripts/my-script.ts arg1 arg2\n```"
  },
  {
    "path": "medusa/src/scripts/index-products.ts",
    "content": "import { ExecArgs, ISearchService } from '@medusajs/framework/types';\nimport { Modules } from '@medusajs/framework/utils';\n\nexport default async function indexProducts({ container }: ExecArgs) {\n  const logger = container.resolve('logger');\n\n  const meilisearchService = container.resolve(\n    'meilisearchService',\n  ) as ISearchService;\n\n  const productModuleService = container.resolve(Modules.PRODUCT);\n\n  const [products, count] = await productModuleService.listAndCountProducts(\n    undefined,\n    {\n      relations: [\n        'variants',\n        'options',\n        'tags',\n        'collection',\n        'type',\n        'images',\n        'categories',\n      ],\n    },\n  );\n\n  logger.info(`Adding ${count} products to MeiliSearch...`);\n\n  await meilisearchService.addDocuments('products', products, 'products');\n\n  logger.info('Products added to MeiliSearch');\n}\n"
  },
  {
    "path": "medusa/src/scripts/seed.ts",
    "content": "import {\n  createApiKeysWorkflow,\n  createCollectionsWorkflow,\n  createProductCategoriesWorkflow,\n  createProductsWorkflow,\n  createProductTypesWorkflow,\n  createRegionsWorkflow,\n  createSalesChannelsWorkflow,\n  createShippingOptionsWorkflow,\n  createShippingProfilesWorkflow,\n  createStockLocationsWorkflow,\n  createTaxRegionsWorkflow,\n  linkSalesChannelsToApiKeyWorkflow,\n  linkSalesChannelsToStockLocationWorkflow,\n  updateStoresWorkflow,\n  uploadFilesWorkflow,\n} from '@medusajs/medusa/core-flows';\nimport {\n  ExecArgs,\n  IFulfillmentModuleService,\n  ISalesChannelModuleService,\n  IStoreModuleService,\n} from '@medusajs/framework/types';\nimport {\n  ContainerRegistrationKeys,\n  Modules,\n  ProductStatus,\n} from '@medusajs/framework/utils';\nimport type FashionModuleService from '../modules/fashion/service';\nimport type { MaterialModelType } from '../modules/fashion/models/material';\n\nasync function getImageUrlContent(url: string) {\n  const response = await fetch(url);\n\n  if (!response.ok) {\n    throw new Error(`Failed to fetch image \"${url}\": ${response.statusText}`);\n  }\n\n  const arrayBuffer = await response.arrayBuffer();\n\n  return Buffer.from(arrayBuffer).toString('binary');\n}\n\nexport default async function seedDemoData({ container }: ExecArgs) {\n  const logger = container.resolve(ContainerRegistrationKeys.LOGGER);\n  const remoteLink = container.resolve(ContainerRegistrationKeys.LINK);\n  const fulfillmentModuleService: IFulfillmentModuleService = container.resolve(\n    Modules.FULFILLMENT,\n  );\n  const salesChannelModuleService: ISalesChannelModuleService =\n    container.resolve(Modules.SALES_CHANNEL);\n  const storeModuleService: IStoreModuleService = container.resolve(\n    Modules.STORE,\n  );\n  const fashionModuleService: FashionModuleService = container.resolve(\n    'fashionModuleService',\n  );\n\n  const countries = ['hr', 'gb', 'de', 'dk', 'se', 'fr', 'es', 'it'];\n\n  logger.info('Seeding store data...');\n  const [store] = await storeModuleService.listStores();\n  let defaultSalesChannel = await salesChannelModuleService.listSalesChannels({\n    name: 'Default Sales Channel',\n  });\n\n  if (!defaultSalesChannel.length) {\n    // create the default sales channel\n    const { result: salesChannelResult } = await createSalesChannelsWorkflow(\n      container,\n    ).run({\n      input: {\n        salesChannelsData: [\n          {\n            name: 'Default Sales Channel',\n          },\n        ],\n      },\n    });\n    defaultSalesChannel = salesChannelResult;\n  }\n\n  logger.info('Seeding region data...');\n  const { result: regionResult } = await createRegionsWorkflow(container).run({\n    input: {\n      regions: [\n        {\n          name: 'Europe',\n          currency_code: 'eur',\n          countries,\n          payment_providers: ['pp_stripe_stripe'],\n        },\n      ],\n    },\n  });\n  const region = regionResult[0];\n  logger.info('Finished seeding regions.');\n\n  await updateStoresWorkflow(container).run({\n    input: {\n      selector: { id: store.id },\n      update: {\n        supported_currencies: [\n          {\n            currency_code: 'eur',\n            is_default: true,\n          },\n          {\n            currency_code: 'usd',\n          },\n        ],\n        default_sales_channel_id: defaultSalesChannel[0].id,\n        default_region_id: region.id,\n      },\n    },\n  });\n\n  logger.info('Seeding tax regions...');\n  await createTaxRegionsWorkflow(container).run({\n    input: countries.map((country_code) => ({\n      country_code,\n    })),\n  });\n  logger.info('Finished seeding tax regions.');\n\n  logger.info('Seeding stock location data...');\n  const { result: stockLocationResult } = await createStockLocationsWorkflow(\n    container,\n  ).run({\n    input: {\n      locations: [\n        {\n          name: 'European Warehouse',\n          address: {\n            city: 'Copenhagen',\n            country_code: 'DK',\n            address_1: '',\n          },\n        },\n      ],\n    },\n  });\n  const stockLocation = stockLocationResult[0];\n\n  await remoteLink.create({\n    [Modules.STOCK_LOCATION]: {\n      stock_location_id: stockLocation.id,\n    },\n    [Modules.FULFILLMENT]: {\n      fulfillment_provider_id: 'manual_manual',\n    },\n  });\n\n  logger.info('Seeding fulfillment data...');\n  const { result: shippingProfileResult } =\n    await createShippingProfilesWorkflow(container).run({\n      input: {\n        data: [\n          {\n            name: 'Default',\n            type: 'default',\n          },\n        ],\n      },\n    });\n  const shippingProfile = shippingProfileResult[0];\n\n  const fulfillmentSet = await fulfillmentModuleService.createFulfillmentSets({\n    name: 'European Warehouse delivery',\n    type: 'shipping',\n    service_zones: [\n      {\n        name: 'Europe',\n        geo_zones: [\n          {\n            country_code: 'hr',\n            type: 'country',\n          },\n          {\n            country_code: 'gb',\n            type: 'country',\n          },\n          {\n            country_code: 'de',\n            type: 'country',\n          },\n          {\n            country_code: 'dk',\n            type: 'country',\n          },\n          {\n            country_code: 'se',\n            type: 'country',\n          },\n          {\n            country_code: 'fr',\n            type: 'country',\n          },\n          {\n            country_code: 'es',\n            type: 'country',\n          },\n          {\n            country_code: 'it',\n            type: 'country',\n          },\n        ],\n      },\n    ],\n  });\n\n  await remoteLink.create({\n    [Modules.STOCK_LOCATION]: {\n      stock_location_id: stockLocation.id,\n    },\n    [Modules.FULFILLMENT]: {\n      fulfillment_set_id: fulfillmentSet.id,\n    },\n  });\n\n  await createShippingOptionsWorkflow(container).run({\n    input: [\n      {\n        name: 'Standard Shipping',\n        price_type: 'flat',\n        provider_id: 'manual_manual',\n        service_zone_id: fulfillmentSet.service_zones[0].id,\n        shipping_profile_id: shippingProfile.id,\n        type: {\n          label: 'Standard',\n          description: 'Ship in 2-3 days.',\n          code: 'standard',\n        },\n        prices: [\n          {\n            currency_code: 'usd',\n            amount: 10,\n          },\n          {\n            currency_code: 'eur',\n            amount: 10,\n          },\n          {\n            region_id: region.id,\n            amount: 10,\n          },\n        ],\n        rules: [\n          {\n            attribute: 'enabled_in_store',\n            value: '\"true\"',\n            operator: 'eq',\n          },\n          {\n            attribute: 'is_return',\n            value: 'false',\n            operator: 'eq',\n          },\n        ],\n      },\n      {\n        name: 'Express Shipping',\n        price_type: 'flat',\n        provider_id: 'manual_manual',\n        service_zone_id: fulfillmentSet.service_zones[0].id,\n        shipping_profile_id: shippingProfile.id,\n        type: {\n          label: 'Express',\n          description: 'Ship in 24 hours.',\n          code: 'express',\n        },\n        prices: [\n          {\n            currency_code: 'usd',\n            amount: 10,\n          },\n          {\n            currency_code: 'eur',\n            amount: 10,\n          },\n          {\n            region_id: region.id,\n            amount: 10,\n          },\n        ],\n        rules: [\n          {\n            attribute: 'enabled_in_store',\n            value: '\"true\"',\n            operator: 'eq',\n          },\n          {\n            attribute: 'is_return',\n            value: 'false',\n            operator: 'eq',\n          },\n        ],\n      },\n    ],\n  });\n\n  const pickupFulfillmentSet =\n    await fulfillmentModuleService.createFulfillmentSets({\n      name: 'Store pickup',\n      type: 'pickup',\n      service_zones: [\n        {\n          name: 'Store pickup',\n          geo_zones: [\n            {\n              country_code: 'hr',\n              type: 'country',\n            },\n            {\n              country_code: 'dk',\n              type: 'country',\n            },\n          ],\n        },\n      ],\n    });\n\n  await remoteLink.create({\n    [Modules.STOCK_LOCATION]: {\n      stock_location_id: stockLocation.id,\n    },\n    [Modules.FULFILLMENT]: {\n      fulfillment_set_id: pickupFulfillmentSet.id,\n    },\n  });\n\n  await createShippingOptionsWorkflow(container).run({\n    input: [\n      {\n        name: 'Denmark Store Pickup',\n        price_type: 'flat',\n        provider_id: 'manual_manual',\n        service_zone_id: pickupFulfillmentSet.service_zones[0].id,\n        shipping_profile_id: shippingProfile.id,\n        type: {\n          label: 'Denmark Store Pickup',\n          description: 'Free in-store pickup.',\n          code: 'standard',\n        },\n        prices: [\n          {\n            currency_code: 'usd',\n            amount: 0,\n          },\n          {\n            currency_code: 'eur',\n            amount: 0,\n          },\n          {\n            region_id: region.id,\n            amount: 0,\n          },\n        ],\n        rules: [\n          {\n            attribute: 'enabled_in_store',\n            value: '\"true\"',\n            operator: 'eq',\n          },\n          {\n            attribute: 'is_return',\n            value: 'false',\n            operator: 'eq',\n          },\n        ],\n      },\n    ],\n  });\n\n  logger.info('Finished seeding fulfillment data.');\n\n  await linkSalesChannelsToStockLocationWorkflow(container).run({\n    input: {\n      id: stockLocation.id,\n      add: [defaultSalesChannel[0].id],\n    },\n  });\n  logger.info('Finished seeding stock location data.');\n\n  logger.info('Seeding publishable API key data...');\n  const { result: publishableApiKeyResult } = await createApiKeysWorkflow(\n    container,\n  ).run({\n    input: {\n      api_keys: [\n        {\n          title: 'Webshop',\n          type: 'publishable',\n          created_by: '',\n        },\n      ],\n    },\n  });\n  const publishableApiKey = publishableApiKeyResult[0];\n\n  await linkSalesChannelsToApiKeyWorkflow(container).run({\n    input: {\n      id: publishableApiKey.id,\n      add: [defaultSalesChannel[0].id],\n    },\n  });\n  logger.info('Finished seeding publishable API key data.');\n\n  logger.info('Seeding product data...');\n\n  const { result: categoryResult } = await createProductCategoriesWorkflow(\n    container,\n  ).run({\n    input: {\n      product_categories: [\n        {\n          name: 'One seater',\n          is_active: true,\n        },\n        {\n          name: 'Two seater',\n          is_active: true,\n        },\n        {\n          name: 'Three seater',\n          is_active: true,\n        },\n      ],\n    },\n  });\n\n  const [sofasImage, armChairsImage] = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'sofas.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/product-types/sofas/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'arm-chairs.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/product-types/arm-chairs/image.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  const { result: productTypes } = await createProductTypesWorkflow(\n    container,\n  ).run({\n    input: {\n      product_types: [\n        {\n          value: 'Sofas',\n          metadata: {\n            image: sofasImage,\n          },\n        },\n        {\n          value: 'Arm Chairs',\n          metadata: {\n            image: armChairsImage,\n          },\n        },\n      ],\n    },\n  });\n\n  const [\n    scandinavianSimplicityImage,\n    scandinavianSimplicityCollectionPageImage,\n    scandinavianSimplicityProductPageImage,\n    scandinavianSimplicityProductPageWideImage,\n    scandinavianSimplicityProductPageCtaImage,\n    modernLuxeImage,\n    modernLuxeCollectionPageImage,\n    modernLuxeProductPageImage,\n    modernLuxeProductPageWideImage,\n    modernLuxeProductPageCtaImage,\n    bohoChicImage,\n    bohoChicCollectionPageImage,\n    bohoChicProductPageImage,\n    bohoChicProductPageWideImage,\n    bohoChicProductPageCtaImage,\n    timelessClassicsImage,\n    timelessClassicsCollectionPageImage,\n    timelessClassicsProductPageImage,\n    timelessClassicsProductPageWideImage,\n    timelessClassicsProductPageCtaImage,\n  ] = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'scandinavian-simplicity.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/scandinavian-simplicity/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'scandinavian-simplicity-collection-page-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/scandinavian-simplicity/collection_page_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'scandinavian-simplicity-product-page-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/scandinavian-simplicity/product_page_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'scandinavian-simplicity-product-page-wide-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/scandinavian-simplicity/product_page_wide_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'scandinavian-simplicity-product-page-cta-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/scandinavian-simplicity/product_page_cta_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'modern-luxe.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/modern-luxe/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'modern-luxe-collection-page-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/modern-luxe/collection_page_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'modern-luxe-product-page-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/modern-luxe/product_page_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'modern-luxe-product-page-wide-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/modern-luxe/product_page_wide_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'modern-luxe-product-page-cta-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/modern-luxe/product_page_cta_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'boho-chic.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/boho-chic/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'boho-chic-collection-page-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/boho-chic/collection_page_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'boho-chic-product-page-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/boho-chic/product_page_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'boho-chic-product-page-wide-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/boho-chic/product_page_wide_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'boho-chic-product-page-cta-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/boho-chic/product_page_cta_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'timeless-classics.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/timeless-classics/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'timeless-classics-collection-page-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/timeless-classics/collection_page_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'timeless-classics-product-page-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/timeless-classics/product_page_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'timeless-classics-product-page-wide-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/timeless-classics/product_page_wide_image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'timeless-classics-product-page-cta-image.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/collections/timeless-classics/product_page_cta_image.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  const { result: collections } = await createCollectionsWorkflow(\n    container,\n  ).run({\n    input: {\n      collections: [\n        {\n          title: 'Scandinavian Simplicity',\n          handle: 'scandinavian-simplicity',\n          metadata: {\n            description:\n              'Minimalistic designs, neutral colors, and high-quality textures',\n            image: scandinavianSimplicityImage,\n            collection_page_image: scandinavianSimplicityCollectionPageImage,\n            collection_page_heading:\n              'Scandinavian Simplicity: Effortless elegance, timeless comfort',\n            collection_page_content: `Minimalistic designs, neutral colors, and high-quality textures. Perfect for those who seek comfort with a clean and understated aesthetic.\n\nThis collection brings the essence of Scandinavian elegance to your living room.`,\n            product_page_heading: 'Collection Inspired Interior',\n            product_page_image: scandinavianSimplicityProductPageImage,\n            product_page_wide_image: scandinavianSimplicityProductPageWideImage,\n            product_page_cta_image: scandinavianSimplicityProductPageCtaImage,\n            product_page_cta_heading:\n              \"The 'Name of sofa' embodies Scandinavian minimalism with clean lines and a soft, neutral palette.\",\n            product_page_cta_link:\n              'See more out of ‘Scandinavian Simplicity’ collection',\n          },\n        },\n        {\n          title: 'Modern Luxe',\n          handle: 'modern-luxe',\n          metadata: {\n            description:\n              'Sophisticated and sleek, these sofas blend modern design with luxurious comfort',\n            image: modernLuxeImage,\n            collection_page_image: modernLuxeCollectionPageImage,\n            collection_page_heading:\n              'Modern Luxe: Where modern design meets luxurious living',\n            collection_page_content: `Sophisticated and sleek, these sofas blend modern design with luxurious comfort. Bold lines and premium materials create the ultimate statement pieces for any contemporary home.\n\nElevate your space with timeless beauty.`,\n            product_page_heading: 'Collection Inspired Interior',\n            product_page_image: modernLuxeProductPageImage,\n            product_page_wide_image: modernLuxeProductPageWideImage,\n            product_page_cta_image: modernLuxeProductPageCtaImage,\n            product_page_cta_heading:\n              \"The 'Name of sofa' is a masterpiece of minimalism and luxury.\",\n            product_page_cta_link: 'See more out of ‘Modern Luxe’ collection',\n          },\n        },\n        {\n          title: 'Boho Chic',\n          handle: 'boho-chic',\n          metadata: {\n            description:\n              'Infused with playful textures and vibrant patterns with eclectic vibes',\n            image: bohoChicImage,\n            collection_page_image: bohoChicCollectionPageImage,\n            collection_page_heading:\n              'Boho Chic: Relaxed, eclectic style with a touch of free-spirited charm',\n            collection_page_content: `Infused with playful textures and vibrant patterns, this collection embodies relaxed, eclectic vibes. Soft fabrics and creative designs add warmth and personality to any room.\n\nIt’s comfort with a bold, carefree spirit.`,\n            product_page_heading: 'Collection Inspired Interior',\n            product_page_image: bohoChicProductPageImage,\n            product_page_wide_image: bohoChicProductPageWideImage,\n            product_page_cta_image: bohoChicProductPageCtaImage,\n            product_page_cta_heading:\n              \"The 'Name of sofa' captures the essence of boho style with its relaxed, oversized form and eclectic fabric choices.\",\n            product_page_cta_link: 'See more out of ‘Boho Chic’ collection',\n          },\n        },\n        {\n          title: 'Timeless Classics',\n          handle: 'timeless-classics',\n          metadata: {\n            description:\n              'Elegant shapes and rich textures, traditional craftsmanship with modern comfort',\n            image: timelessClassicsImage,\n            collection_page_image: timelessClassicsCollectionPageImage,\n            collection_page_heading:\n              'Timeless Classics: Enduring style, crafted for comfort and lasting beauty',\n            collection_page_content: `Designed for those who appreciate enduring style, this collection features elegant shapes and rich textures. These sofas combine traditional craftsmanship with modern comfort.\n\nPerfect for creating a warm, inviting atmosphere that never goes out of style.`,\n            product_page_heading: 'Collection Inspired Interior',\n            product_page_image: timelessClassicsProductPageImage,\n            product_page_wide_image: timelessClassicsProductPageWideImage,\n            product_page_cta_image: timelessClassicsProductPageCtaImage,\n            product_page_cta_heading:\n              \"The 'Name of sofa' brings a touch of traditional charm with its elegant curves and classic silhouette\",\n            product_page_cta_link:\n              'See more out of ‘Timeless Classics’ collection',\n          },\n        },\n      ],\n    },\n  });\n\n  const materials: MaterialModelType[] =\n    await fashionModuleService.createMaterials([\n      {\n        name: 'Velvet',\n      },\n      {\n        name: 'Linen',\n      },\n      {\n        name: 'Boucle',\n      },\n      {\n        name: 'Leather',\n      },\n      {\n        name: 'Microfiber',\n      },\n    ]);\n\n  await fashionModuleService.createColors([\n    // Velvet\n    {\n      name: 'Black',\n      hex_code: '#4C4D4E',\n      material_id: materials.find((m) => m.name === 'Velvet').id,\n    },\n    {\n      name: 'Purple',\n      hex_code: '#904C94',\n      material_id: materials.find((m) => m.name === 'Velvet').id,\n    },\n    // Linen\n    {\n      name: 'Green',\n      hex_code: '#438849',\n      material_id: materials.find((m) => m.name === 'Linen').id,\n    },\n    {\n      name: 'Light Gray',\n      hex_code: '#B1B1B1',\n      material_id: materials.find((m) => m.name === 'Linen').id,\n    },\n    {\n      name: 'Yellow',\n      hex_code: '#F1BD37',\n      material_id: materials.find((m) => m.name === 'Linen').id,\n    },\n    {\n      name: 'Red',\n      hex_code: '#CD1F23',\n      material_id: materials.find((m) => m.name === 'Linen').id,\n    },\n    {\n      name: 'Blue',\n      hex_code: '#475F8A',\n      material_id: materials.find((m) => m.name === 'Linen').id,\n    },\n    // Microfiber\n    {\n      name: 'Orange',\n      hex_code: '#EF7218',\n      material_id: materials.find((m) => m.name === 'Microfiber').id,\n    },\n    {\n      name: 'Dark Gray',\n      hex_code: '#4A4A4A',\n      material_id: materials.find((m) => m.name === 'Microfiber').id,\n    },\n    {\n      name: 'Black',\n      hex_code: '#282828',\n      material_id: materials.find((m) => m.name === 'Microfiber').id,\n    },\n    // Boucle\n    {\n      name: 'Beige',\n      hex_code: '#C8BCB3',\n      material_id: materials.find((m) => m.name === 'Boucle').id,\n    },\n    {\n      name: 'White',\n      hex_code: '#EAEAEA',\n      material_id: materials.find((m) => m.name === 'Boucle').id,\n    },\n    {\n      name: 'Light Gray',\n      hex_code: '#C3C0BE',\n      material_id: materials.find((m) => m.name === 'Boucle').id,\n    },\n    // Leather\n    {\n      name: 'Violet',\n      hex_code: '#B1ABBF',\n      material_id: materials.find((m) => m.name === 'Leather').id,\n    },\n    {\n      name: 'Beige',\n      hex_code: '#A79D9B',\n      material_id: materials.find((m) => m.name === 'Leather').id,\n    },\n  ]);\n\n  const astridCurveImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'astrid-curve.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/astrid-curve/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'astrid-curve-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/astrid-curve/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Astrid Curve',\n          handle: 'astrid-curve',\n          description:\n            'The Astrid Curve combines flowing curves and cozy, textured fabric for a truly bohemian vibe. Its relaxed design adds character and comfort, perfect for eclectic living spaces with a free-spirited charm.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Three seater').id,\n          ],\n          collection_id: collections.find((c) => c.handle === 'boho-chic').id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: astridCurveImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Microfiber', 'Velvet'],\n            },\n            {\n              title: 'Color',\n              values: ['Dark Gray', 'Purple'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Microfiber / Dark Gray',\n              sku: 'ASTRID-CURVE-MICROFIBER-DARK-GRAY',\n              options: {\n                Material: 'Microfiber',\n                Color: 'Dark Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Velvet / Purple',\n              sku: 'ASTRID-CURVE-VELVET-PURPLE',\n              options: {\n                Material: 'Velvet',\n                Color: 'Purple',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const belimeEstateImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'belime-estate.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/belime-estate/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'belime-estate-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/belime-estate/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Belime Estate',\n          handle: 'belime-estate',\n          description:\n            'The Belime Estate exudes classic sophistication with its tufted back and rich fabric. Its luxurious look and enduring comfort make it a perfect fit for traditional, elegant interiors.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Two seater').id,\n          ],\n          collection_id: collections.find(\n            (c) => c.handle === 'timeless-classics',\n          ).id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: belimeEstateImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Linen', 'Boucle'],\n            },\n            {\n              title: 'Color',\n              values: ['Red', 'Blue', 'Beige'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Linen / Red',\n              sku: 'BELIME-ESTATE-LINEN-RED',\n              options: {\n                Material: 'Linen',\n                Color: 'Red',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Linen / Blue',\n              sku: 'BELIME-ESTATE-LINEN-BLUE',\n              options: {\n                Material: 'Linen',\n                Color: 'Blue',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Boucle / Beige',\n              sku: 'BELIME-ESTATE-BOUCLE-BEIGE',\n              options: {\n                Material: 'Boucle',\n                Color: 'Beige',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const cypressRetreatImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'cypress-retreat.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/cypress-retreat/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'cypress-retreat-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/cypress-retreat/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Cypress Retreat',\n          handle: 'cypress-retreat',\n          description:\n            'The Cypress Retreat is a nod to traditional design with its elegant lines and durable, high-quality upholstery. A timeless choice, it offers long-lasting comfort and a refined aesthetic for any home.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Three seater').id,\n          ],\n          collection_id: collections.find(\n            (c) => c.handle === 'timeless-classics',\n          ).id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: cypressRetreatImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Leather'],\n            },\n            {\n              title: 'Color',\n              values: ['Beige', 'Violet'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Leather / Beige',\n              sku: 'CYPRESS-RETREAT-LEATHER-BEIGE',\n              options: {\n                Material: 'Leather',\n                Color: 'Beige',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Leather / Violet',\n              sku: 'CYPRESS-RETREAT-LEATHER-VIOLET',\n              options: {\n                Material: 'Leather',\n                Color: 'Violet',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const everlyEstateImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'everly-estate.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/everly-estate/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'everly-estate-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/everly-estate/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Everly Estate',\n          handle: 'everly-estate',\n          description:\n            'The Everly Estate offers a blend of modern elegance and plush luxury, with its sleek lines and soft velvet upholstery. Perfect for upscale interiors, it exudes sophistication and comfort in equal measure.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Two seater').id,\n          ],\n          collection_id: collections.find((c) => c.handle === 'modern-luxe').id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: everlyEstateImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Microfiber', 'Velvet'],\n            },\n            {\n              title: 'Color',\n              values: ['Orange', 'Black'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Microfiber / Orange',\n              sku: 'EVERLY-ESTATE-MICROFIBER-ORANGE',\n              options: {\n                Material: 'Microfiber',\n                Color: 'Orange',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Velvet / Black',\n              sku: 'EVERLY-ESTATE-VELVET-BLACK',\n              options: {\n                Material: 'Velvet',\n                Color: 'Black',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const havenhillEstateImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'havenhill-estate.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/havenhill-estate/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'havenhill-estate-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/havenhill-estate/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Havenhill Estate',\n          handle: 'havenhill-estate',\n          description:\n            'The Havenhill Estate brings a touch of traditional charm with its elegant curves and classic silhouette. Upholstered in durable, luxurious fabric, it’s a timeless piece that combines comfort and style, fitting seamlessly into any sophisticated home.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'One seater').id,\n          ],\n          collection_id: collections.find(\n            (c) => c.handle === 'timeless-classics',\n          ).id,\n          type_id: productTypes.find((pt) => pt.value === 'Arm Chairs').id,\n          status: ProductStatus.PUBLISHED,\n          images: havenhillEstateImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Linen', 'Boucle'],\n            },\n            {\n              title: 'Color',\n              values: ['Green', 'Light Gray', 'Yellow'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Linen / Green',\n              sku: 'HAVENHILL-ESTATE-LINEN-GREEN',\n              options: {\n                Material: 'Linen',\n                Color: 'Green',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Boucle / Light Gray',\n              sku: 'HAVENHILL-ESTATE-BOUCLE-LIGHT-GRAY',\n              options: {\n                Material: 'Boucle',\n                Color: 'Light Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1200,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1400,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const monacoFlairImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'monaco-flair.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/monaco-flair/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'monaco-flair-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/monaco-flair/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Monaco Flair',\n          handle: 'monaco-flair',\n          description:\n            'The Monaco Flair combines sleek metallic accents with rich fabric, delivering a bold, luxurious statement. Its minimalist design and deep seating make it a standout piece for modern living rooms.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Three seater').id,\n          ],\n          collection_id: collections.find((c) => c.handle === 'modern-luxe').id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: monacoFlairImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Linen', 'Boucle'],\n            },\n            {\n              title: 'Color',\n              values: ['Green', 'Light Gray', 'Beige'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Linen / Green',\n              sku: 'MONACO-FLAIR-LINEN-GREEN',\n              options: {\n                Material: 'Linen',\n                Color: 'Green',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Boucle / Light Gray',\n              sku: 'MONACO-FLAIR-BOUCLE-LIGHT-GRAY',\n              options: {\n                Material: 'Boucle',\n                Color: 'Light Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Boucle / Beige',\n              sku: 'MONACO-FLAIR-BOUCLE-BEIGE',\n              options: {\n                Material: 'Boucle',\n                Color: 'Beige',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const nordicBreezeImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'nordic-breeze.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/nordic-breeze/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'nordic-breeze-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/nordic-breeze/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Nordic Breeze',\n          handle: 'nordic-breeze',\n          description:\n            'The Nordic Breeze is a refined expression of Scandinavian minimalism, with its crisp silhouette and airy aesthetic. Crafted for both comfort and simplicity, it’s perfect for creating a serene living space.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'One seater').id,\n          ],\n          collection_id: collections.find(\n            (c) => c.handle === 'scandinavian-simplicity',\n          ).id,\n          type_id: productTypes.find((pt) => pt.value === 'Arm Chairs').id,\n          status: ProductStatus.PUBLISHED,\n          images: nordicBreezeImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Boucle', 'Linen'],\n            },\n            {\n              title: 'Color',\n              values: ['Beige', 'White', 'Light Gray'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Boucle / Beige',\n              sku: 'NORDIC-BREEZE-BOUCLE-BEIGE',\n              options: {\n                Material: 'Boucle',\n                Color: 'Beige',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1200,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1400,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Boucle / White',\n              sku: 'NORDIC-BREEZE-BOUCLE-WHITE',\n              options: {\n                Material: 'Boucle',\n                Color: 'White',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1200,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1400,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Linen / Light Gray',\n              sku: 'NORDIC-BREEZE-LINEN-LIGHT-GRAY',\n              options: {\n                Material: 'Linen',\n                Color: 'Light Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1800,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2000,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const nordicHavenImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'nordic-haven.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/nordic-haven/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'nordic-haven-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/nordic-haven/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Nordic Haven',\n          handle: 'nordic-haven',\n          description:\n            'The Nordic Haven features clean lines and soft textures, embodying the essence of Scandinavian design. Its natural tones and minimalist frame bring effortless serenity and comfort to any home.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Three seater').id,\n          ],\n          collection_id: collections.find(\n            (c) => c.handle === 'scandinavian-simplicity',\n          ).id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: nordicHavenImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Linen', 'Boucle'],\n            },\n            {\n              title: 'Color',\n              values: ['Light Gray', 'White', 'Beige'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Linen / Light Gray',\n              sku: 'NORDIC-HAVEN-LINEN-LIGHT-GRAY',\n              options: {\n                Material: 'Linen',\n                Color: 'Light Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Boucle / White',\n              sku: 'NORDIC-HAVEN-BOUCLE-WHITE',\n              options: {\n                Material: 'Boucle',\n                Color: 'White',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Boucle / Beige',\n              sku: 'NORDIC-HAVEN-BOUCLE-BEIGE',\n              options: {\n                Material: 'Boucle',\n                Color: 'Beige',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const osloDriftImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'oslo-drift.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/oslo-drift/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'oslo-drift-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/oslo-drift/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Oslo Drift',\n          handle: 'oslo-drift',\n          description:\n            'The Oslo Drift is designed for ultimate relaxation, with soft, supportive cushions and a sleek, modern frame. Its understated elegance and neutral tones make it an ideal fit for contemporary, minimalist homes.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Two seater').id,\n          ],\n          collection_id: collections.find(\n            (c) => c.handle === 'scandinavian-simplicity',\n          ).id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: osloDriftImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Boucle', 'Linen'],\n            },\n            {\n              title: 'Color',\n              values: ['White', 'Beige', 'Light Gray'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Boucle / White',\n              sku: 'OSLO-DRIFT-BOUCLE-WHITE',\n              options: {\n                Material: 'Boucle',\n                Color: 'White',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Boucle / Beige',\n              sku: 'OSLO-DRIFT-BOUCLE-BEIGE',\n              options: {\n                Material: 'Boucle',\n                Color: 'Beige',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Linen / Light Gray',\n              sku: 'OSLO-DRIFT-LINEN-LIGHT-GRAY',\n              options: {\n                Material: 'Linen',\n                Color: 'Light Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const osloSerenityImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'oslo-serenity.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/oslo-serenity/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'oslo-serenity-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/oslo-serenity/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Oslo Serenity',\n          handle: 'oslo-serenity',\n          description:\n            'The Oslo Serenity embodies Scandinavian minimalism with clean lines and a soft, neutral palette. Its tailored silhouette and plush cushions deliver a balance of simplicity and comfort, making it perfect for those who value understated elegance.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Two seater').id,\n          ],\n          collection_id: collections.find(\n            (c) => c.handle === 'scandinavian-simplicity',\n          ).id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: osloSerenityImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Leather'],\n            },\n            {\n              title: 'Color',\n              values: ['Violet', 'Beige'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Leather / Violet',\n              sku: 'OSLO-SERENITY-LEATHER-VIOLET',\n              options: {\n                Material: 'Leather',\n                Color: 'Violet',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Leather / Beige',\n              sku: 'OSLO-SERENITY-LEATHER-BEIGE',\n              options: {\n                Material: 'Leather',\n                Color: 'Beige',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const palomaHavenImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'paloma-haven.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/paloma-haven/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'paloma-haven-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/paloma-haven/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Paloma Haven',\n          handle: 'paloma-haven',\n          description:\n            'Minimalistic designs, neutral colors, and high-quality textures. Perfect for those who seek comfort with a clean and understated aesthetic. This collection brings the essence of Scandinavian elegance to your living room.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'One seater').id,\n          ],\n          collection_id: collections.find((c) => c.handle === 'modern-luxe').id,\n          type_id: productTypes.find((pt) => pt.value === 'Arm Chairs').id,\n          status: ProductStatus.PUBLISHED,\n          images: palomaHavenImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Linen', 'Boucle'],\n            },\n            {\n              title: 'Color',\n              values: ['Light Gray', 'Green', 'Beige'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Linen / Light Gray',\n              sku: 'PALOMA-HAVEN-LINEN-LIGHT-GRAY',\n              options: {\n                Material: 'Linen',\n                Color: 'Light Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 900,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1100,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Linen / Green',\n              sku: 'PALOMA-HAVEN-LINEN-GREEN',\n              options: {\n                Material: 'Linen',\n                Color: 'Green',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 900,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1100,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Boucle / Beige',\n              sku: 'PALOMA-HAVEN-BOUCLE-BEIGE',\n              options: {\n                Material: 'Boucle',\n                Color: 'Beige',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1200,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1400,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const savannahGroveImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'savannah-grove.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/savannah-grove/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'savannah-grove-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/savannah-grove/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Savannah Grove',\n          handle: 'savannah-grove',\n          description:\n            'The Savannah Grove captures the essence of boho style with its relaxed, oversized form and eclectic fabric choices. Designed for both comfort and personality, it’s the ideal piece for those who seek a cozy, free-spirited vibe in their living spaces.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'One seater').id,\n          ],\n          collection_id: collections.find((c) => c.handle === 'boho-chic').id,\n          type_id: productTypes.find((pt) => pt.value === 'Arm Chairs').id,\n          status: ProductStatus.PUBLISHED,\n          images: savannahGroveImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Boucle', 'Linen'],\n            },\n            {\n              title: 'Color',\n              values: ['Light Gray', 'Yellow'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Boucle / Light Gray',\n              sku: 'SAVANNAH-GROVE-BOUCLE-LIGHT-GRAY',\n              options: {\n                Material: 'Boucle',\n                Color: 'Light Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1200,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1400,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Linen / Yellow',\n              sku: 'SAVANNAH-GROVE-LINEN-YELLOW',\n              options: {\n                Material: 'Linen',\n                Color: 'Yellow',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 900,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1100,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Linen / Light Gray',\n              sku: 'SAVANNAH-GROVE-LINEN-LIGHT-GRAY',\n              options: {\n                Material: 'Linen',\n                Color: 'Light Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 900,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1100,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const serenaMeadowImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'serena-meadow.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/serena-meadow/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'serena-meadow-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/serena-meadow/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Serena Meadow',\n          handle: 'serena-meadow',\n          description:\n            'The Serena Meadow combines a classic silhouette with modern comfort, offering a relaxed yet polished look. Its soft upholstery and subtle curves bring a timeless elegance to any living room.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Two seater').id,\n          ],\n          collection_id: collections.find(\n            (c) => c.handle === 'timeless-classics',\n          ).id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: serenaMeadowImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Microfiber', 'Velvet'],\n            },\n            {\n              title: 'Color',\n              values: ['Black', 'Dark Gray'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Microfiber / Black',\n              sku: 'SERENA-MEADOW-MICROFIBER-BLACK',\n              options: {\n                Material: 'Microfiber',\n                Color: 'Black',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Microfiber / Dark Gray',\n              sku: 'SERENA-MEADOW-MICROFIBER-DARK-GRAY',\n              options: {\n                Material: 'Microfiber',\n                Color: 'Dark Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Velvet / Black',\n              sku: 'SERENA-MEADOW-VELVET-BLACK',\n              options: {\n                Material: 'Velvet',\n                Color: 'Black',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const suttonRoyaleImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'sutton-royale.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/sutton-royale/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'sutton-royale-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/sutton-royale/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Sutton Royale',\n          handle: 'sutton-royale',\n          description:\n            'The Sutton Royale blends eclectic design with classic bohemian comfort, featuring soft, tufted fabric and a wide, welcoming frame. Its unique style adds a touch of vintage flair to any space.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Two seater').id,\n          ],\n          collection_id: collections.find((c) => c.handle === 'boho-chic').id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: suttonRoyaleImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Velvet', 'Microfiber'],\n            },\n            {\n              title: 'Color',\n              values: ['Purple', 'Dark Gray'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Velvet / Purple',\n              sku: 'SUTTON-ROYALE-VELVET-PURPLE',\n              options: {\n                Material: 'Velvet',\n                Color: 'Purple',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Microfiber / Dark Gray',\n              sku: 'SUTTON-ROYALE-MICROFIBER-DARK-GRAY',\n              options: {\n                Material: 'Microfiber',\n                Color: 'Dark Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const velarLoftImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'velar-loft.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/velar-loft/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'velar-loft-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/velar-loft/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Velar Loft',\n          handle: 'velar-loft',\n          description:\n            'The Velar Loft offers a refined blend of modern design and opulent comfort. Upholstered in rich fabric with sleek metallic accents, this sofa delivers both luxury and a contemporary edge, making it a striking centerpiece for sophisticated interiors.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'One seater').id,\n          ],\n          collection_id: collections.find((c) => c.handle === 'modern-luxe').id,\n          type_id: productTypes.find((pt) => pt.value === 'Arm Chairs').id,\n          status: ProductStatus.PUBLISHED,\n          images: velarLoftImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Velvet', 'Microfiber'],\n            },\n            {\n              title: 'Color',\n              values: ['Black', 'Orange'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Velvet / Black',\n              sku: 'VELAR-LOFT-VELVET-BLACK',\n              options: {\n                Material: 'Velvet',\n                Color: 'Black',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1300,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1500,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Microfiber / Orange',\n              sku: 'VELAR-LOFT-MICROFIBER-ORANGE',\n              options: {\n                Material: 'Microfiber',\n                Color: 'Orange',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1100,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1300,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  const veloraLuxeImages = await uploadFilesWorkflow(container)\n    .run({\n      input: {\n        files: [\n          {\n            access: 'public',\n            filename: 'velora-luxe.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/velora-luxe/image.png',\n            ),\n          },\n          {\n            access: 'public',\n            filename: 'velora-luxe-2.png',\n            mimeType: 'image/png',\n            content: await getImageUrlContent(\n              'https://assets.agilo.com/fashion-starter/products/velora-luxe/image1.png',\n            ),\n          },\n        ],\n      },\n    })\n    .then((res) => res.result);\n\n  await createProductsWorkflow(container).run({\n    input: {\n      products: [\n        {\n          title: 'Velora Luxe',\n          handle: 'velora-luxe',\n          description:\n            'The Velora Luxe brings a touch of luxury to bohemian design with its bold patterns and plush comfort. Its oversized shape and inviting cushions make it an ideal centerpiece for laid-back, stylish interiors.',\n          category_ids: [\n            categoryResult.find((cat) => cat.name === 'Three seater').id,\n          ],\n          collection_id: collections.find((c) => c.handle === 'boho-chic').id,\n          type_id: productTypes.find((pt) => pt.value === 'Sofas').id,\n          status: ProductStatus.PUBLISHED,\n          images: veloraLuxeImages,\n          options: [\n            {\n              title: 'Material',\n              values: ['Linen', 'Boucle'],\n            },\n            {\n              title: 'Color',\n              values: ['Yellow', 'Light Gray'],\n            },\n          ],\n          variants: [\n            {\n              title: 'Linen / Yellow',\n              sku: 'VELORA-LUXE-LINEN-YELLOW',\n              options: {\n                Material: 'Linen',\n                Color: 'Yellow',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 1500,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 1700,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n            {\n              title: 'Boucle / Light Gray',\n              sku: 'VELORA-LUXE-BOUCLE-LIGHT-GRAY',\n              options: {\n                Material: 'Boucle',\n                Color: 'Light Gray',\n              },\n              manage_inventory: false,\n              prices: [\n                {\n                  amount: 2000,\n                  currency_code: 'eur',\n                },\n                {\n                  amount: 2200,\n                  currency_code: 'usd',\n                },\n              ],\n            },\n          ],\n          sales_channels: [\n            {\n              id: defaultSalesChannel[0].id,\n            },\n          ],\n        },\n      ],\n    },\n  });\n\n  logger.info('Finished seeding product data.');\n}\n"
  },
  {
    "path": "medusa/src/subscribers/README.md",
    "content": "# Custom subscribers\n\nSubscribers handle events emitted in the Medusa application.\n\nThe subscriber is created in a TypeScript or JavaScript file under the `src/subscribers` directory.\n\nFor example, create the file `src/subscribers/product-created.ts` with the following content:\n\n```ts\nimport {\n  type SubscriberConfig,\n} from \"@medusajs/medusa\"\n\n// subscriber function\nexport default async function productCreateHandler() {\n  console.log(\"A product was created\")\n}\n\n// subscriber config\nexport const config: SubscriberConfig = {\n  event: \"product.created\",\n}\n```\n\nA subscriber file must export:\n\n- The subscriber function that is an asynchronous function executed whenever the associated event is triggered.\n- A configuration object defining the event this subscriber is listening to.\n\n## Subscriber Parameters\n\nA subscriber receives an object having the following properties:\n\n- `event`: An object holding the event's details. It has a `data` property, which is the event's data payload.\n- `container`: The Medusa container. Use it to resolve modules' main services and other registered resources.\n\n```ts\nimport type {\n  SubscriberArgs,\n  SubscriberConfig,\n} from \"@medusajs/medusa\"\nimport { IProductModuleService } from \"@medusajs/framework/types\"\nimport { Modules } from \"@medusajs/framework/utils\"\n\nexport default async function productCreateHandler({\n  event: { data },\n  container,\n}: SubscriberArgs<{ id: string }>) {\n  const productId = data.id\n\n  const productModuleService: IProductModuleService =\n    container.resolve(Modules.PRODUCT)\n\n  const product = await productModuleService.retrieve(productId)\n\n  console.log(`The product ${product.title} was created`)\n}\n\nexport const config: SubscriberConfig = {\n  event: \"product.created\",\n}\n```"
  },
  {
    "path": "medusa/src/subscribers/auth-password-reset-notification.ts",
    "content": "import type { SubscriberArgs, SubscriberConfig } from \"@medusajs/medusa\";\nimport { ContainerRegistrationKeys, Modules } from \"@medusajs/framework/utils\";\nimport type { CustomerDTO } from \"@medusajs/framework/types\";\n\nexport default async function sendPasswordResetNotification({\n  event: { data },\n  container,\n}: SubscriberArgs<{ entity_id: string; token: string; actor_type: string }>) {\n  const query = container.resolve(ContainerRegistrationKeys.QUERY);\n  const notificationModuleService = container.resolve(Modules.NOTIFICATION);\n\n  const fields = [\n    \"id\",\n    \"email\",\n    \"first_name\",\n    \"last_name\",\n  ] as const satisfies (keyof CustomerDTO)[];\n\n  const { data: customers } = await query.graph({\n    entity: \"customer\",\n    fields,\n    filters: { email: data.entity_id },\n  });\n  const customer = customers[0] as Pick<CustomerDTO, (typeof fields)[number]>;\n\n  await notificationModuleService.createNotifications({\n    to: customer.email,\n    channel: \"email\",\n    template:\n      data.actor_type === \"logged-in-customer\"\n        ? \"auth-password-reset\"\n        : \"auth-forgot-password\",\n    data: { customer, token: data.token },\n  });\n}\n\nexport const config: SubscriberConfig = {\n  event: \"auth.password_reset\",\n};\n"
  },
  {
    "path": "medusa/src/subscribers/customer-welcome-notification.ts",
    "content": "import type { SubscriberArgs, SubscriberConfig } from '@medusajs/medusa';\nimport { ContainerRegistrationKeys, Modules } from '@medusajs/framework/utils';\nimport type { CustomerDTO } from '@medusajs/framework/types';\n\nexport default async function sendCustomerWelcomeNotification({\n  event: { data },\n  container,\n}: SubscriberArgs<{ id: string }>) {\n  const query = container.resolve(ContainerRegistrationKeys.QUERY);\n  const notificationModuleService = container.resolve(Modules.NOTIFICATION);\n\n  const fields = [\n    'id',\n    'email',\n    'first_name',\n    'last_name',\n  ] as const satisfies (keyof CustomerDTO)[];\n\n  const { data: customers } = await query.graph({\n    entity: 'customer',\n    fields,\n    filters: { id: data.id },\n  });\n\n  const customer = customers[0] as Pick<CustomerDTO, (typeof fields)[number]>;\n\n  await notificationModuleService.createNotifications({\n    to: customer.email,\n    channel: 'email',\n    template: 'customer-welcome',\n    data: { customer },\n  });\n}\n\nexport const config: SubscriberConfig = {\n  event: 'customer.welcome',\n};\n"
  },
  {
    "path": "medusa/src/subscribers/index-products.ts",
    "content": "import type { SubscriberArgs, SubscriberConfig } from '@medusajs/medusa';\nimport { Modules } from '@medusajs/framework/utils';\nimport { ISearchService } from '@medusajs/framework/types';\n\nexport default async function indexProductHandler({\n  event: { data, name },\n  container,\n}: SubscriberArgs<{ id: string }>) {\n  const productId = data.id;\n\n  const logger = container.resolve('logger');\n  const productModuleService = container.resolve(Modules.PRODUCT);\n  const meilisearchService = container.resolve(\n    'meilisearchService',\n  ) as ISearchService;\n\n  if (name === 'product.deleted') {\n    await meilisearchService.deleteDocument('products', productId);\n    logger.info(`The product ${productId} was deleted from MeiliSearch`);\n    return;\n  }\n\n  const product = await productModuleService.retrieveProduct(productId, {\n    relations: ['variants', 'options', 'tags', 'collection', 'type', 'images', 'categories'],\n  });\n\n  if (name === 'product.updated') {\n    await meilisearchService.replaceDocuments(\n      'products',\n      [product],\n      'products',\n    );\n    logger.info(\n      `The product ${productId} ${product.title} was updated in MeiliSearch`,\n    );\n    return;\n  }\n\n  await meilisearchService.addDocuments('products', [product], 'products');\n  logger.info(\n    `The product ${productId} ${product.title} was added to MeiliSearch`,\n  );\n}\n\nexport const config: SubscriberConfig = {\n  event: ['product.created', 'product.updated', 'product.deleted'],\n};\n"
  },
  {
    "path": "medusa/src/subscribers/order-placed-notification.ts",
    "content": "import type { SubscriberArgs, SubscriberConfig } from '@medusajs/medusa';\nimport {\n  ContainerRegistrationKeys,\n  MathBN,\n  Modules,\n} from '@medusajs/framework/utils';\nimport type { OrderPlacedEmailProps } from '../modules/resend/emails/order-placed';\n\ntype Country = {\n  iso_2: string;\n  name: string;\n  display_name: string;\n};\n\ntype MathBNInput = Parameters<typeof MathBN.convert>[0];\n\nconst toNumber = (value: MathBNInput | null | undefined): number =>\n  MathBN.convert(value ?? 0).toNumber();\n\nconst buildVariantOptionValues = (item: {\n  variant_option_values?: Record<string, unknown>;\n  variant?: { options?: unknown[] };\n}): Record<string, string> => {\n  if (\n    item.variant_option_values &&\n    Object.keys(item.variant_option_values).length\n  ) {\n    return Object.entries(item.variant_option_values).reduce<\n      Record<string, string>\n    >((acc, [key, value]) => {\n      if (typeof value === 'string') {\n        acc[key] = value;\n      }\n      return acc;\n    }, {});\n  }\n\n  return (\n    item.variant?.options?.reduce<Record<string, string>>((acc, option) => {\n      if (!option || typeof option !== 'object') {\n        return acc;\n      }\n\n      const optionRecord = option as Record<string, unknown>;\n      const optionObject = optionRecord.option;\n      const optionTitle =\n        optionObject && typeof optionObject === 'object'\n          ? (optionObject as Record<string, unknown>).title\n          : undefined;\n      const optionValue = optionRecord.value;\n\n      if (typeof optionTitle === 'string' && typeof optionValue === 'string') {\n        acc[optionTitle] = optionValue;\n      }\n\n      return acc;\n    }, {}) ?? {}\n  );\n};\n\nexport default async function sendOrderConfirmationHandler({\n  event: { data },\n  container,\n}: SubscriberArgs<{ id: string }>) {\n  const query = container.resolve(ContainerRegistrationKeys.QUERY);\n  const notificationModuleService = container.resolve(Modules.NOTIFICATION);\n\n  const {\n    data: [order],\n  } = await query.graph({\n    entity: 'order',\n    fields: [\n      'id',\n      'currency_code',\n      'total',\n      'subtotal',\n      'tax_total',\n      'discount_total',\n      'discount_tax_total',\n      'original_total',\n      'original_tax_total',\n      'item_total',\n      'item_subtotal',\n      'item_tax_total',\n      'original_item_total',\n      'original_item_subtotal',\n      'original_item_tax_total',\n      'shipping_total',\n      'shipping_subtotal',\n      'shipping_tax_total',\n      'original_shipping_tax_total',\n      'original_shipping_subtotal',\n      'original_shipping_total',\n      'email',\n      'shipping_address.*',\n      'billing_address.*',\n      'customer_id',\n      'items.*',\n      'items.variant.options.value',\n      'items.variant.options.option.title',\n      'summary.*',\n    ],\n    filters: { id: data.id },\n  });\n\n  if (!order || !order.email) {\n    return;\n  }\n\n  const countryCodes = [\n    order.shipping_address?.country_code,\n    order.billing_address?.country_code,\n  ].filter(Boolean);\n\n  const countryMap: Map<string, Country> = new Map();\n\n  if (countryCodes.length > 0) {\n    const { data: countries } = await query.graph({\n      entity: 'country',\n      fields: ['iso_2', 'name', 'display_name'],\n      filters: {\n        iso_2: countryCodes,\n      },\n    });\n\n    countries.forEach((country) => {\n      countryMap.set(country.iso_2, {\n        iso_2: country.iso_2,\n        name: country.name,\n        display_name: country.display_name,\n      });\n    });\n  }\n\n  const getFallbackCountry = (countryCode: string): Country => ({\n    iso_2: countryCode,\n    name: countryCode.toUpperCase(),\n    display_name: countryCode.toUpperCase(),\n  });\n\n  const shippingAddressForEmail = order.shipping_address\n    ? {\n        ...order.shipping_address,\n        country: order.shipping_address.country_code\n          ? (countryMap.get(order.shipping_address.country_code) ??\n            getFallbackCountry(order.shipping_address.country_code))\n          : undefined,\n      }\n    : order.shipping_address;\n\n  const billingAddressForEmail = order.billing_address\n    ? {\n        ...order.billing_address,\n        country: order.billing_address.country_code\n          ? (countryMap.get(order.billing_address.country_code) ??\n            getFallbackCountry(order.billing_address.country_code))\n          : undefined,\n      }\n    : order.billing_address;\n\n  const transformedItems = order.items.map((item) => ({\n    id: item.id,\n    quantity: Math.trunc(toNumber(item.quantity)),\n    total: toNumber(item.total),\n    thumbnail:\n      item.thumbnail ??\n      item.product.thumbnail ??\n      item.product.images?.[0]?.url ??\n      null,\n    product_title: item.product_title ?? '',\n    variant_title: item.variant_title ?? '',\n    variant_option_values: buildVariantOptionValues(item),\n  }));\n\n  const orderForEmail = {\n    ...order,\n    subtotal: toNumber(order.subtotal),\n    shipping_total: toNumber(order.shipping_total),\n    total: toNumber(order.total),\n    tax_total: toNumber(order.tax_total),\n    shipping_address: shippingAddressForEmail,\n    billing_address: billingAddressForEmail,\n    items: transformedItems,\n  };\n\n  await notificationModuleService.createNotifications({\n    to: order.email,\n    channel: 'email',\n    template: 'order-placed',\n    data: { order: orderForEmail } satisfies OrderPlacedEmailProps,\n  });\n}\n\nexport const config: SubscriberConfig = {\n  event: 'order.placed',\n};\n"
  },
  {
    "path": "medusa/src/workflows/README.md",
    "content": "# Custom Workflows\n\nA workflow is a series of queries and actions that complete a task.\n\nThe workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory.\n\nFor example:\n\n```ts\nimport { \n  createStep,\n  createWorkflow,\n  StepResponse,\n} from \"@medusajs/framework/workflows-sdk\"\n\nconst step1 = createStep(\"step-1\", async () => {\n  return new StepResponse(`Hello from step one!`)\n})\n\ntype WorkflowInput = {\n  name: string\n}\n\nconst step2 = createStep(\n  \"step-2\",\n  async ({ name }: WorkflowInput) => {\n    return new StepResponse(`Hello ${name} from step two!`)\n  }\n)\n\ntype WorkflowOutput = {\n  message: string\n}\n\nconst myWorkflow = createWorkflow<\n  WorkflowInput,\n  WorkflowOutput\n>(\"hello-world\", function (input) {\n  const str1 = step1()\n  // to pass input\n  step2(input)\n\n  return {\n    message: str1,\n  }\n})\n\nexport default myWorkflow\n```\n\n## Execute Workflow\n\nYou can execute the workflow from other resources, such as API routes, scheduled jobs, or subscribers.\n\nFor example, to execute the workflow in an API route:\n\n```ts\nimport type {\n  MedusaRequest,\n  MedusaResponse,\n} from \"@medusajs/medusa\"\nimport myWorkflow from \"../../../workflows/hello-world\"\n\nexport async function GET(\n  req: MedusaRequest,\n  res: MedusaResponse\n) {\n  const { result } = await myWorkflow(req.scope)\n    .run({\n      input: {\n        name: req.query.name as string,\n      },\n    })\n\n  res.send(result)\n}\n```\n"
  },
  {
    "path": "medusa/src/workflows/emit-customer-welcome-event.ts",
    "content": "import {\n  createWorkflow,\n  WorkflowResponse,\n} from '@medusajs/framework/workflows-sdk';\nimport { emitEventStep } from '@medusajs/medusa/core-flows';\n\nconst emitCustomerWelcomeEvent = createWorkflow(\n  'emit-customer-welcome-event',\n  function (input: { id: string }) {\n    emitEventStep({\n      eventName: 'customer.welcome',\n      data: {\n        id: input.id,\n      },\n    });\n\n    return new WorkflowResponse({ id: input.id });\n  },\n);\n\nexport default emitCustomerWelcomeEvent;\n"
  },
  {
    "path": "medusa/src/workflows/index-products.ts",
    "content": "import {\n  createStep,\n  createWorkflow,\n  StepResponse,\n  WorkflowResponse,\n} from '@medusajs/framework/workflows-sdk';\nimport { Modules } from '@medusajs/framework/utils';\nimport { ISearchService, ProductDTO } from '@medusajs/framework/types';\n\nconst retrieveProductsStep = createStep(\n  {\n    name: 'retrieveProductsStep',\n  },\n  async (input: undefined, context) => {\n    const productModuleService = context.container.resolve(Modules.PRODUCT);\n\n    const products = await productModuleService.listProducts(undefined, {\n      relations: [\n        'variants',\n        'options',\n        'tags',\n        'collection',\n        'type',\n        'images',\n      ],\n    });\n\n    return new StepResponse(products);\n  },\n);\n\nconst indexProductsStep = createStep(\n  {\n    name: 'indexProductsStep',\n  },\n  async (input: ProductDTO[], context) => {\n    const meilisearchService = context.container.resolve(\n      'meilisearchService',\n    ) as ISearchService;\n    const result = await meilisearchService.addDocuments(\n      'products',\n      input,\n      'products',\n    );\n    return new StepResponse(result);\n  },\n);\n\nexport const indexProductsWorkflow = createWorkflow(\n  {\n    name: 'indexProducts',\n    idempotent: true,\n    retentionTime: 60 * 60 * 24 * 3, // 3 days\n    store: true,\n  },\n  () => {\n    const products = retrieveProductsStep();\n    const result = indexProductsStep(products);\n\n    return new WorkflowResponse(result);\n  },\n);\n"
  },
  {
    "path": "medusa/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"es2021\"],\n    \"target\": \"es2021\",\n    \"allowJs\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"Node16\",\n    \"moduleResolution\": \"Node16\",\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"skipLibCheck\": true,\n    \"skipDefaultLibCheck\": true,\n    \"declaration\": false,\n    \"sourceMap\": false,\n    \"outDir\": \"./.medusa/server\",\n    \"rootDir\": \"./\",\n    \"baseUrl\": \".\",\n    \"jsx\": \"react-jsx\",\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"checkJs\": false\n  },\n  \"ts-node\": {\n    \"swc\": true\n  },\n  \"include\": [\"**/*\", \".medusa/types/*\"],\n  \"exclude\": [\n    \"**/__tests__\",\n    \"**/__fixtures__\",\n    \"node_modules\",\n    \".medusa/server\",\n    \".medusa/admin\",\n    \".cache\"\n  ]\n}\n"
  },
  {
    "path": "storefront/.github/scripts/medusa-config.js",
    "content": "const { defineConfig, loadEnv } = require(\"@medusajs/utils\")\n\nloadEnv(process.env.NODE_ENV || \"development\", process.cwd())\n\n// CORS when consuming Medusa from admin\n// Medusa's docs are added for a better learning experience. Feel free to remove.\nconst ADMIN_CORS = `${\n  process.env.ADMIN_CORS?.length\n    ? `${process.env.ADMIN_CORS},`\n    : \"http://localhost:7000,http://localhost:7001,\"\n}https://docs.medusajs.com,https://medusa-docs-v2-git-docs-v2-medusajs.vercel.app,https://medusa-resources-git-docs-v2-medusajs.vercel.app`\n\n// CORS to avoid issues when consuming Medusa from a client\n// Medusa's docs are added for a better learning experience. Feel free to remove.\nconst STORE_CORS = `${\n  process.env.STORE_CORS?.length\n    ? `${process.env.STORE_CORS},`\n    : \"http://localhost:8000,\"\n}https://docs.medusajs.com,https://medusa-docs-v2-git-docs-v2-medusajs.vercel.app,https://medusa-resources-git-docs-v2-medusajs.vercel.app`\n\nconst DATABASE_URL =\n  process.env.DATABASE_URL || \"postgres://medusa:password@localhost/medusa\"\n\nconst REDIS_URL = process.env.REDIS_URL || \"redis://localhost:6379\"\n\nexport default defineConfig({\n  plugins: [\n    `medusa-fulfillment-manual`,\n    `medusa-payment-manual`,\n    {\n      resolve: `@medusajs/file-local`,\n      options: {\n        upload_dir: \"uploads\",\n      },\n    },\n    {\n      resolve: `medusa-plugin-meilisearch`,\n      options: {\n        config: {\n          host: process.env.MEILISEARCH_HOST,\n          apiKey: process.env.MEILISEARCH_API_KEY,\n        },\n        settings: {\n          products: {\n            indexSettings: {\n              searchableAttributes: [\"title\", \"description\", \"variant_sku\"],\n              displayedAttributes: [\n                \"id\",\n                \"title\",\n                \"description\",\n                \"variant_sku\",\n                \"thumbnail\",\n                \"handle\",\n              ],\n            },\n            primaryKey: \"id\",\n          },\n        },\n      },\n    },\n  ],\n  admin: {\n    backendUrl: \"http://localhost:9000\",\n  },\n  projectConfig: {\n    databaseUrl: DATABASE_URL,\n    http: {\n      storeCors: STORE_CORS,\n      adminCors: ADMIN_CORS,\n      authCors: process.env.AUTH_CORS || ADMIN_CORS,\n      jwtSecret: process.env.JWT_SECRET || \"supersecret\",\n      cookieSecret: process.env.COOKIE_SECRET || \"supersecret\",\n    },\n    redisUrl: REDIS_URL,\n  },\n})\n"
  },
  {
    "path": "storefront/.github/workflows/test-e2e.yaml",
    "content": "name: Medusa NextJS Template Tests\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\n\nenv:\n  PGHOST: localhost\n  PGPORT: 5432\n  PGUSER: postgres\n  PGPASSWORD: password\n  PGDATABASE: postgres\n\n  TEST_POSTGRES_USER: test_medusa_user\n  TEST_POSTGRES_PASSWORD: password\n  TEST_POSTGRES_DATABASE: test_medusa_db\n  TEST_POSTGRES_DATABASE_TEMPLATE: test_medusa_db_template\n  TEST_POSTGRES_HOST: localhost\n  TEST_POSTGREST_PORT: 5432\n  PRODUCTION_POSTGRES_DATABASE: medusa_db\n  CLIENT_SERVER: http://localhost:9000\n\n  JWT_SECRET: something\n  COOKIE_SECRET: something\n\n  DATABASE_TYPE: \"postgres\"\n  REDIS_URL: redis://localhost:6379\n  DATABASE_URL: postgres://test_medusa_user:password@localhost/test_medusa_db\n  MEILISEARCH_HOST: http://localhost:7700\n  MEILISEARCH_API_KEY: meili_api_key\n\n  NEXT_PUBLIC_BASE_URL: http://localhost:8000\n  NEXT_PUBLIC_DEFAULT_REGION: us\n  NEXT_PUBLIC_MEDUSA_BACKEND_URL: http://localhost:9000\n  NEXT_PUBLIC_INDEX_NAME: products\n  NEXT_PUBLIC_SEARCH_ENDPOINT: http://127.0.0.1:7700\n  NEXT_PUBLIC_SEARCH_API_KEY: meili_api_key\n  REVALIDATE_SECRET: supersecret\n\njobs:\n  e2e-test-runner:\n    timeout-minutes: 20\n    runs-on:\n      - ubuntu-latest\n    services:\n      postgres:\n        image: postgres:latest\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: password\n          POSTGRES_DB: test\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n      meilisearch:\n        image: getmeili/meilisearch:v1.7\n        env:\n          MEILI_MASTER_KEY: meili_api_key\n          MEILI_ENV: development\n        ports:\n          - 7700:7700\n        options: >-\n          --health-cmd \"curl --fail http://localhost:7700/health\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n      redis:\n        image: redis:latest\n        ports:\n          - 6379:6379\n        options: >-\n          --health-cmd \"redis-cli ping\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Use Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: \"20\"\n\n      - name: Initialize PostgreSQL\n        run: |\n          echo \"Initializing Databases\"\n          psql -h localhost -U postgres -d test -c \"CREATE USER ${{ env.TEST_POSTGRES_USER }} WITH PASSWORD '${{ env.TEST_POSTGRES_PASSWORD }}';\"\n          psql -h localhost -U postgres -d test -c \"CREATE DATABASE ${{ env.TEST_POSTGRES_DATABASE }} OWNER ${{ env.TEST_POSTGRES_USER }};\"\n\n      - name: Install Medusa CLI\n        run: npm install @medusajs/medusa-cli@preview -g\n      - name: Setup medusa backend server\n        working-directory: ../\n        # https://docs.medusajs.com/cli/reference#options\n        run: |\n          medusa new backend \\\n          -y \\\n          --v2 \\\n          --branch feat/v2 \\\n          --skip-db \\\n          --skip-migrations \\\n          --skip-env \\\n          --db-user ${{ env.TEST_POSTGRES_USER }} \\\n          --db-pass ${{ env.TEST_POSTGRES_PASSWORD }} \\\n          --db-database ${{ env.TEST_POSTGRES_DATABASE }} \\\n          --db-host ${{ env.TEST_POSTGRES_HOST }} \\\n          --db-port ${{ env.TEST_POSTGREST_PORT }}\n\n      - name: Setup search in the backend\n        working-directory: ../backend\n        run: yarn add medusa-plugin-meilisearch\n\n      - name: Move custom medusa config to the backend\n        run: cp .github/scripts/medusa-config.js ../backend/medusa-config.js\n\n      - name: Seed data from default seed file\n        working-directory: ../backend\n        run: medusa seed --seed-file=data/seed.json\n\n      - name: Run backend server\n        working-directory: ../backend\n        run: medusa develop\n\n      - name: Install packages\n        run: yarn install -y\n\n      - name: Install playwright\n        run: yarn playwright install --with-deps\n\n      - name: Setup frontend\n        run: yarn build\n\n      - name: Run Tests\n        run: yarn test-e2e\n\n      - uses: actions/upload-artifact@v3\n        if: always()\n        with:\n          name: playwright-report\n          path: test-results\n          retention-days: 30\n"
  },
  {
    "path": "storefront/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# IDEs\n.idea\n.vscode\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnode_modules\n\n.swc\ndump.rdb\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/playwright/.auth\n"
  },
  {
    "path": "storefront/.prettierrc",
    "content": "{\n  \"arrowParens\": \"always\",\n  \"semi\": false,\n  \"endOfLine\": \"auto\",\n  \"singleQuote\": false,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"es5\"\n}\n"
  },
  {
    "path": "storefront/.yarnrc.yml",
    "content": "nodeLinker: node-modules\n"
  },
  {
    "path": "storefront/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Medusa\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "storefront/README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://www.medusajs.com\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://user-images.githubusercontent.com/59018053/229103275-b5e482bb-4601-46e6-8142-244f531cebdb.svg\">\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg\">\n    <img alt=\"Medusa logo\" src=\"https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg\">\n    </picture>\n  </a>\n</p>\n\n<h1 align=\"center\">\n  Medusa Next.js Starter Template\n</h1>\n\n<p align=\"center\">\nCombine Medusa's modules for your commerce backend with the newest Next.js 14 features for a performant storefront.</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/medusajs/medusa/blob/master/CONTRIBUTING.md\">\n    <img src=\"https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat\" alt=\"PRs welcome!\" />\n  </a>\n  <a href=\"https://discord.gg/xpCwq3Kfn8\">\n    <img src=\"https://img.shields.io/badge/chat-on%20discord-7289DA.svg\" alt=\"Discord Chat\" />\n  </a>\n  <a href=\"https://twitter.com/intent/follow?screen_name=medusajs\">\n    <img src=\"https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs\" alt=\"Follow @medusajs\" />\n  </a>\n</p>\n\n### Prerequisites\n\nTo use the [Next.js Starter Template](https://medusajs.com/nextjs-commerce/), you should have a Medusa server running locally on port 9000.\nFor a quick setup, run:\n\n```shell\nnpx create-medusa-app@latest\n```\n\nCheck out [create-medusa-app docs](https://docs.medusajs.com/create-medusa-app) for more details and troubleshooting.\n\n# Overview\n\nThe Medusa Next.js Starter is built with:\n\n- [Next.js](https://nextjs.org/)\n- [Tailwind CSS](https://tailwindcss.com/)\n- [Typescript](https://www.typescriptlang.org/)\n- [Medusa](https://medusajs.com/)\n\nFeatures include:\n\n- Full ecommerce support:\n  - Product Detail Page\n  - Product Overview Page\n  - Search with Algolia / MeiliSearch\n  - Product Collections\n  - Cart\n  - Checkout with PayPal and Stripe\n  - User Accounts\n  - Order Details\n- Full Next.js 14 support:\n  - App Router\n  - Next fetching/caching\n  - Server Components\n  - Server Actions\n  - Streaming\n  - Static Pre-Rendering\n\n\n# Quickstart\n\n### Setting up the environment variables\n\nNavigate into your projects directory and get your environment variables ready:\n\n```shell\ncd nextjs-starter-medusa/\nmv .env.template .env.local\n```\n\n### Install dependencies\n\nUse Yarn to install all dependencies.\n\n```shell\nyarn\n```\n\n### Start developing\n\nYou are now ready to start up your project.\n\n```shell\nyarn dev\n```\n\n### Open the code and start customizing\n\nYour site is now running at http://localhost:8000!\n\n# Payment integrations\n\nBy default this starter supports the following payment integrations\n\n- [Stripe](https://stripe.com/)\n- [Paypal](https://www.paypal.com/)\n\nTo enable the integrations you need to add the following to your `.env.local` file:\n\n```shell\nNEXT_PUBLIC_STRIPE_KEY=<your-stripe-public-key>\nNEXT_PUBLIC_PAYPAL_CLIENT_ID=<your-paypal-client-id>\n```\n\nYou will also need to setup the integrations in your Medusa server. See the [Medusa documentation](https://docs.medusajs.com) for more information on how to configure [Stripe](https://docs.medusajs.com/add-plugins/stripe) and [PayPal](https://docs.medusajs.com/add-plugins/paypal) in your Medusa project.\n\n# Search integration\n\nThis starter is configured to support using the `medusa-search-meilisearch` plugin out of the box. To enable search you will need to enable the feature flag in `./store.config.json`, which you do by changing the config to this:\n\n```javascript\n{\n  \"features\": {\n    // other features...\n    \"search\": true\n  }\n}\n```\n\nBefore you can search you will need to install the plugin in your Medusa server, for a written guide on how to do this – [see our documentation](https://docs.medusajs.com/add-plugins/meilisearch).\n\nThe search components in this starter are developed with Algolia's `react-instant-search-hooks-web` library which should make it possible for you to seemlesly change your search provider to Algolia instead of MeiliSearch.\n\nTo do this you will need to add `algoliasearch` to the project, by running\n\n```shell\nyarn add algoliasearch\n```\n\nAfter this you will need to switch the current MeiliSearch `SearchClient` out with a Alogolia client. To do this update `@lib/search-client`.\n\n```ts\nimport algoliasearch from \"algoliasearch/lite\"\n\nconst appId = process.env.NEXT_PUBLIC_SEARCH_APP_ID || \"test_app_id\" // You should add this to your environment variables\n\nconst apiKey = process.env.NEXT_PUBLIC_SEARCH_API_KEY || \"test_key\"\n\nexport const searchClient = algoliasearch(appId, apiKey)\n\nexport const SEARCH_INDEX_NAME =\n  process.env.NEXT_PUBLIC_INDEX_NAME || \"products\"\n```\n\nThen, in `src/app/(main)/search/actions.ts`, remove the MeiliSearch code (line 10-16) and uncomment the Algolia code.\n\n```ts\n\"use server\"\n\nimport { searchClient, SEARCH_INDEX_NAME } from \"@lib/search-client\"\n\n/**\n * Uses MeiliSearch or Algolia to search for a query\n * @param {string} query - search query\n */\nexport async function search(query: string) {\n  const index = searchClient.initIndex(SEARCH_INDEX_NAME)\n  const { hits } = await index.search(query)\n\n  return hits\n}\n```\n\nAfter this you will need to set up Algolia with your Medusa server, and then you should be good to go. For a more thorough walkthrough of using Algolia with Medusa – [see our documentation](https://docs.medusajs.com/add-plugins/algolia), and the [documentation for using `react-instantsearch-hooks-web`](https://www.algolia.com/doc/guides/building-search-ui/getting-started/react-hooks/).\n\n## App structure\n\nFor the new version, the main folder structure remains unchanged. The contents have changed quite a bit though.\n\n```\n.\n└── src\n    ├── app\n    ├── lib\n    ├── modules\n    ├── styles\n    ├── types\n    └── middleware.ts\n\n```\n\n### `/app` directory\n\nThe app folder contains all Next.js App Router pages and layouts, and takes care of the routing.\n\n```\n.\n└── [countryCode]\n    ├── (checkout)\n        └── checkout\n    └── (main)\n        ├── account\n        │   ├── addresses\n        │   └── orders\n        │       └── details\n        │           └── [id]\n        ├── cart\n        ├── categories\n        │   └── [...category]\n        ├── collections\n        │   └── [handle]\n        ├── order\n        │   └── confirmed\n        │       └── [id]\n        ├── products\n        │   └── [handle]\n        ├── results\n        │   └── [query]\n        ├── search\n        └── store\n```\n\nThe app router folder structure represents the routes of the Starter. In this case, the structure is as follows:\n\n- The root directory is represented by the `[countryCode]` folder. This indicates a dynamic route based on the country code. The this will be populated by the countries you set up in your Medusa server. The param is then used to fetch region specific prices, languages, etc.\n- Within the root directory, there two Route Groups: `(checkout)` and `(main)`. This is done because the checkout flow uses a different layout.  All other parts of the app share the same layout and are in subdirectories of the `(main)` group. Route Groups do not affect the url.\n- Each of these subdirectories may have further subdirectories. For instance, the `account` directory has `addresses` and `orders` subdirectories. The `orders` directory further has a `details` subdirectory, which itself has a dynamic `[id]` subdirectory.\n- This nested structure allows for specific routing to various pages within the application. For example, a URL like `/account/orders/details/123` would correspond to the `account > orders > details > [id]` path in the router structure, with `123` being the dynamic `[id]`.\n\nThis structure enables efficient routing and organization of different parts of the Starter.\n\n### `/lib` **directory**\n\nThe lib directory contains all utilities like the Medusa JS client functions, util functions, config and constants. \n\nThe most important file here is `/lib/data/index.ts`. This file defines various functions for interacting with the Medusa API, using the JS client. The functions cover a range of actions related to shopping carts, orders, shipping, authentication, customer management, regions, products, collections, and categories. It also includes utility functions for handling headers and errors, as well as some functions for sorting and transforming product data.\n\nThese functions are used in different Server Actions.\n\n### `/modules` directory\n\nThis is where all the components, templates and Server Actions are, grouped by section. Some subdirectories have an `actions.ts` file. These files contain all Server Actions relevant to that section of the app.\n\n### `/styles` directory\n\n`global.css` imports Tailwind classes and defines a couple of global CSS classes. Tailwind and Medusa UI classes are used for styling throughout the app.\n\n### `/types` directory\n\nContains global TypeScript type defintions.\n\n### `middleware.ts`\n\nNext.js Middleware, which is basically an Edge function that runs before (almost) every request. In our case it enforces a `countryCode` in the url. So when a user visits any url on your storefront without a `countryCode` param, it will redirect the user to the url for the most relevant region.\n\nThe region will be decided as follows:\n\n- When deployed on Vercel and you’re active in the user’s current country, it will use the country code from the `x-vercel-ip-country` header.\n- Else, if you have defined a `NEXT_PUBLIC_DEFAULT_REGION` environment variable, it will redirect to that.\n- Else, it will redirect the user to the first region it finds on your Medusa server.\n\nIf you want to use the `countryCode` param in your code, there’s two ways to do that:\n\n1. On the server in any `page.tsx` - the `countryCode` is in the `params` object:\n    \n    ```tsx\n    export default async function Page({\n      params: { countryCode },\n    }: {\n      params: { countryCode: string }\n    }) {\n      const region = await getRegion(countryCode)\n    \n    // rest of code\n    ```\n    \n2. From client components, with the `useParam` hook:\n    \n    ```tsx\n    import { useParams } from \"next/navigation\"\n    \n    const Component = () => {\n    \tconst { countryCode } = useParams()\n    \t\n    \t// rest of code\n    ```\n    \n\nThe middleware also sets a cookie based on the onboarding status of a user. This is related to the Medusa Admin onboarding flow, and may be safely removed in your production storefront.\n\n# Resources\n\n## Learn more about Medusa\n\n- [Website](https://www.medusajs.com/)\n- [GitHub](https://github.com/medusajs)\n- [Documentation](https://docs.medusajs.com/)\n\n## Learn more about Next.js\n\n- [Website](https://nextjs.org/)\n- [GitHub](https://github.com/vercel/next.js)\n- [Documentation](https://nextjs.org/docs)\n"
  },
  {
    "path": "storefront/check-env-variables.js",
    "content": "const c = require(\"ansi-colors\")\n\nconst requiredEnvs = [\n  {\n    key: \"NEXT_PUBLIC_MEDUSA_BACKEND_URL\",\n    description:\n      \"Your Medusa backend, should be updated to where you are hosting your server. Remember to update CORS settings for your server. See - https://docs.medusajs.com/usage/configurations#admin_cors-and-store_cors.\",\n  },\n  {\n    key: \"NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY\",\n    description:\n      \"Your publishable key that can be attached to sales channels. See - https://docs.medusajs.com/development/publishable-api-keys.\",\n  },\n  {\n    key: \"NEXT_PUBLIC_BASE_URL\",\n    description:\n      \"Your store URL, should be updated to where you are hosting your storefront.\",\n  },\n  {\n    key: \"NEXT_PUBLIC_DEFAULT_REGION\",\n    description:\n      'Your preferred default region. When middleware cannot determine the user region from the \"x-vercel-country\" header, the default region will be used. ISO-2 lowercase format.',\n  },\n  {\n    key: \"NEXT_PUBLIC_STRIPE_KEY\",\n    description:\n      \"Your Stripe public key. See - https://docs.medusajs.com/add-plugins/stripe.\",\n  },\n]\n\nfunction checkEnvVariables() {\n  const missingEnvs = requiredEnvs.filter((env) => {\n    return !process.env[env.key]\n  })\n\n  if (missingEnvs.length > 0) {\n    console.error(\n      c.red.bold(\"\\n🚫 Error: Missing required environment variables\\n\")\n    )\n\n    missingEnvs.forEach((env) => {\n      console.error(c.yellow(`  ${c.bold(env.key)}`))\n      if (env.description) {\n        console.error(c.dim(`    ${env.description}\\n`))\n      }\n    })\n\n    console.error(\n      c.yellow(\n        \"\\nPlease set these variables in your .env file or environment before starting the application.\\n\"\n      )\n    )\n\n    process.exit(1)\n  }\n}\n\nmodule.exports = checkEnvVariables\n"
  },
  {
    "path": "storefront/e2e/README.md",
    "content": "# About\n\nThis folder contains an end to end testing suite written with playwright checking all of the main functionality provided by this template. Note it assumes you are using a postgres database on the backend and have configured a test database. This is required because the tests will **drop and recreate the test database** in order to ensure replicability between test runs.\n\nThis test suite was built off of using the [medusa-starter-default](https://github.com/medusajs/medusa-starter-default) repository with the seed data from `data/seed.json`.\n\n# Setup\n\n## .env\n\nThese tests have a number of dependent environment variables, with an example found in `.env.example`. You can setup your local environment by copying the example environment file\n\n```sh\ncat e2e/.env.example >> .env\n```\n\nand configuring the `.env` file from there. There are more details below about what the test values correspond to and how to set them. But we mention that\n\n* `CLIENT_SERVER` - is the server the next server is listening on\n\n## Playwright\n\nIn order to run these tests, make sure playwright and a playwright-enabled browser is installed. You can do this by running\n\n```sh\nnpx playwright install\n```\n\n## Database\n\nNote that **these tests drop and reset the database** after each test run. This means you will need to configure a separate test database based on your development or production database. We give some instructions for doing so, and enforce a rule which requires the test database to have the prefix `test_` in its name.\n\n### Environment variables\n\n- `TEST_POSTGRES_USER` - user for connecting to the test database, for example, `medusa`\n- `TEST_POSTGRES_PASSWORD` - password for connecting to the test database, for example `my_secret_password`\n- `TEST_POSTGRES_DATABASE` - name of the test database, must start with the prefix `test*`, for example `test_medusa_db`\n- `TEST_POSTGRES_HOST` - optional - host for the postgres database, defaults to `localhost`\n- `TEST_POSTGREST_PORT` - optional - host for the postgres\n- `PRODUCTION_POSTGRES_DATABASE` - name of the production database, for example `medusa_db`\n\nin addition, there are environment variables for connecting to the database as a superuser, so we can efficiently reset the database.\n\n* `PGHOST` - host for the postgres instance\n* `PGPORT` - port for the postgres instance\n* `PGUSER` - superuser for the postgres instance\n* `PGPASSWORD` - superuser password for the postgres instance\n* `PGDATABASE` - database we connect to while updating the other databases\n\n### Test Database Failsafes\n\nThere are a few failsafes to ensure the test and production databases don't get mixed up. This includes:\n\n- Ensuring the production database doesn't have the same name as the test database\n- Ensuring the test database starts with the prefix `test_`\n\nNote running the test suite will trigger database drops and recreations of the test database.\n\n### Using a separate database\n\nIf you need to run your project with a separate database, such as sqlite, MySQL, or something else, please refer to `seed/reset.ts` and implement your own `resetDatabase` function which can be run between test runs.\n\n# Running the test suite\n\n## Test environment\n\nBefore running the test suite, make sure to start the backend server the medusa client is using. In addition, make sure to run in the nextjs template directory\n\n```sh\nyarn build\n```\n\nso the project is built.\n\n## Calling the tests\n\nYou can run the test suite in the base directory of the project with either\n\n```sh\nyarn test-e2e\n```\n\nor\n\n```sh\nnpm run test-e2e\n```\n\nWhile the test suite is running, it is configured to automatically run the nextjs template during test execution.\n"
  },
  {
    "path": "storefront/e2e/data/reset.ts",
    "content": "import { Client } from \"pg\"\n\nasync function getDatabaseClient() {\n  testEnvChecks()\n  const env = getEnv()\n  const client = new Client(env.superuser)\n  await client.connect()\n  return client\n}\n\nfunction getEnv() {\n  return {\n    host: process.env.TEST_POSTGRES_HOST || \"localhost\",\n    port: process.env.TEST_POSTGRES_HOST\n      ? Number(process.env.TEST_POSTGRES_HOST)\n      : 5432,\n    user: process.env.TEST_POSTGRES_USER || \"test_medusa_user\",\n    testDatabase: process.env.TEST_POSTGRES_DATABASE || \"test_medusa_db\",\n    testDatabaseTemplate:\n      process.env.TEST_POSTGRES_DATABASE_TEMPLATE || \"test_medusa_db_template\",\n    productionDatabase: process.env.PRODUCTION_POSTGRES_DATABASE || \"medusa_db\",\n    superuser: {\n      host: process.env.PGHOST || \"localhost\",\n      port: process.env.PGPORT ? Number(process.env.PGPORT) : 5432,\n      user: process.env.PGUSER || \"postgres\",\n      password: process.env.PGPASSWORD || \"password\",\n      database: process.env.PGDATABASE || \"postgres\",\n    },\n  }\n}\n\nasync function testEnvChecks() {\n  const env = getEnv()\n  if (!env.testDatabase.startsWith(\"test_\")) {\n    const msg =\n      \"Please make sure your test environment database name starts with test_\"\n    console.error(msg)\n    throw new Error(msg)\n  }\n  if (env.testDatabase === env.productionDatabase) {\n    const msg =\n      \"Please make sure your test environment database and production environment database names are not equal\"\n    console.error(msg)\n    throw new Error(msg)\n  }\n}\n\nasync function createTemplateDatabase(client: Client) {\n  const { user, testDatabase, testDatabaseTemplate } = getEnv()\n  try {\n    // close current connections\n    await client.query(`\n      ALTER DATABASE ${testDatabase} WITH ALLOW_CONNECTIONS false;\n      SELECT pg_terminate_backend(pid) FROM pg_stat_activity\n        WHERE datname='${testDatabase}';\n    `)\n    await client.query(`\n      CREATE DATABASE ${testDatabaseTemplate} WITH\n        OWNER=${user}\n        TEMPLATE=${testDatabase}\n        IS_TEMPLATE=true;\n    `)\n  } catch (e: any) {\n    // duplicate database code\n    if (e.code === \"42P04\") {\n      return\n    }\n    throw e\n  }\n}\n\nasync function createTestDatabase(client: Client) {\n  const { user, testDatabase, testDatabaseTemplate } = getEnv()\n  const deleteDatabase = `${testDatabase}_del`\n  // drop connections and alter database name\n  await client.query(`\n    SELECT pg_terminate_backend(pid)\n      FROM pg_stat_activity\n      WHERE datname='${testDatabase}';\n    ALTER DATABASE ${testDatabase}\n      RENAME TO ${deleteDatabase};\n  `)\n  await client.query(`\n    CREATE DATABASE ${testDatabase}\n      WITH OWNER ${user}\n      TEMPLATE=${testDatabaseTemplate};\n  `)\n  await client.query(`DROP DATABASE ${deleteDatabase}`)\n}\n\nexport async function resetDatabase() {\n  const client = await getDatabaseClient()\n  await createTemplateDatabase(client)\n  await createTestDatabase(client)\n  await client.end()\n}\n\nexport async function dropTemplate() {\n  const client = await getDatabaseClient()\n  const env = getEnv()\n  await client.query(\n    `ALTER DATABASE ${env.testDatabaseTemplate} is_template false`\n  )\n  await client.query(`DROP DATABASE ${env.testDatabaseTemplate}`)\n  await client.end()\n}\n"
  },
  {
    "path": "storefront/e2e/data/seed.ts",
    "content": "import axios, { AxiosError, AxiosInstance } from \"axios\"\n\naxios.defaults.baseURL = process.env.CLIENT_SERVER || \"http://localhost:9000\"\nlet region = undefined as any\n\nexport async function seedData() {\n  const axios = getOrInitAxios()\n  return {\n    user: await seedUser(),\n  }\n}\n\nexport async function seedUser(email?: string, password?: string) {\n  const user = {\n    first_name: \"Test\",\n    last_name: \"User\",\n    email: email || \"test@example.com\",\n    password: password || \"password\",\n  }\n  try {\n    await axios.post(\"/store/customers\", user)\n    return user\n  } catch (e: unknown) {\n    if (e instanceof AxiosError) {\n      if (e.response && e.response.status) {\n        const status = e.response.status\n        // https://docs.medusajs.com/api/store#customers_postcustomers\n        if (status === 422) {\n          return user\n        }\n      }\n      throw e\n    }\n  }\n}\n\nasync function loadRegion(axios: AxiosInstance) {\n  const resp = await axios.get(\"/admin/regions\")\n  region = resp.data.regions.filter((r: any) => r.currency_code === \"usd\")[0]\n}\n\nasync function getOrInitAxios(axios?: AxiosInstance) {\n  if (!axios) {\n    axios = await loginAdmin()\n  }\n  if (!region) {\n    await loadRegion(axios)\n  }\n  return axios\n}\n\nexport async function seedGiftcard(axios?: AxiosInstance) {\n  axios = await getOrInitAxios(axios)\n  const resp = await axios.post(\"/admin/gift-cards\", {\n    region_id: region.id,\n    value: 10000,\n  })\n  resp.data.gift_card.amount = resp.data.gift_card.value.toString()\n  return resp.data.gift_card as {\n    id: string\n    code: string\n    value: number\n    amount: string\n    balance: string\n  }\n}\n\nexport async function seedDiscount(axios?: AxiosInstance) {\n  axios = await getOrInitAxios(axios)\n  const amount = 2000\n  const resp = await axios.post(\"/admin/discounts\", {\n    code: \"TEST_DISCOUNT_FIXED\",\n    regions: [region.id],\n    rule: {\n      type: \"fixed\",\n      value: amount,\n      allocation: \"total\",\n    },\n  })\n  const discount = resp.data.discount\n  return {\n    id: discount.id,\n    code: discount.code,\n    rule_id: discount.rule_id,\n    amount,\n  }\n}\n\nasync function loginAdmin() {\n  const resp = await axios.post(\"/admin/auth/token\", {\n    email: process.env.MEDUSA_ADMIN_EMAIL || \"admin@medusa-test.com\",\n    password: process.env.MEDUSA_ADMIN_PASSWORD || \"supersecret\",\n  })\n  if (resp.status !== 200) {\n    throw { error: \"must be able to log in user\" }\n  }\n  return axios.create({\n    headers: {\n      Authorization: `Bearer ${resp.data.access_token}`,\n    },\n  })\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/account/account-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { BasePage } from \"../base/base-page\"\n\nexport class AccountPage extends BasePage {\n  container: Locator\n  accountNav: Locator\n\n  overviewLink: Locator\n  profileLink: Locator\n  addressesLink: Locator\n  ordersLink: Locator\n  logoutLink: Locator\n\n  mobileAccountNav: Locator\n  mobileAccountMainLink : Locator\n  mobileOverviewLink : Locator\n  mobileProfileLink : Locator\n  mobileAddressesLink : Locator\n  mobileOrdersLink : Locator\n  mobileLogoutLink : Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.container = page.getByTestId(\"account-page\")\n    this.accountNav = this.container.getByTestId(\"account-nav\")\n    this.overviewLink = this.accountNav.getByTestId(\"overview-link\")\n    this.profileLink = this.accountNav.getByTestId(\"profile-link\")\n    this.addressesLink = this.accountNav.getByTestId(\"addresses-link\")\n    this.ordersLink = this.accountNav.getByTestId(\"orders-link\")\n    this.logoutLink = this.accountNav.getByTestId(\"logout-button\")\n\n    this.mobileAccountNav = this.container.getByTestId(\"mobile-account-nav\")\n    this.mobileAccountMainLink = this.mobileAccountNav.getByTestId(\"account-main-link\")\n    this.mobileOverviewLink = this.mobileAccountNav.getByTestId(\"overview-link\")\n    this.mobileProfileLink = this.mobileAccountNav.getByTestId(\"profile-link\")\n    this.mobileAddressesLink = this.mobileAccountNav.getByTestId(\"addresses-link\")\n    this.mobileOrdersLink = this.mobileAccountNav.getByTestId(\"orders-link\")\n    this.mobileLogoutLink = this.mobileAccountNav.getByTestId(\"logout-button\")\n  }\n\n  async goto() {\n    await this.navMenu.navAccountLink.click()\n    await this.container.waitFor({ state: \"visible\" })\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/account/addresses-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { AccountPage } from \"./account-page\"\nimport { AddressModal } from \"./modals/address-modal\"\n\nexport class AddressesPage extends AccountPage {\n  addAddressModal: AddressModal\n  editAddressModal: AddressModal\n  addressContainer: Locator\n  addressesWrapper: Locator\n  newAddressButton: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.addAddressModal = new AddressModal(page, \"add\")\n    this.editAddressModal = new AddressModal(page, \"edit\")\n    this.addressContainer = this.container.getByTestId(\"address-container\")\n    this.addressesWrapper = page.getByTestId(\"addresses-page-wrapper\")\n    this.newAddressButton = this.container.getByTestId(\"add-address-button\")\n  }\n\n  getAddressContainer(text: string) {\n    const container = this.page\n      .getByTestId(\"address-container\")\n      .filter({ hasText: text })\n    return {\n      container,\n      editButton: container.getByTestId('address-edit-button'),\n      deleteButton: container.getByTestId(\"address-delete-button\"),\n      name: container.getByTestId(\"address-name\"),\n      company: container.getByTestId(\"address-company\"),\n      address: container.getByTestId(\"address-address\"),\n      postalCity: container.getByTestId(\"address-postal-city\"),\n      provinceCountry: container.getByTestId(\"address-province-country\"),\n    }\n  }\n\n  async goto() {\n    await super.goto()\n    await this.addressesLink.click()\n    await this.addressesWrapper.waitFor({ state: \"visible\" })\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/account/index.ts",
    "content": "import { test as base } from \"@playwright/test\"\nimport { AddressesPage } from \"./addresses-page\"\nimport { LoginPage } from \"./login-page\"\nimport { OrderPage } from \"./order-page\"\nimport { OrdersPage } from \"./orders-page\"\nimport { OverviewPage } from \"./overview-page\"\nimport { ProfilePage } from \"./profile-page\"\nimport { RegisterPage } from \"./register-page\"\n\nexport const accountFixtures = base.extend<{\n  accountAddressesPage: AddressesPage\n  accountOrderPage: OrderPage\n  accountOrdersPage: OrdersPage\n  accountOverviewPage: OverviewPage\n  accountProfilePage: ProfilePage\n  loginPage: LoginPage\n  registerPage: RegisterPage\n}>({\n  accountAddressesPage: async ({ page }, use) => {\n    const addressesPage = new AddressesPage(page)\n    await use(addressesPage)\n  },\n  accountOrderPage: async ({ page }, use) => {\n    const orderPage = new OrderPage(page)\n    await use(orderPage)\n  },\n  accountOrdersPage: async ({ page }, use) => {\n    const ordersPage = new OrdersPage(page)\n    await use(ordersPage)\n  },\n  accountOverviewPage: async ({ page }, use) => {\n    const overviewPage = new OverviewPage(page)\n    await use(overviewPage)\n  },\n  accountProfilePage: async ({ page }, use) => {\n    const profilePage = new ProfilePage(page)\n    await use(profilePage)\n  },\n  loginPage: async ({ page }, use) => {\n    const loginPage = new LoginPage(page)\n    await use(loginPage)\n  },\n  registerPage: async ({ page }, use) => {\n    const registerPage = new RegisterPage(page)\n    await use(registerPage)\n  },\n})\n"
  },
  {
    "path": "storefront/e2e/fixtures/account/login-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { BasePage } from \"../base/base-page\"\n\nexport class LoginPage extends BasePage {\n  container: Locator\n  emailInput: Locator\n  passwordInput: Locator\n  signInButton: Locator\n  registerButton: Locator\n  errorMessage: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.container = page.getByTestId(\"login-page\")\n    this.emailInput = this.container.getByTestId(\"email-input\")\n    this.passwordInput = this.container.getByTestId(\"password-input\")\n    this.signInButton = this.container.getByTestId(\"sign-in-button\")\n    this.registerButton = this.container.getByTestId(\"register-button\")\n    this.errorMessage = this.container.getByTestId(\"login-error-message\")\n  }\n\n  async goto() {\n    await this.page.goto(\"/account\")\n    await this.container.waitFor({ state: \"visible\" })\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/account/modals/address-modal.ts",
    "content": "import { Page, Locator } from \"@playwright/test\"\nimport { BaseModal } from \"../../base/base-modal\"\n\nexport class AddressModal extends BaseModal {\n  saveButton: Locator\n  cancelButton: Locator\n\n  firstNameInput: Locator\n  lastNameInput: Locator\n  companyInput: Locator\n  address1Input: Locator\n  address2Input: Locator\n  postalCodeInput: Locator\n  cityInput: Locator\n  stateInput: Locator\n  countrySelect: Locator\n  phoneInput: Locator\n\n  constructor(page: Page, modalType: \"add\" | \"edit\") {\n    if (modalType === \"add\") {\n      super(page, page.getByTestId(\"add-address-modal\"))\n    } else {\n      super(page, page.getByTestId(\"edit-address-modal\"))\n    }\n\n    this.saveButton = this.container.getByTestId(\"save-button\")\n    this.cancelButton = this.container.getByTestId(\"cancel-button\")\n\n    this.firstNameInput = this.container.getByTestId(\"first-name-input\")\n    this.lastNameInput = this.container.getByTestId(\"last-name-input\")\n    this.companyInput = this.container.getByTestId(\"company-input\")\n    this.address1Input = this.container.getByTestId(\"address-1-input\")\n    this.address2Input = this.container.getByTestId(\"address-2-input\")\n    this.postalCodeInput = this.container.getByTestId(\"postal-code-input\")\n    this.cityInput = this.container.getByTestId(\"city-input\")\n    this.stateInput = this.container.getByTestId(\"state-input\")\n    this.countrySelect = this.container.getByTestId(\"country-select\")\n    this.phoneInput = this.container.getByTestId(\"phone-input\")\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/account/order-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { AccountPage } from \"./account-page\"\n\nexport class OrderPage extends AccountPage {\n  container: Locator\n  backToOverviewButton: Locator\n  orderEmail: Locator\n  orderDate: Locator\n  orderId: Locator\n  orderStatus: Locator\n  orderPaymentStatus: Locator\n  shippingAddressSummary: Locator\n  shippingContactSummary: Locator\n  shippingMethodSummary: Locator\n  paymentMethod: Locator\n  paymentAmount: Locator\n  productsTable: Locator\n  productRow: Locator\n  productTitle: Locator\n  productVariant: Locator\n  productQuantity: Locator\n  productOriginalPrice: Locator\n  productPrice: Locator\n  productUnitOriginalPrice: Locator\n  productUnitPrice: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.container = page.getByTestId(\"order-details-container\")\n    this.backToOverviewButton = page.getByTestId(\"back-to-overview-button\")\n    this.orderEmail = this.container.getByTestId(\"order-email\")\n    this.orderDate = this.container.getByTestId(\"order-date\")\n    this.orderId = this.container.getByTestId(\"order-id\")\n    this.orderStatus = this.container.getByTestId(\"order-status\")\n    this.orderPaymentStatus = this.container.getByTestId(\"order-payment-status\")\n    this.shippingAddressSummary = this.container.getByTestId(\n      \"shipping-address-summary\"\n    )\n    this.shippingContactSummary = this.container.getByTestId(\n      \"shipping-contact-summary\"\n    )\n    this.shippingMethodSummary = this.container.getByTestId(\n      \"shipping-method-summary\"\n    )\n    this.paymentMethod = this.container.getByTestId(\"payment-method\")\n    this.paymentAmount = this.container.getByTestId(\"payment-amount\")\n\n    this.productsTable = this.container.getByTestId(\"products-table\")\n    this.productRow = this.container.getByTestId(\"product-row\")\n    this.productTitle = this.container.getByTestId(\"product-title\")\n    this.productVariant = this.container.getByTestId(\"product-variant\")\n    this.productQuantity = this.container.getByTestId(\"product-quantity\")\n    this.productOriginalPrice = this.container.getByTestId(\n      \"product-original-price\"\n    )\n    this.productPrice = this.container.getByTestId(\"product-price\")\n    this.productUnitOriginalPrice = this.container.getByTestId(\n      \"product-unit-original-price\"\n    )\n    this.productUnitPrice = this.container.getByTestId(\"product-unit-price\")\n  }\n\n  async getProduct(title: string, variant: string) {\n    const productRow = this.productRow\n      .filter({\n        hasText: title,\n      })\n      .filter({\n        hasText: `Variant: ${variant}`,\n      })\n    return {\n      productRow,\n      name: productRow.getByTestId(\"product-name\"),\n      variant: productRow.getByTestId(\"product-variant\"),\n      quantity: productRow.getByTestId(\"product-quantity\"),\n      price: productRow.getByTestId(\"product-unit-price\"),\n      total: productRow.getByTestId(\"product-price\"),\n    }\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/account/orders-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { AccountPage } from \"./account-page\"\n\nexport class OrdersPage extends AccountPage {\n  ordersWrapper: Locator\n  noOrdersContainer: Locator\n  continueShoppingButton: Locator\n  orderCard: Locator\n  orderDisplayId: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.ordersWrapper = page.getByTestId(\"orders-page-wrapper\")\n    this.noOrdersContainer = page.getByTestId(\"no-orders-container\")\n    this.continueShoppingButton = page.getByTestId(\"continue-shopping-button\")\n    this.orderCard = page.getByTestId(\"order-card\")\n    this.orderDisplayId = page.getByTestId(\"order-display-id\")\n\n    this.orderCard = page.getByTestId(\"order-card\")\n    this.orderDisplayId = page.getByTestId(\"order-display-id\")\n  }\n\n  async getOrderById(orderId: string) {\n    const orderIdLocator = this.page\n      .getByTestId(\"order-display-id\")\n      .filter({\n        hasText: orderId,\n      })\n      .first()\n    const card = this.orderCard.filter({ has: orderIdLocator }).first()\n    const items = (await card.getByTestId(\"order-item\").all()).map(\n      (orderItem) => {\n        return {\n          item: orderItem,\n          title: orderItem.getByTestId(\"item-title\"),\n          quantity: orderItem.getByTestId(\"item-quantity\"),\n        }\n      }\n    )\n    return {\n      card,\n      displayId: card.getByTestId(\"order-display-id\"),\n      createdAt: card.getByTestId(\"order-created-at\"),\n      orderId: card.getByTestId(\"order-display-id\"),\n      amount: card.getByTestId(\"order-amount\"),\n      detailsLink: card.getByTestId(\"order-details-link\"),\n      itemsLocator: card.getByTestId(\"order-item\"),\n      items,\n    }\n  }\n\n  async goto() {\n    await super.goto()\n    await this.ordersLink.click()\n    await this.ordersWrapper.waitFor({ state: \"visible\" })\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/account/overview-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { AccountPage } from \"./account-page\"\n\nexport class OverviewPage extends AccountPage {\n  welcomeMessage: Locator\n  customerEmail: Locator\n  profileCompletion: Locator\n  addressesCount: Locator\n  noOrdersMessage: Locator\n  ordersWrapper: Locator\n  orderWrapper: Locator\n  overviewWrapper: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.overviewWrapper = this.container.getByTestId(\"overview-page-wrapper\")\n    this.welcomeMessage = this.container.getByTestId(\"welcome-message\")\n    this.customerEmail = this.container.getByTestId(\"customer-email\")\n    this.profileCompletion = this.container.getByTestId(\n      \"customer-profile-completion\"\n    )\n    this.addressesCount = this.container.getByTestId(\"addresses-count\")\n    this.noOrdersMessage = this.container.getByTestId(\"no-orders-message\")\n    this.ordersWrapper = this.container.getByTestId(\"orders-wrapper\")\n    this.orderWrapper = this.container.getByTestId(\"order-wrapper\")\n  }\n\n  async getOrder(orderId: string) {\n    const order = this.ordersWrapper.locator(\n      `[data-testid=\"order-wrapper\"][data-value=\"${orderId}\"]`\n    )\n    return {\n      locator: order,\n      id: await order.getAttribute(\"value\"),\n      createdDate: await order.getByTestId(\"order-created-date\"),\n      displayId: await order.getByTestId(\"order-id\").getAttribute(\"value\"),\n      amount: await order.getByTestId(\"order-amount\").textContent(),\n      openButton: order.getByTestId(\"open-order-button\"),\n    }\n  }\n\n  async goto() {\n    await this.navMenu.navAccountLink.click()\n    await this.container.waitFor({ state: \"visible\" })\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/account/profile-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { AccountPage } from \"./account-page\"\nimport { camelCase } from \"lodash\"\n\nexport class ProfilePage extends AccountPage {\n  profileWrapper: Locator\n  accountNameEditor: Locator\n  accountEmailEditor: Locator\n  accountPhoneEditor: Locator\n  accountPasswordEditor: Locator\n  accountBillingAddressEditor: Locator\n\n  nameEditButton: Locator\n  emailEditButton: Locator\n  phoneEditButton: Locator\n  passwordEditButton: Locator\n  billingAddressEditButton: Locator\n\n  nameSaveButton: Locator\n  emailSaveButton: Locator\n  phoneSaveButton: Locator\n  passwordSaveButton: Locator\n  billingAddressSaveButton: Locator\n\n  savedName: Locator\n  savedEmail: Locator\n  savedPhone: Locator\n  savedPassword: Locator\n  savedBillingAddress: Locator\n\n  nameSuccessMessage: Locator\n  emailSuccessMessage: Locator\n  phoneSuccessMessage: Locator\n  passwordSuccessMessage: Locator\n  billingAddressSuccessMessage: Locator\n\n  nameErrorMessage: Locator\n  emailErrorMessage: Locator\n  phoneErrorMessage: Locator\n  passwordErrorMessage: Locator\n  billingAddressErrorMessage: Locator\n\n  emailInput: Locator\n  firstNameInput: Locator\n  lastNameInput: Locator\n\n  phoneInput: Locator\n\n  oldPasswordInput: Locator\n  newPasswordInput: Locator\n  confirmPasswordInput: Locator\n\n  billingAddress1Input: Locator\n  billingAddress2Input: Locator\n  billingCityInput: Locator\n  billingCompanyInput: Locator\n  billingFirstNameInput: Locator\n  billingLastNameInput: Locator\n  billingPostcalCodeInput: Locator\n  billingProvinceInput: Locator\n  billingCountryCodeSelect: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.profileWrapper = page.getByTestId(\"profile-page-wrapper\")\n    this.accountNameEditor = this.container.getByTestId(\"account-name-editor\")\n    this.accountEmailEditor = this.container.getByTestId(\"account-email-editor\")\n    this.accountPhoneEditor = this.container.getByTestId(\"account-phone-editor\")\n    this.accountPasswordEditor = this.container.getByTestId(\n      \"account-password-editor\"\n    )\n    this.accountBillingAddressEditor = this.container.getByTestId(\n      \"account-billing-address-editor\"\n    )\n\n    this.nameEditButton = this.accountNameEditor.getByTestId(\"edit-button\")\n    this.emailEditButton = this.accountEmailEditor.getByTestId(\"edit-button\")\n    this.phoneEditButton = this.accountPhoneEditor.getByTestId(\"edit-button\")\n    this.passwordEditButton =\n      this.accountPasswordEditor.getByTestId(\"edit-button\")\n    this.billingAddressEditButton =\n      this.accountBillingAddressEditor.getByTestId(\"edit-button\")\n\n    this.nameSaveButton = this.accountNameEditor.getByTestId(\"save-button\")\n    this.emailSaveButton = this.accountEmailEditor.getByTestId(\"save-button\")\n    this.phoneSaveButton = this.accountPhoneEditor.getByTestId(\"save-button\")\n    this.passwordSaveButton =\n      this.accountPasswordEditor.getByTestId(\"save-button\")\n    this.billingAddressSaveButton =\n      this.accountBillingAddressEditor.getByTestId(\"save-button\")\n\n    this.savedName = this.accountNameEditor.getByTestId(\"current-info\")\n    this.savedEmail = this.accountEmailEditor.getByTestId(\"current-info\")\n    this.savedPhone = this.accountPhoneEditor.getByTestId(\"current-info\")\n    this.savedPassword = this.accountPasswordEditor.getByTestId(\"current-info\")\n    this.savedBillingAddress =\n      this.accountBillingAddressEditor.getByTestId(\"current-info\")\n    this.nameSuccessMessage =\n      this.accountNameEditor.getByTestId(\"success-message\")\n    this.emailSuccessMessage =\n      this.accountEmailEditor.getByTestId(\"success-message\")\n    this.phoneSuccessMessage =\n      this.accountPhoneEditor.getByTestId(\"success-message\")\n    this.passwordSuccessMessage =\n      this.accountPasswordEditor.getByTestId(\"success-message\")\n    this.billingAddressSuccessMessage =\n      this.accountBillingAddressEditor.getByTestId(\"success-message\")\n    this.nameErrorMessage = this.accountNameEditor.getByTestId(\"error-message\")\n    this.emailErrorMessage =\n      this.accountEmailEditor.getByTestId(\"error-message\")\n    this.phoneErrorMessage =\n      this.accountPhoneEditor.getByTestId(\"error-message\")\n    this.passwordErrorMessage =\n      this.accountPasswordEditor.getByTestId(\"error-message\")\n    this.billingAddressErrorMessage =\n      this.accountBillingAddressEditor.getByTestId(\"error-message\")\n\n    this.firstNameInput = page.getByTestId(\"first-name-input\")\n    this.lastNameInput = page.getByTestId(\"last-name-input\")\n    this.emailInput = page.getByTestId(\"email-input\")\n    this.phoneInput = page.getByTestId(\"phone-input\")\n    this.oldPasswordInput = page.getByTestId(\"old-password-input\")\n    this.newPasswordInput = page.getByTestId(\"new-password-input\")\n    this.confirmPasswordInput = page.getByTestId(\"confirm-password-input\")\n\n    this.billingAddress1Input = page.getByTestId(\"billing-address-1-input\")\n    this.billingAddress2Input = page.getByTestId(\"billing-address-2-input\")\n    this.billingCityInput = page.getByTestId(\"billing-city-input\")\n    this.billingCompanyInput = page.getByTestId(\"billing-company-input\")\n    this.billingFirstNameInput = page.getByTestId(\"billing-first-name-input\")\n    this.billingLastNameInput = page.getByTestId(\"billing-last-name-input\")\n    this.billingPostcalCodeInput = page.getByTestId(\n      \"billing-postcal-code-input\"\n    )\n    this.billingProvinceInput = page.getByTestId(\"billing-province-input\")\n    this.billingCountryCodeSelect = page.getByTestId(\n      \"billing-country-code-select\"\n    )\n  }\n\n  async getEditorInputs(editor: Locator) {\n    const editButton = editor.getByTestId(\"edit-button\")\n    if ((await editButton.getAttribute(\"active\")) !== \"true\") {\n      await editButton.click()\n    }\n    // get all the inputs\n    const inputs = editor.locator(\n      '[data-testid]:not([data-testid=\"edit-button\"])'\n    )\n    const o = {\n      editButton,\n    } as { [k: string]: Locator }\n    for (const input of await inputs.all()) {\n      const testId = (await input.getAttribute(\"data-testid\")) as string\n      const key = camelCase(testId)\n      o[key] = input\n    }\n    return o\n  }\n\n  async goto() {\n    super.goto()\n    await this.profileLink.click()\n    await this.profileWrapper.waitFor({ state: \"visible\" })\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/account/register-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { BasePage } from \"../base/base-page\"\n\nexport class RegisterPage extends BasePage {\n  container: Locator\n  firstNameInput: Locator\n  lastNameInput: Locator\n  emailInput: Locator\n  phoneInput: Locator\n  passwordInput: Locator\n  registerButton: Locator\n  registerError: Locator\n  loginLink: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.container = page.getByTestId(\"register-page\")\n    this.firstNameInput = this.container.getByTestId(\"first-name-input\")\n    this.lastNameInput = this.container.getByTestId(\"last-name-input\")\n    this.emailInput = this.container.getByTestId(\"email-input\")\n    this.phoneInput = this.container.getByTestId(\"phone-input\")\n    this.passwordInput = this.container.getByTestId(\"password-input\")\n    this.registerButton = this.container.getByTestId(\"register-button\")\n    this.registerError = this.container.getByTestId(\"register-error\")\n    this.loginLink = this.container.getByTestId(\"login-link\")\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/base/base-modal.ts",
    "content": "import { Page, Locator } from \"@playwright/test\"\n\nexport class BaseModal {\n  page: Page\n  container: Locator\n  closeButton: Locator\n\n  constructor(page: Page, container: Locator) {\n    this.page = page\n    this.container = container\n    this.closeButton = this.container.getByTestId(\"close-modal-button\")\n  }\n\n  async close() {\n    const button = this.container.getByTestId(\"close-modal-button\")\n    await button.click()\n  }\n\n  async isOpen() {\n    return await this.container.isVisible()\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/base/base-page.ts",
    "content": "import { CartDropdown } from \"./cart-dropdown\"\nimport { NavMenu } from \"./nav-menu\"\nimport { Page, Locator } from \"@playwright/test\"\nimport { SearchModal } from \"./search-modal\"\n\nexport class BasePage {\n  page: Page\n  navMenu: NavMenu\n  cartDropdown: CartDropdown\n  searchModal: SearchModal\n  accountLink: Locator\n  cartLink: Locator\n  searchLink: Locator\n  storeLink: Locator\n  categoriesList: Locator\n\n  constructor(page: Page) {\n    this.page = page\n    this.navMenu = new NavMenu(page)\n    this.cartDropdown = new CartDropdown(page)\n    this.searchModal = new SearchModal(page)\n    this.accountLink = page.getByTestId(\"nav-account-link\")\n    this.cartLink = page.getByTestId(\"nav-cart-link\")\n    this.storeLink = page.getByTestId(\"nav-store-link\")\n    this.searchLink = page.getByTestId(\"nav-search-link\")\n    this.categoriesList = page.getByTestId(\"footer-categories\")\n  }\n\n  async clickCategoryLink(category: string) {\n    const link = this.categoriesList.getByTestId(\"category-link\")\n    await link.click()\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/base/cart-dropdown.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\n\nexport class CartDropdown {\n  page: Page\n  navCartLink: Locator\n  cartDropdown: Locator\n  cartSubtotal: Locator\n  goToCartButton: Locator\n\n  constructor(page: Page) {\n    this.page = page\n    this.navCartLink = page.getByTestId(\"nav-cart-link\")\n    this.cartDropdown = page.getByTestId(\"nav-cart-dropdown\")\n    this.cartSubtotal = this.cartDropdown.getByTestId(\"cart-subtotal\")\n    this.goToCartButton = this.cartDropdown.getByTestId(\"go-to-cart-button\")\n  }\n\n  async displayCart() {\n    await this.navCartLink.hover()\n  }\n\n  async close() {\n    if (await this.cartDropdown.isVisible()) {\n      const box = await this.cartDropdown.boundingBox()\n      if (!box) {\n        return\n      }\n      await this.page.mouse.move(box.x + box.width / 4, box.y + box.height / 4)\n      await this.page.mouse.move(5, 10)\n    }\n  }\n\n  async getCartItem(name: string, variant: string) {\n    const cartItem = this.cartDropdown\n      .getByTestId(\"cart-item\")\n      .filter({\n        hasText: name,\n      })\n      .filter({\n        hasText: `Variant: ${variant}`,\n      })\n    return {\n      locator: cartItem,\n      productLink: cartItem.getByTestId(\"product-link\"),\n      removeButton: cartItem.getByTestId(\"cart-item-remove-button\"),\n      name,\n      quantity: cartItem.getByTestId(\"cart-item-quantity\"),\n      variant: cartItem.getByTestId(\"cart-item-variant\"),\n    }\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/base/nav-menu.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\n\nexport class NavMenu {\n  page: Page\n  navMenuButton: Locator\n  navMenu: Locator\n  navAccountLink: Locator\n  homeLink: Locator\n  storeLink: Locator\n  searchLink: Locator\n  accountLink: Locator\n  cartLink: Locator\n  closeButton: Locator\n  shippingToLink: Locator\n  shippingToMenu: Locator\n\n  constructor(page: Page) {\n    this.page = page\n    this.navMenuButton = page.getByTestId(\"nav-menu-button\")\n    this.navMenu = page.getByTestId(\"nav-menu-popup\")\n    this.navAccountLink = page.getByTestId(\"nav-account-link\")\n    this.homeLink = this.navMenu.getByTestId(\"home-link\")\n    this.storeLink = this.navMenu.getByTestId(\"store-link\")\n    this.searchLink = this.navMenu.getByTestId(\"search-link\")\n    this.accountLink = this.navMenu.getByTestId(\"account-link\")\n    this.cartLink = this.navMenu.getByTestId(\"nav-cart-link\")\n    this.closeButton = this.navMenu.getByTestId(\"close-menu-button\")\n    this.shippingToLink = this.navMenu.getByTestId(\"shipping-to-button\")\n    this.shippingToMenu = this.navMenu.getByTestId(\"shipping-to-choices\")\n  }\n\n  async selectShippingCountry(country: string) {\n    if (!(await this.navMenu.isVisible())) {\n      throw {\n        error:\n          `You cannot call ` +\n          `NavMenu.selectShippingCountry(\"${country}\") without having the ` +\n          `navMenu visible first!`,\n      }\n    }\n    const countryLink = this.navMenu.getByTestId(\n      `select-${country.toLowerCase()}-choice`\n    )\n    await this.shippingToLink.hover()\n    await this.shippingToMenu.waitFor({\n      state: \"visible\",\n    })\n    await countryLink.click()\n  }\n\n  async open() {\n    await this.navMenuButton.click()\n    await this.navMenu.waitFor({ state: \"visible\" })\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/base/search-modal.ts",
    "content": "import { Page, Locator } from \"@playwright/test\"\nimport { BaseModal } from \"./base-modal\"\nimport { NavMenu } from \"./nav-menu\"\n\nexport class SearchModal extends BaseModal {\n  searchInput: Locator\n  searchResults: Locator\n  noSearchResultsContainer: Locator\n  searchResult: Locator\n  searchResultTitle: Locator\n\n  constructor(page: Page) {\n    super(page, page.getByTestId(\"search-modal-container\"))\n    this.searchInput = this.container.getByTestId(\"search-input\")\n    this.searchResults = this.container.getByTestId(\"search-results\")\n    this.noSearchResultsContainer = this.container.getByTestId(\n      \"no-search-results-container\"\n    )\n    this.searchResult = this.container.getByTestId(\"search-result\")\n    this.searchResultTitle = this.container.getByTestId(\"search-result-title\")\n  }\n\n  async open() {\n    const menu = new NavMenu(this.page)\n    await menu.open()\n    await menu.searchLink.click()\n    await this.container.waitFor({ state: \"visible\" })\n  }\n\n  async close() {\n    const viewport = this.page.viewportSize()\n    const y = viewport ? viewport.height / 2 : 100\n    await this.page.mouse.click(1, y, { clickCount: 2, delay: 100 })\n    await this.container.waitFor({ state: \"hidden\" })\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/cart-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { BasePage } from \"./base/base-page\"\n\nexport class CartPage extends BasePage {\n  container: Locator\n  emptyCartMessage: Locator\n  signInButton: Locator\n  productRow: Locator\n  productTitle: Locator\n  productVariant: Locator\n  productDeleteButton: Locator\n  productQuantitySelect: Locator\n  discountButton: Locator\n  discountInput: Locator\n  discountApplyButton: Locator\n  discountErrorMessage: Locator\n  discountRow: Locator\n  giftCardRow: Locator\n  giftCardCode: Locator\n  giftCardAmount: Locator\n  giftCardRemoveButton: Locator\n  cartSubtotal: Locator\n  cartDiscount: Locator\n  cartGiftCardAmount: Locator\n  cartShipping: Locator\n  cartTaxes: Locator\n  cartTotal: Locator\n  checkoutButton: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.container = page.getByTestId(\"cart-container\")\n    this.emptyCartMessage = this.container.getByTestId(\"empty-cart-message\")\n    this.signInButton = this.container.getByTestId(\"sign-in-button\")\n    this.productRow = this.container.getByTestId(\"product-row\")\n    this.productTitle = this.container.getByTestId(\"product-title\")\n    this.productVariant = this.container.getByTestId(\"product-variant\")\n    this.productDeleteButton = this.container.getByTestId(\n      \"product-delete-button\"\n    )\n    this.productQuantitySelect = this.container.getByTestId(\n      \"product-quantity-select\"\n    )\n    this.checkoutButton = this.container.getByTestId(\"checkout-button\")\n    this.discountButton = this.container.getByTestId(\"add-discount-button\")\n    this.discountInput = this.container.getByTestId(\"discount-input\")\n    this.discountApplyButton = this.container.getByTestId(\n      \"discount-apply-button\"\n    )\n    this.discountErrorMessage = this.container.getByTestId(\n      \"discount-error-message\"\n    )\n    this.discountRow = this.container.getByTestId(\"discount-row\")\n    this.giftCardRow = this.container.getByTestId(\"gift-card\")\n    this.giftCardCode = this.container.getByTestId(\"gift-card-code\")\n    this.giftCardAmount = this.container.getByTestId(\"gift-card-amount\")\n    this.giftCardRemoveButton = this.container.getByTestId(\n      \"remove-gift-card-button\"\n    )\n    this.cartSubtotal = this.container.getByTestId(\"cart-subtotal\")\n    this.cartDiscount = this.container.getByTestId(\"cart-discount\")\n    this.cartGiftCardAmount = this.container.getByTestId(\n      \"cart-gift-card-amount\"\n    )\n    this.cartShipping = this.container.getByTestId(\"cart-shipping\")\n    this.cartTaxes = this.container.getByTestId(\"cart-taxes\")\n    this.cartTotal = this.container.getByTestId(\"cart-total\")\n  }\n\n  async getProduct(title: string, variant: string) {\n    const productRow = this.productRow\n      .filter({\n        hasText: title,\n      })\n      .filter({\n        hasText: `Variant: ${variant}`,\n      })\n    return {\n      productRow,\n      title: productRow.getByTestId(\"product-title\"),\n      variant: productRow.getByTestId(\"product-variant\"),\n      deleteButton: productRow.getByTestId(\"delete-button\"),\n      quantitySelect: productRow.getByTestId(\"product-select-button\"),\n      price: productRow.getByTestId(\"product-unit-price\"),\n      total: productRow.getByTestId(\"product-price\"),\n    }\n  }\n\n  async getGiftCard(code: string) {\n    const giftCardRow = this.giftCardRow.filter({\n      hasText: code,\n    })\n    const amount = giftCardRow.getByTestId(\"gift-card-amount\")\n    return {\n      locator: giftCardRow,\n      code: giftCardRow.getByTestId(\"gift-card-code\"),\n      amount,\n      amountValue: await amount.getAttribute(\"data-value\"),\n      removeButton: giftCardRow.getByTestId(\"remove-gift-card-button\"),\n    }\n  }\n\n  async getDiscount(code: string) {\n    const discount = this.discountRow\n    const amount = discount.getByTestId(\"discount-amount\")\n    return {\n      locator: discount,\n      code: discount.getByTestId(\"discount-code\"),\n      amount,\n      amountValue: await amount.getAttribute(\"data-value\"),\n      removeButton: discount.getByTestId(\"remove-discount-button\"),\n    }\n  }\n\n  async goto() {\n    await this.cartLink.click({ clickCount: 2 })\n    await this.container.waitFor({ state: \"visible\" })\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/category-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { BasePage } from \"./base/base-page\"\n\nexport class CategoryPage extends BasePage {\n  container: Locator\n  sortByContainer: Locator\n\n  pageTitle: Locator\n  pagination: Locator\n  productsListLoader: Locator\n  productsList: Locator\n  productWrapper: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.container = page.getByTestId(\"category-container\")\n    this.pageTitle = page.getByTestId(\"category-page-title\")\n    this.sortByContainer = page.getByTestId(\"sort-by-container\")\n    this.productsListLoader = this.container.getByTestId(\"products-list-loader\")\n    this.productsList = this.container.getByTestId(\"products-list\")\n    this.productWrapper = this.productsList.getByTestId(\"product-wrapper\")\n    this.pagination = this.container.getByTestId(\"product-pagination\")\n  }\n\n  async getProduct(name: string) {\n    const product = this.productWrapper.filter({ hasText: name })\n    return {\n      locator: product,\n      title: product.getByTestId(\"product-title\"),\n      price: product.getByTestId(\"price\"),\n      originalPrice: product.getByTestId(\"original-price\"),\n    }\n  }\n\n  async sortBy(sortString: string) {\n    const link = this.sortByContainer.getByTestId(\"sort-by-link\").filter({\n      hasText: sortString,\n    })\n    await link.click()\n    // wait for page change\n    await this.page.waitForFunction((linkElement) => {\n      if (!linkElement) {\n        return true\n      }\n      return linkElement.dataset.active === \"true\"\n    }, await link.elementHandle())\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/checkout-page.ts",
    "content": "import { ElementHandle, Locator, Page } from \"@playwright/test\"\nimport { BasePage } from \"./base/base-page\"\n\nexport class CheckoutPage extends BasePage {\n  backToCartLink: Locator\n  storeLink: Locator\n  container: Locator\n  editAddressButton: Locator\n  editDeliveryButton: Locator\n  editPaymentButton: Locator\n\n  shippingAddressSelect: Locator\n  shippingAddressOptions: Locator\n  shippingAddressOption: Locator\n\n  billingAddressCheckbox: Locator\n  billingAddressInput: Locator\n  billingCityInput: Locator\n  billingCompanyInput: Locator\n  billingFirstNameInput: Locator\n  billingLastNameInput: Locator\n  billingPhoneInput: Locator\n  billingPostalInput: Locator\n  billingProvinceInput: Locator\n  shippingAddressInput: Locator\n  shippingCityInput: Locator\n  shippingCompanyInput: Locator\n  shippingEmailInput: Locator\n  shippingFirstNameInput: Locator\n  shippingLastNameInput: Locator\n  shippingPhoneInput: Locator\n  shippingPostalCodeInput: Locator\n  shippingProvinceInput: Locator\n\n  billingCountrySelect: Locator\n  shippingCountrySelect: Locator\n\n  shippingAddressSummary: Locator\n  shippingContactSummary: Locator\n  billingAddressSummary: Locator\n\n  submitAddressButton: Locator\n  addressErrorMessage: Locator\n\n  deliveryOptionsContainer: Locator\n  deliveryOptionRadio: Locator\n  deliveryOptionErrorMessage: Locator\n  submitDeliveryOptionButton: Locator\n  deliveryOptionSummary: Locator\n\n  paymentMethodSummary: Locator\n  paymentDetailsSummary: Locator\n  paymentMethodErrorMessage: Locator\n  stripePaymentErrorMessage: Locator\n  paypalPaymentErrorMessage: Locator\n  manualPaymentErrorMessage: Locator\n  submitPaymentButton: Locator\n  submitOrderButton: Locator\n\n  discountButton: Locator\n  discountInput: Locator\n  discountApplyButton: Locator\n  discountErrorMessage: Locator\n  discountRow: Locator\n  giftCardRow: Locator\n  giftCardCode: Locator\n  giftCardAmount: Locator\n  giftCardRemoveButton: Locator\n  cartSubtotal: Locator\n  cartDiscount: Locator\n  cartGiftCardAmount: Locator\n  cartShipping: Locator\n  cartTaxes: Locator\n  cartTotal: Locator\n  itemsTable: Locator\n  itemRow: Locator\n  itemTitle: Locator\n  itemVariant: Locator\n  itemQuantity: Locator\n  itemOriginalPrice: Locator\n  itemReducedPrice: Locator\n  itemUnitOriginalPrice: Locator\n  itemUnitReducedPrice: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.backToCartLink = page.getByTestId(\"back-to-cart-link\")\n    this.storeLink = page.getByTestId(\"store-link\")\n    this.container = page.getByTestId(\"checkout-container\")\n\n    this.editAddressButton = this.container.getByTestId(\"edit-address-button\")\n    this.editDeliveryButton = this.container.getByTestId(\"edit-delivery-button\")\n    this.editPaymentButton = this.container.getByTestId(\"edit-payment-button\")\n\n    this.shippingAddressSelect = this.container.getByTestId(\n      \"shipping-address-select\"\n    )\n    this.shippingAddressOptions = this.container.getByTestId(\n      \"shipping-address-options\"\n    )\n    this.shippingAddressOption = this.container.getByTestId(\n      \"shipping-address-option\"\n    )\n    this.billingAddressCheckbox = this.container.getByTestId(\n      \"billing-address-checkbox\"\n    )\n    this.billingAddressInput = this.container.getByTestId(\n      \"billing-address-input\"\n    )\n    this.billingCityInput = this.container.getByTestId(\"billing-city-input\")\n    this.billingCompanyInput = this.container.getByTestId(\n      \"billing-company-input\"\n    )\n    this.billingFirstNameInput = this.container.getByTestId(\n      \"billing-first-name-input\"\n    )\n    this.billingLastNameInput = this.container.getByTestId(\n      \"billing-last-name-input\"\n    )\n    this.billingPhoneInput = this.container.getByTestId(\"billing-phone-input\")\n    this.billingPostalInput = this.container.getByTestId(\"billing-postal-input\")\n    this.billingProvinceInput = this.container.getByTestId(\n      \"billing-province-input\"\n    )\n    this.shippingAddressInput = this.container.getByTestId(\n      \"shipping-address-input\"\n    )\n    this.shippingCityInput = this.container.getByTestId(\"shipping-city-input\")\n    this.shippingCompanyInput = this.container.getByTestId(\n      \"shipping-company-input\"\n    )\n    this.shippingEmailInput = this.container.getByTestId(\"shipping-email-input\")\n    this.shippingFirstNameInput = this.container.getByTestId(\n      \"shipping-first-name-input\"\n    )\n    this.shippingLastNameInput = this.container.getByTestId(\n      \"shipping-last-name-input\"\n    )\n    this.shippingPhoneInput = this.container.getByTestId(\"shipping-phone-input\")\n    this.shippingPostalCodeInput = this.container.getByTestId(\n      \"shipping-postal-code-input\"\n    )\n    this.shippingProvinceInput = this.container.getByTestId(\n      \"shipping-province-input\"\n    )\n\n    this.billingCountrySelect = this.container.getByTestId(\n      \"billing-country-select\"\n    )\n    this.shippingCountrySelect = this.container.getByTestId(\n      \"shipping-country-select\"\n    )\n\n    this.shippingAddressSummary = this.container.getByTestId(\n      \"shipping-address-summary\"\n    )\n    this.shippingContactSummary = this.container.getByTestId(\n      \"shipping-contact-summary\"\n    )\n    this.billingAddressSummary = this.container.getByTestId(\n      \"billing-address-summary\"\n    )\n\n    this.submitAddressButton = this.container.getByTestId(\n      \"submit-address-button\"\n    )\n    this.addressErrorMessage = this.container.getByTestId(\n      \"address-error-message\"\n    )\n\n    this.deliveryOptionsContainer = this.container.getByTestId(\n      \"delivery-options-container\"\n    )\n    this.deliveryOptionRadio = this.container.getByTestId(\n      \"delivery-option-radio\"\n    )\n    this.deliveryOptionErrorMessage = this.container.getByTestId(\n      \"delivery-option-error-message\"\n    )\n    this.submitDeliveryOptionButton = this.container.getByTestId(\n      \"submit-delivery-option-button\"\n    )\n    this.deliveryOptionSummary = this.container.getByTestId(\n      \"delivery-option-summary\"\n    )\n\n    this.paymentMethodSummary = this.container.getByTestId(\n      \"payment-method-summary\"\n    )\n    this.paymentDetailsSummary = this.container.getByTestId(\n      \"payment-details-summary\"\n    )\n    this.paymentMethodErrorMessage = this.container.getByTestId(\n      \"payment-method-error-message\"\n    )\n    this.submitPaymentButton = this.container.getByTestId(\n      \"submit-payment-button\"\n    )\n    this.stripePaymentErrorMessage = this.container.getByTestId(\n      \"stripe-payment-error-message\"\n    )\n    this.paypalPaymentErrorMessage = this.container.getByTestId(\n      \"paypal-payment-error-message\"\n    )\n    this.manualPaymentErrorMessage = this.container.getByTestId(\n      \"manual-payment-error-message\"\n    )\n    this.submitOrderButton = this.container.getByTestId(\"submit-order-button\")\n\n    this.discountButton = this.container.getByTestId(\"add-discount-button\")\n    this.discountInput = this.container.getByTestId(\"discount-input\")\n    this.discountApplyButton = this.container.getByTestId(\n      \"discount-apply-button\"\n    )\n    this.discountErrorMessage = this.container.getByTestId(\n      \"discount-error-message\"\n    )\n    this.discountRow = this.container.getByTestId(\"discount-row\")\n    this.giftCardRow = this.container.getByTestId(\"gift-card\")\n    this.giftCardCode = this.container.getByTestId(\"gift-card-code\")\n    this.giftCardAmount = this.container.getByTestId(\"gift-card-amount\")\n    this.giftCardRemoveButton = this.container.getByTestId(\n      \"remove-gift-card-button\"\n    )\n    this.cartSubtotal = this.container.getByTestId(\"cart-subtotal\")\n    this.cartDiscount = this.container.getByTestId(\"cart-discount\")\n    this.cartGiftCardAmount = this.container.getByTestId(\n      \"cart-gift-card-amount\"\n    )\n    this.cartShipping = this.container.getByTestId(\"cart-shipping\")\n    this.cartTaxes = this.container.getByTestId(\"cart-taxes\")\n    this.cartTotal = this.container.getByTestId(\"cart-total\")\n    this.itemsTable = this.container.getByTestId(\"items-table\")\n    this.itemRow = this.container.getByTestId(\"item-row\")\n    this.itemTitle = this.container.getByTestId(\"item-title\")\n    this.itemVariant = this.container.getByTestId(\"item-variant\")\n    this.itemQuantity = this.container.getByTestId(\"item-quantity\")\n    this.itemOriginalPrice = this.container.getByTestId(\"item-original-price\")\n    this.itemReducedPrice = this.container.getByTestId(\"item-reduced-price\")\n    this.itemUnitOriginalPrice = this.container.getByTestId(\n      \"item-unit-original-price\"\n    )\n    this.itemUnitReducedPrice = this.container.getByTestId(\n      \"item-unit-reduced-price\"\n    )\n  }\n\n  async selectSavedAddress(address: string) {\n    await this.shippingAddressSelect.click()\n    const addressOption = this.shippingAddressOption.filter({\n      hasText: address,\n    })\n    await addressOption.getByTestId(\"shipping-address-radio\").click()\n\n    const selectHandle = await this.shippingAddressSelect.elementHandle()\n    await this.page.waitForFunction(\n      (opts) => {\n        const select = opts[0]\n        const choice = opts[1]\n        return (select.textContent || \"\").includes(choice)\n      },\n      [selectHandle, address] as [ElementHandle, string]\n    )\n  }\n\n  async selectDeliveryOption(option: string) {\n    await this.deliveryOptionRadio.filter({ hasText: option }).click()\n  }\n\n  async getGiftCard(code: string) {\n    const giftCardRow = this.giftCardRow.filter({\n      hasText: code,\n    })\n    const amount = giftCardRow.getByTestId(\"gift-card-amount\")\n    return {\n      locator: giftCardRow,\n      code: giftCardRow.getByTestId(\"gift-card-code\"),\n      amount,\n      amountValue: await amount.getAttribute(\"data-value\"),\n      removeButton: giftCardRow.getByTestId(\"remove-gift-card-button\"),\n    }\n  }\n\n  async getDiscount(code: string) {\n    const discount = this.discountRow\n    const amount = discount.getByTestId(\"discount-amount\")\n    return {\n      locator: discount,\n      code: discount.getByTestId(\"discount-code\"),\n      amount,\n      amountValue: await amount.getAttribute(\"data-value\"),\n      removeButton: discount.getByTestId(\"remove-discount-button\"),\n    }\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/index.ts",
    "content": "import { test as base, Page } from \"@playwright/test\"\nimport { resetDatabase } from \"../data/reset\"\nimport { CartPage } from \"./cart-page\"\nimport { CategoryPage } from \"./category-page\"\nimport { CheckoutPage } from \"./checkout-page\"\nimport { OrderPage } from \"./order-page\"\nimport { ProductPage } from \"./product-page\"\nimport { StorePage } from \"./store-page\"\n\nexport const fixtures = base.extend<{\n  resetDatabaseFixture: void\n  cartPage: CartPage\n  categoryPage: CategoryPage\n  checkoutPage: CheckoutPage\n  orderPage: OrderPage\n  productPage: ProductPage\n  storePage: StorePage\n}>({\n  page: async ({ page }, use) => {\n    await page.goto(\"/\")\n    use(page)\n  },\n  resetDatabaseFixture: [\n    async function ({}, use) {\n      await resetDatabase()\n      await use()\n    },\n    { auto: true, timeout: 10000 },\n  ],\n  cartPage: async ({ page }, use) => {\n    const cartPage = new CartPage(page)\n    await use(cartPage)\n  },\n  categoryPage: async ({ page }, use) => {\n    const categoryPage = new CategoryPage(page)\n    await use(categoryPage)\n  },\n  checkoutPage: async ({ page }, use) => {\n    const checkoutPage = new CheckoutPage(page)\n    await use(checkoutPage)\n  },\n  orderPage: async ({ page }, use) => {\n    const orderPage = new OrderPage(page)\n    await use(orderPage)\n  },\n  productPage: async ({ page }, use) => {\n    const productPage = new ProductPage(page)\n    await use(productPage)\n  },\n  storePage: async ({ page }, use) => {\n    const storePage = new StorePage(page)\n    await use(storePage)\n  },\n})\n"
  },
  {
    "path": "storefront/e2e/fixtures/modals/mobile-actions-modal.ts",
    "content": "import { Page, Locator } from \"@playwright/test\"\nimport { BaseModal } from \"../base/base-modal\"\n\nexport class MobileActionsModal extends BaseModal {\n  optionButton: Locator\n\n  constructor(page: Page) {\n    super(page, page.getByTestId(\"mobile-actions-modal\"))\n    this.optionButton = this.container.getByTestId(\"option-button\")\n  }\n\n  getOption(option: string) {\n    return this.optionButton.filter({\n      hasText: option,\n    })\n  }\n\n  async selectOption(option: string) {\n    const optionButton = this.getOption(option)\n    await optionButton.click()\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/order-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { BasePage } from \"./base/base-page\"\n\nexport class OrderPage extends BasePage {\n  container: Locator\n  cartSubtotal: Locator\n  cartDiscount: Locator\n  cartGiftCardAmount: Locator\n  cartShipping: Locator\n  cartTaxes: Locator\n  cartTotal: Locator\n  orderEmail: Locator\n  orderDate: Locator\n  orderId: Locator\n  orderStatus: Locator\n  orderPaymentStatus: Locator\n  shippingAddressSummary: Locator\n  shippingContactSummary: Locator\n  shippingMethodSummary: Locator\n  paymentMethod: Locator\n  paymentAmount: Locator\n  productsTable: Locator\n  productRow: Locator\n  productTitle: Locator\n  productVariant: Locator\n  productQuantity: Locator\n  productOriginalPrice: Locator\n  productPrice: Locator\n  productUnitOriginalPrice: Locator\n  productUnitPrice: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.container = page.getByTestId(\"order-complete-container\")\n    this.orderEmail = this.container.getByTestId(\"order-email\")\n    this.orderDate = this.container.getByTestId(\"order-date\")\n    this.orderId = this.container.getByTestId(\"order-id\")\n    this.orderStatus = this.container.getByTestId(\"order-status\")\n    this.cartSubtotal = this.container.getByTestId(\"cart-subtotal\")\n    this.cartDiscount = this.container.getByTestId(\"cart-discount\")\n    this.cartGiftCardAmount = this.container.getByTestId(\n      \"cart-gift-card-amount\"\n    )\n    this.cartShipping = this.container.getByTestId(\"cart-shipping\")\n    this.cartTaxes = this.container.getByTestId(\"cart-taxes\")\n    this.cartTotal = this.container.getByTestId(\"cart-total\")\n    this.orderPaymentStatus = this.container.getByTestId(\"order-payment-status\")\n    this.shippingAddressSummary = this.container.getByTestId(\n      \"shipping-address-summary\"\n    )\n    this.shippingContactSummary = this.container.getByTestId(\n      \"shipping-contact-summary\"\n    )\n    this.shippingMethodSummary = this.container.getByTestId(\n      \"shipping-method-summary\"\n    )\n    this.paymentMethod = this.container.getByTestId(\"payment-method\")\n    this.paymentAmount = this.container.getByTestId(\"payment-amount\")\n\n    this.productsTable = this.container.getByTestId(\"products-table\")\n    this.productRow = this.container.getByTestId(\"product-row\")\n    this.productTitle = this.container.getByTestId(\"product-title\")\n    this.productVariant = this.container.getByTestId(\"product-variant\")\n    this.productQuantity = this.container.getByTestId(\"product-quantity\")\n    this.productOriginalPrice = this.container.getByTestId(\n      \"product-original-price\"\n    )\n    this.productPrice = this.container.getByTestId(\"product-price\")\n    this.productUnitOriginalPrice = this.container.getByTestId(\n      \"product-unit-original-price\"\n    )\n    this.productUnitPrice = this.container.getByTestId(\"product-unit-price\")\n  }\n\n  async getProduct(title: string, variant: string) {\n    const productRow = this.productRow\n      .filter({\n        hasText: title,\n      })\n      .filter({\n        hasText: `Variant: ${variant}`,\n      })\n    return {\n      productRow,\n      name: productRow.getByTestId(\"product-name\"),\n      variant: productRow.getByTestId(\"product-variant\"),\n      quantity: productRow.getByTestId(\"product-quantity\"),\n      price: productRow.getByTestId(\"product-unit-price\"),\n      total: productRow.getByTestId(\"product-price\"),\n    }\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/product-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { BasePage } from \"./base/base-page\"\nimport { MobileActionsModal } from \"./modals/mobile-actions-modal\"\n\nexport class ProductPage extends BasePage {\n  mobileActionsModal: MobileActionsModal\n\n  container: Locator\n  productTitle: Locator\n  productDescription: Locator\n  productOptions: Locator\n  productPrice: Locator\n  addProductButton: Locator\n  mobileActionsContainer: Locator\n  mobileTitle: Locator\n  mobileActionsButton: Locator\n  mobileAddToCartButton: Locator\n\n  constructor(page: Page) {\n    super(page)\n\n    this.mobileActionsModal = new MobileActionsModal(page)\n\n    this.container = page.getByTestId(\"product-container\")\n    this.productTitle = this.container.getByTestId(\"product-title\")\n    this.productDescription = this.container.getByTestId(\"product-description\")\n    this.productOptions = this.container.getByTestId(\"product-options\")\n    this.productPrice = this.container.getByTestId(\"product-price\")\n    this.addProductButton = this.container.getByTestId(\"add-product-button\")\n    this.mobileActionsContainer = page.getByTestId(\"mobile-actions\")\n    this.mobileTitle = this.mobileActionsContainer.getByTestId(\"mobile-title\")\n    this.mobileAddToCartButton = this.mobileActionsContainer.getByTestId(\n      \"mobile-actions-button\"\n    )\n    this.mobileActionsButton = this.mobileActionsContainer.getByTestId(\n      \"mobile-actions-select\"\n    )\n  }\n\n  async clickAddProduct() {\n    await this.addProductButton.click()\n    await this.cartDropdown.cartDropdown.waitFor({ state: \"visible\" })\n  }\n\n  async selectOption(option: string) {\n    await this.page.mouse.move(0, 0) // hides the checkout container\n    const optionButton = this.productOptions\n      .getByTestId(\"option-button\")\n      .filter({ hasText: option })\n    await optionButton.click({ clickCount: 2 })\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/fixtures/store-page.ts",
    "content": "import { Locator, Page } from \"@playwright/test\"\nimport { CategoryPage } from \"./category-page\"\n\nexport class StorePage extends CategoryPage {\n  pageTitle: Locator\n\n  constructor(page: Page) {\n    super(page)\n    this.pageTitle = page.getByTestId(\"store-page-title\")\n  }\n\n  async goto() {\n    await this.navMenu.open()\n    await this.navMenu.storeLink.click()\n    await this.pageTitle.waitFor({ state: \"visible\" })\n    await this.productsListLoader.waitFor({ state: \"hidden\" })\n  }\n}"
  },
  {
    "path": "storefront/e2e/index.ts",
    "content": "import { mergeTests } from \"@playwright/test\"\nimport { fixtures } from \"./fixtures\"\nimport { accountFixtures } from \"./fixtures/account\"\n\nexport const test = mergeTests(fixtures, accountFixtures)\nexport { expect } from \"@playwright/test\"\n"
  },
  {
    "path": "storefront/e2e/tests/authenticated/address.spec.ts",
    "content": "import { AddressesPage } from \"../../fixtures/account/addresses-page\"\nimport { test, expect } from \"../../index\"\nimport { getSelectedOptionText } from \"../../utils/locators\"\n\ntest.describe(\"Addresses tests\", () => {\n  test(\"Creating a new address is displayed during checkout\", async ({\n    accountAddressesPage: addressesPage,\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Navigate to the new address modal\", async () => {\n      await addressesPage.goto()\n      await addressesPage.newAddressButton.click()\n      await addressesPage.addAddressModal.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Inputs and saves the new address\", async () => {\n      const modal = addressesPage.addAddressModal\n      await modal.firstNameInput.fill(\"First\")\n      await modal.lastNameInput.fill(\"Last\")\n      await modal.companyInput.fill(\"FirstCorp\")\n      await modal.address1Input.fill(\"123 Fake Street\")\n      await modal.address2Input.fill(\"Apt 1\")\n      await modal.postalCodeInput.fill(\"11111\")\n      await modal.cityInput.fill(\"City\")\n      await modal.stateInput.fill(\"Colorado\")\n      await modal.countrySelect.selectOption({\n        label: \"United States\",\n      })\n      await modal.phoneInput.fill(\"1112223333\")\n      await modal.saveButton.click()\n      await modal.container.waitFor({ state: \"hidden\" })\n    })\n\n    await test.step(\"Navigate to a product page and add a product to the cart\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.highlight()\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n      await productPage.selectOption(\"M\")\n      await productPage.addProductButton.click()\n      await productPage.cartDropdown.navCartLink.click()\n      await productPage.cartDropdown.goToCartButton.click()\n      await cartPage.container.waitFor({ state: \"visible\" })\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Verify the address is correct in the checkout process\", async () => {\n      await checkoutPage.selectSavedAddress(\"123 Fake Street\")\n      await expect(checkoutPage.shippingFirstNameInput).toHaveValue(\"First\")\n      await expect(checkoutPage.shippingLastNameInput).toHaveValue(\"Last\")\n      await expect(checkoutPage.shippingCompanyInput).toHaveValue(\"FirstCorp\")\n      await expect(checkoutPage.shippingAddressInput).toHaveValue(\n        \"123 Fake Street\"\n      )\n      await expect(checkoutPage.shippingPostalCodeInput).toHaveValue(\"11111\")\n      await expect(checkoutPage.shippingCityInput).toHaveValue(\"City\")\n      await expect(checkoutPage.shippingProvinceInput).toHaveValue(\"Colorado\")\n      expect(\n        await getSelectedOptionText(\n          checkoutPage.page,\n          checkoutPage.shippingCountrySelect\n        )\n      ).toContain(\"United States\")\n    })\n  })\n\n  test(\"Performing all the CRUD actions for an address\", async ({\n    accountAddressesPage: addressesPage,\n  }) => {\n    await test.step(\"Navigate to the new address modal\", async () => {\n      await addressesPage.goto()\n      await addressesPage.newAddressButton.click()\n      await addressesPage.addAddressModal.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Input and save a new address\", async () => {\n      const { addAddressModal } = addressesPage\n      await addAddressModal.firstNameInput.fill(\"First\")\n      await addAddressModal.lastNameInput.fill(\"Last\")\n      await addAddressModal.companyInput.fill(\"MyCorp\")\n      await addAddressModal.address1Input.fill(\"123 Fake Street\")\n      await addAddressModal.address2Input.fill(\"Apt 1\")\n      await addAddressModal.postalCodeInput.fill(\"80010\")\n      await addAddressModal.cityInput.fill(\"Denver\")\n      await addAddressModal.stateInput.fill(\"Colorado\")\n      await addAddressModal.countrySelect.selectOption({ label: \"United States\" })\n      await addAddressModal.phoneInput.fill(\"3031112222\")\n      await addAddressModal.saveButton.click()\n      await addAddressModal.container.waitFor({ state: \"hidden\" })\n    })\n\n    let addressContainer: ReturnType<AddressesPage[\"getAddressContainer\"]>\n    await test.step(\"Make sure the address container was appended to the page\", async () => {\n      addressContainer = addressesPage.getAddressContainer(\"First Last\")\n      await expect(addressContainer.name).toHaveText(\"First Last\")\n      await expect(addressContainer.company).toHaveText(\"MyCorp\")\n      await expect(addressContainer.address).toContainText(\"123 Fake Street\")\n      await expect(addressContainer.address).toContainText(\"Apt 1\")\n      await expect(addressContainer.postalCity).toContainText(\"80010, Denver\")\n      await expect(addressContainer.provinceCountry).toContainText(\"Colorado, US\")\n    })\n\n    await test.step(\"Refresh the page and assert address was saved\", async () => {\n      await addressesPage.page.reload()\n      addressContainer = addressesPage.getAddressContainer(\"First Last\")\n      await expect(addressContainer.name).toHaveText(\"First Last\")\n      await expect(addressContainer.company).toHaveText(\"MyCorp\")\n      await expect(addressContainer.address).toContainText(\"123 Fake Street\")\n      await expect(addressContainer.address).toContainText(\"Apt 1\")\n      await expect(addressContainer.postalCity).toContainText(\"80010, Denver\")\n      await expect(addressContainer.provinceCountry).toContainText(\"Colorado, US\")\n    })\n\n    await test.step(\"Edit the address\", async () => {\n      await addressContainer.editButton.click()\n      await addressesPage.editAddressModal.container.waitFor({ state: \"visible\" })\n      await addressesPage.editAddressModal.firstNameInput.fill(\"Second\")\n      await addressesPage.editAddressModal.lastNameInput.fill(\"Final\")\n      await addressesPage.editAddressModal.companyInput.fill(\"MeCorp\")\n      await addressesPage.editAddressModal.address1Input.fill(\"123 Spark Street\")\n      await addressesPage.editAddressModal.address2Input.fill(\"Unit 3\")\n      await addressesPage.editAddressModal.postalCodeInput.fill(\"80011\")\n      await addressesPage.editAddressModal.cityInput.fill(\"Broomfield\")\n      await addressesPage.editAddressModal.stateInput.fill(\"CO\")\n      await addressesPage.editAddressModal.countrySelect.selectOption({\n        label: \"Canada\",\n      })\n      await addressesPage.editAddressModal.phoneInput.fill(\"3032223333\")\n      await addressesPage.editAddressModal.saveButton.click()\n      await addressesPage.editAddressModal.container.waitFor({ state: \"hidden\" })\n    })\n\n    await test.step(\"Make sure edits were saved on the addressContainer\", async () => {\n      addressContainer = addressesPage.getAddressContainer(\"Second Final\")\n      await expect(addressContainer.name).toContainText(\"Second Final\")\n      await expect(addressContainer.company).toContainText(\"MeCorp\")\n      await expect(addressContainer.address).toContainText(\"123 Spark Street, Unit 3\")\n      await expect(addressContainer.postalCity).toContainText(\"80011, Broomfield\")\n      await expect(addressContainer.provinceCountry).toContainText(\"CO, CA\")\n    })\n\n    await test.step(\"Refresh the page and assert edits were saved\", async () => {\n      await addressesPage.page.reload()\n      await expect(addressContainer.name).toContainText(\"Second Final\")\n      await expect(addressContainer.company).toContainText(\"MeCorp\")\n      await expect(addressContainer.address).toContainText(\"123 Spark Street, Unit 3\")\n      await expect(addressContainer.postalCity).toContainText(\"80011, Broomfield\")\n      await expect(addressContainer.provinceCountry).toContainText(\"CO, CA\")\n    })\n\n    await test.step(\"Delete the address\", async () => {\n      await addressContainer.deleteButton.click()\n      await addressContainer.container.waitFor({ state: \"hidden\" })\n      await addressesPage.page.reload()\n      await expect(addressContainer.container).not.toBeVisible()\n    })\n\n    await test.step(\"Ensure address remains deleted after refresh\", async () => {\n      await addressesPage.page.reload()\n      await expect(addressContainer.container).not.toBeVisible()\n    })\n  })\n\n  test.skip(\"Attempt to create duplicate addresses on the address page\", async ({\n    accountAddressesPage: addressesPage\n  }) => {\n    await test.step(\"navigate to the new address modal\", async () => {\n      await addressesPage.goto()\n      await addressesPage.newAddressButton.click()\n      await addressesPage.addAddressModal.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Input and save a new address\", async () => {\n      await addressesPage.addAddressModal.firstNameInput.fill(\"First\")\n      await addressesPage.addAddressModal.lastNameInput.fill(\"Last\")\n      await addressesPage.addAddressModal.companyInput.fill(\"MyCorp\")\n      await addressesPage.addAddressModal.address1Input.fill(\"123 Fake Street\")\n      await addressesPage.addAddressModal.address2Input.fill(\"Apt 1\")\n      await addressesPage.addAddressModal.postalCodeInput.fill(\"80010\")\n      await addressesPage.addAddressModal.cityInput.fill(\"Denver\")\n      await addressesPage.addAddressModal.stateInput.fill(\"Colorado\")\n      await addressesPage.addAddressModal.countrySelect.selectOption({\n        label: \"United States\",\n      })\n      await addressesPage.addAddressModal.phoneInput.fill(\"3031112222\")\n      await addressesPage.addAddressModal.saveButton.click()\n      await addressesPage.addAddressModal.container.waitFor({ state: \"hidden\" })\n    })\n\n    await test.step(\"Attempt to create the same address\", async () => {\n      await addressesPage.newAddressButton.click()\n      await addressesPage.addAddressModal.container.waitFor({ state: \"visible\" })\n      await addressesPage.addAddressModal.firstNameInput.fill(\"First\")\n      await addressesPage.addAddressModal.lastNameInput.fill(\"Last\")\n      await addressesPage.addAddressModal.companyInput.fill(\"MyCorp\")\n      await addressesPage.addAddressModal.address1Input.fill(\"123 Fake Street\")\n      await addressesPage.addAddressModal.address2Input.fill(\"Apt 1\")\n      await addressesPage.addAddressModal.postalCodeInput.fill(\"80010\")\n      await addressesPage.addAddressModal.cityInput.fill(\"Denver\")\n      await addressesPage.addAddressModal.stateInput.fill(\"Colorado\")\n      await addressesPage.addAddressModal.countrySelect.selectOption({\n        label: \"United States\",\n      })\n      await addressesPage.addAddressModal.phoneInput.fill(\"3031112222\")\n      await addressesPage.addAddressModal.saveButton.click()\n    })\n\n    await test.step(\"Validate error state\", async () => {\n\n    })\n  })\n\n  test(\"Creating multiple tests works correctly\", async ({\n    accountAddressesPage: addressesPage,\n  }) => {\n    test.slow()\n    await test.step(\"Navigate to the new address modal\", async () => {\n      await addressesPage.goto()\n    })\n\n    let addressContainer: ReturnType<AddressesPage[\"getAddressContainer\"]>\n    for (let i = 0; i < 10; i++) {\n      await test.step(\"Open up the new address modal\", async () => {\n        await addressesPage.newAddressButton.click()\n        await addressesPage.addAddressModal.container.waitFor({ state: \"visible\" })\n      })\n      await test.step(\"Input and save a new address\", async () => {\n        const { addAddressModal } = addressesPage\n        await addAddressModal.firstNameInput.fill(`First-${i}`)\n        await addAddressModal.lastNameInput.fill(`Last-${i}`)\n        await addAddressModal.companyInput.fill(`MyCorp-${i}`)\n        await addAddressModal.address1Input.fill(`123 Fake Street-${i}`)\n        await addAddressModal.address2Input.fill(\"Apt 1\")\n        await addAddressModal.postalCodeInput.fill(\"80010\")\n        await addAddressModal.cityInput.fill(\"Denver\")\n        await addAddressModal.stateInput.fill(\"Colorado\")\n        await addAddressModal.countrySelect.selectOption({ label: \"United States\" })\n        await addAddressModal.phoneInput.fill(\"3031112222\")\n        await addAddressModal.saveButton.click()\n        await addAddressModal.container.waitFor({ state: \"hidden\" })\n      })\n      await test.step(\"Make sure the address container was appended to the page\", async () => {\n        addressContainer = addressesPage.getAddressContainer(`First-${i} Last-${i}`)\n        await expect(addressContainer.name).toHaveText(`First-${i} Last-${i}`)\n        await expect(addressContainer.company).toHaveText(`MyCorp-${i}`)\n        await expect(addressContainer.address).toContainText(`123 Fake Street-${i}`)\n        await expect(addressContainer.address).toContainText(\"Apt 1\")\n        await expect(addressContainer.postalCity).toContainText(\"80010, Denver\")\n        await expect(addressContainer.provinceCountry).toContainText(\"Colorado, US\")\n      })\n    }\n  })\n})"
  },
  {
    "path": "storefront/e2e/tests/authenticated/orders.spec.ts",
    "content": "import { test, expect } from \"../../index\"\n\ntest.describe(\"Account orders page tests\", async () => {\n  test.beforeEach(async ({ accountAddressesPage }) => {\n    await accountAddressesPage.goto()\n    await accountAddressesPage.newAddressButton.click()\n    await test.step(\"Add default address\", async () => {\n      const modal = accountAddressesPage.addAddressModal\n      await modal.container.waitFor({ state: \"visible\" })\n      await modal.firstNameInput.fill(\"First\")\n      await modal.lastNameInput.fill(\"Last\")\n      await modal.companyInput.fill(\"FirstCorp\")\n      await modal.address1Input.fill(\"123 Fake Street\")\n      await modal.address2Input.fill(\"Apt 1\")\n      await modal.postalCodeInput.fill(\"11111\")\n      await modal.cityInput.fill(\"City\")\n      await modal.stateInput.fill(\"Colorado\")\n      await modal.countrySelect.selectOption({\n        label: \"United States\",\n      })\n      await modal.phoneInput.fill(\"1112223333\")\n      await modal.saveButton.click()\n      await modal.container.waitFor({ state: \"hidden\" })\n    })\n  })\n\n  test(\"Verify account orders page displays empty container\", async ({\n    accountOrdersPage,\n  }) => {\n    await accountOrdersPage.goto()\n    await expect(accountOrdersPage.noOrdersContainer).toBeVisible()\n  })\n\n  test(\"Order shows up after checkout flow\", async ({\n    accountOrdersPage,\n    accountOrderPage,\n    cartPage,\n    checkoutPage,\n    orderPage: publicOrderPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Navigate to a product page\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.highlight()\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the product to the cart and goto checkout\", async () => {\n      await productPage.selectOption(\"M\")\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.navCartLink.click()\n      await productPage.cartDropdown.goToCartButton.click()\n      await cartPage.container.waitFor({ state: \"visible\" })\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Enter in the first step of the checkout process\", async () => {\n      await checkoutPage.selectSavedAddress(\"123 Fake Street\")\n      await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n      await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n      await checkoutPage.submitAddressButton.click()\n      await checkoutPage.deliveryOptionsContainer.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Complete the rest of the payment process\", async () => {\n      await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n      await checkoutPage.submitDeliveryOptionButton.click()\n      await checkoutPage.submitPaymentButton.click()\n      await checkoutPage.submitOrderButton.click()\n      await publicOrderPage.container.waitFor({ state: \"visible\" })\n    })\n\n    let orderId = \"\"\n    await test.step(\"Verify the order page information is correct\", async () => {\n      orderId = (await publicOrderPage.orderId.textContent()) || \"\"\n\n      await test.step(\"Verify the products ordered are correct\", async () => {\n        const product = await publicOrderPage.getProduct(\"Sweatshirt\", \"M\")\n        await expect(product.name).toContainText(\"Sweatshirt\")\n        await expect(product.variant).toContainText(\"M\")\n        await expect(product.quantity).toContainText(\"1\")\n      })\n\n      await test.step(\"Verify the shipping info is correct\", async () => {\n        const address = publicOrderPage.shippingAddressSummary\n        await expect(address).toContainText(\"First\")\n        await expect(address).toContainText(\"Last\")\n        await expect(address).toContainText(\"123 Fake Street\")\n        await expect(address).toContainText(\"11111\")\n        await expect(address).toContainText(\"City\")\n        await expect(address).toContainText(\"US\")\n\n        const contact = publicOrderPage.shippingContactSummary\n        await expect(contact).toContainText(\"test@example.com\")\n        await expect(contact).toContainText(\"3031112222\")\n\n        const method = publicOrderPage.shippingMethodSummary\n        await expect(method).toContainText(\"FakeEx Standard\")\n      })\n    })\n\n    await test.step(\"Verify the account orders page displays a result\", async () => {\n      await accountOrdersPage.goto()\n      const order = await accountOrdersPage.getOrderById(orderId)\n      expect(order.items.length).toBe(1)\n      expect(order.items[0].title).toContainText(\"Sweatshirt\")\n      expect(order.items[0].quantity).toHaveText(\"1\")\n      await order.detailsLink.click()\n      await accountOrderPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Verify the order page displays the correct information\", async () => {\n      await test.step(\"Verify the order id is correct\", async () => {\n        await expect(accountOrderPage.orderId).toHaveText(orderId)\n      })\n\n      await test.step(\"Verify the products ordered are correct\", async () => {\n        const product = await accountOrderPage.getProduct(\"Sweatshirt\", \"M\")\n        await expect(product.name).toContainText(\"Sweatshirt\")\n        await expect(product.variant).toContainText(\"M\")\n        await expect(product.quantity).toContainText(\"1\")\n      })\n\n      await test.step(\"Verify the shipping info is correct\", async () => {\n        const address = accountOrderPage.shippingAddressSummary\n        await expect(address).toContainText(\"First\")\n        await expect(address).toContainText(\"Last\")\n        await expect(address).toContainText(\"123 Fake Street\")\n        await expect(address).toContainText(\"11111\")\n        await expect(address).toContainText(\"City\")\n        await expect(address).toContainText(\"US\")\n\n        const contact = accountOrderPage.shippingContactSummary\n        await contact.highlight()\n        await expect(contact.getByText(\"test@example.com\")).toBeVisible()\n        await expect(contact.getByText(\"3031112222\")).toBeVisible()\n\n        const method = accountOrderPage.shippingMethodSummary\n        await method.highlight()\n        await expect(method).toContainText(\"FakeEx Standard\")\n      })\n    })\n\n    await test.step(\"Navigate back to the orders page, verifying back button works\", async () => {\n      await accountOrderPage.backToOverviewButton.click()\n      await accountOrdersPage.container.waitFor({ state: \"visible\" })\n    })\n  })\n\n  test(\"Order preserves item count, and variants\", async ({\n    accountOrdersPage,\n    accountOrderPage,\n    cartPage,\n    checkoutPage,\n    orderPage: publicOrderPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Add first batch or products to the cart\", async () => {\n      await test.step(\"Navigate to the sweatshirt product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.highlight()\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.close()\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.close()\n      })\n    })\n\n    await test.step(\"Add second batch of products to the cart\", async () => {\n      await test.step(\"Navigate to the sweatshirt product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatpants\")\n        await product.locator.highlight()\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"S\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.close()\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n      })\n\n      await test.step(\"Navigate to the checkout process\", async () => {\n        await productPage.cartDropdown.goToCartButton.click()\n        await productPage.cartDropdown.close()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.checkoutButton.click()\n        await checkoutPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n\n    let orderId = \"\"\n    await test.step(\"Checkout process\", async () => {\n      await test.step(\"Enter in the first step of the checkout process\", async () => {\n        await checkoutPage.selectSavedAddress(\"123 Fake Street\")\n        await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n        await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n        await checkoutPage.submitAddressButton.click()\n        await checkoutPage.deliveryOptionsContainer.waitFor({\n          state: \"visible\",\n        })\n      })\n\n      await test.step(\"Complete the rest of the payment process\", async () => {\n        await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n        await checkoutPage.submitDeliveryOptionButton.click()\n        await checkoutPage.submitPaymentButton.click()\n        await checkoutPage.submitOrderButton.click()\n        await publicOrderPage.container.waitFor({ state: \"visible\" })\n        orderId = (await publicOrderPage.orderId.textContent()) || \"\"\n      })\n    })\n\n    await test.step(\"Verify the order page information is correct\", async () => {\n      await test.step(\"Navigate to the account orders page, verify information, and navigate to the order page\", async () => {\n        await accountOrdersPage.goto()\n        const order = await accountOrdersPage.getOrderById(orderId)\n        expect(order.itemsLocator).toHaveCount(3)\n        expect(\n          order.itemsLocator.filter({ hasText: \"Sweatpants\" })\n        ).toHaveCount(2)\n        expect(\n          order.itemsLocator.filter({ hasText: \"Sweatshirt\" })\n        ).toHaveCount(1)\n        await order.detailsLink.click()\n        await accountOrderPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Verify information on the order page\", async () => {\n        const sweatshirt = await accountOrderPage.getProduct(\"Sweatshirt\", \"M\")\n        await expect(sweatshirt.name).toContainText(\"Sweatshirt\")\n        await expect(sweatshirt.variant).toContainText(\"M\")\n        await expect(sweatshirt.quantity).toContainText(\"2\")\n\n        const smallSweatpants = await accountOrderPage.getProduct(\n          \"Sweatpants\",\n          \"S\"\n        )\n        await expect(smallSweatpants.name).toContainText(\"Sweatpants\")\n        await expect(smallSweatpants.variant).toContainText(\"S\")\n        await expect(smallSweatpants.quantity).toContainText(\"1\")\n\n        const mediumSweatpants = await accountOrderPage.getProduct(\n          \"Sweatpants\",\n          \"M\"\n        )\n        await expect(mediumSweatpants.name).toContainText(\"Sweatpants\")\n        await expect(mediumSweatpants.variant).toContainText(\"M\")\n        await expect(mediumSweatpants.quantity).toContainText(\"1\")\n      })\n    })\n  })\n\n  test(\"Multiple orders are stored correctly\", async ({\n    accountOrdersPage,\n    cartPage,\n    checkoutPage,\n    orderPage: publicOrderPage,\n    productPage,\n    storePage,\n  }) => {\n    let firstOrderId = \"\"\n    let secondOrderId = \"\"\n    await test.step(\"Make the first order\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.highlight()\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.checkoutButton.click()\n        await checkoutPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Enter in the first step of the checkout process\", async () => {\n        await checkoutPage.selectSavedAddress(\"123 Fake Street\")\n        await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n        await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n        await checkoutPage.submitAddressButton.click()\n        await checkoutPage.deliveryOptionsContainer.waitFor({\n          state: \"visible\",\n        })\n      })\n\n      await test.step(\"Complete the rest of the payment process\", async () => {\n        await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n        await checkoutPage.submitDeliveryOptionButton.click()\n        await checkoutPage.submitPaymentButton.click()\n        await checkoutPage.submitOrderButton.click()\n        await publicOrderPage.container.waitFor({ state: \"visible\" })\n        firstOrderId = (await publicOrderPage.orderId.textContent()) || \"\"\n      })\n    })\n\n    await test.step(\"Make the second order\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatpants\")\n        await product.locator.highlight()\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"S\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.checkoutButton.click()\n        await checkoutPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Enter in the first step of the checkout process\", async () => {\n        await checkoutPage.selectSavedAddress(\"123 Fake Street\")\n        await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n        await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n        await checkoutPage.submitAddressButton.click()\n        await checkoutPage.deliveryOptionsContainer.waitFor({\n          state: \"visible\",\n        })\n      })\n\n      await test.step(\"Complete the rest of the payment process\", async () => {\n        await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n        await checkoutPage.submitDeliveryOptionButton.click()\n        await checkoutPage.submitPaymentButton.click()\n        await checkoutPage.submitOrderButton.click()\n        await publicOrderPage.container.waitFor({ state: \"visible\" })\n        secondOrderId = (await publicOrderPage.orderId.textContent()) || \"\"\n      })\n    })\n\n    await test.step(\"Verify there are distinct orders on the orders page\", async () => {\n      await accountOrdersPage.goto()\n      await test.step(\"Verify the first order info\", async () => {\n        const order = await accountOrdersPage.getOrderById(firstOrderId)\n        await expect(order.itemsLocator).toHaveCount(1)\n        await expect(order.items[0].title).toContainText(\"Sweatshirt\")\n        await expect(order.items[0].quantity).toHaveText(\"1\")\n      })\n      await test.step(\"Verify the second order info\", async () => {\n        const order = await accountOrdersPage.getOrderById(secondOrderId)\n        await expect(order.itemsLocator).toHaveCount(1)\n        await expect(order.items[0].title).toContainText(\"Sweatpants\")\n        await expect(order.items[0].quantity).toHaveText(\"1\")\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "storefront/e2e/tests/authenticated/profile.spec.ts",
    "content": "import { test, expect } from \"../../index\"\n\ntest.describe(\"Account profile tests\", () => {\n  test(\"Profile completed update flow\", async ({\n    accountOverviewPage: overviewPage,\n    accountProfilePage: profilePage,\n  }) => {\n    await overviewPage.goto()\n    await expect(overviewPage.profileCompletion).toHaveText(\"50%\")\n\n    await test.step(\"navigate to the profile page\", async () => {\n      await profilePage.profileLink.click()\n      await expect(profilePage.profileWrapper).toBeVisible()\n    })\n\n    await test.step(\"update the saved profile phone number\", async () => {\n      await expect(profilePage.savedPhone).toHaveText(\"null\")\n      await profilePage.phoneEditButton.click()\n      await profilePage.phoneInput.fill(\"8888888888\")\n      await profilePage.phoneSaveButton.click()\n      await expect(profilePage.phoneSuccessMessage).toBeVisible()\n      await expect(profilePage.savedPhone).toHaveText(\"8888888888\")\n    })\n\n    await test.step(\"verify the profile completion state and go back to the profile page\", async () => {\n      await profilePage.overviewLink.click()\n      await expect(overviewPage.profileCompletion).toHaveText(\"75%\")\n\n      await profilePage.profileLink.click()\n      await expect(profilePage.profileWrapper).toBeVisible()\n    })\n\n    await test.step(\"enter in the billing address\", async () => {\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"No billing address\"\n      )\n      await profilePage.billingAddressEditButton.click()\n      await profilePage.billingFirstNameInput.fill(\"First\")\n      await profilePage.billingLastNameInput.fill(\"Last\")\n      await profilePage.billingAddress1Input.fill(\"123 Fake Street\")\n      await profilePage.billingPostcalCodeInput.fill(\"11111\")\n      await profilePage.billingCityInput.fill(\"Springdale\")\n      await profilePage.billingProvinceInput.fill(\"IL\")\n      await profilePage.billingCountryCodeSelect.selectOption({\n        label: \"United States\",\n      })\n      await profilePage.billingAddressSaveButton.click()\n      await expect(profilePage.billingAddressSuccessMessage).toBeVisible()\n    })\n\n    await test.step(\"profile completion state\", async () => {\n      await profilePage.overviewLink.click()\n      await expect(overviewPage.profileCompletion).toHaveText(\"100%\")\n\n      await profilePage.goto()\n      await expect(profilePage.savedBillingAddress).toContainText(\"First Last\")\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"123 Fake Street\"\n      )\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"11111, Springdale\"\n      )\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"United States\"\n      )\n    })\n  })\n\n  test(\"Profile changes persist across page refreshes and logouts\", async ({\n    page,\n    loginPage,\n    accountOverviewPage: overviewPage,\n    accountProfilePage: profilePage,\n  }) => {\n    await overviewPage.goto()\n    await expect(overviewPage.profileCompletion).toHaveText(\"50%\")\n\n    await test.step(\"navigate to the profile page\", async () => {\n      await profilePage.profileLink.click()\n      await expect(profilePage.profileWrapper).toBeVisible()\n    })\n\n    await test.step(\"update the first and last name\", async () => {\n      await profilePage.nameEditButton.click()\n      await profilePage.firstNameInput.fill(\"FirstNew\")\n      await profilePage.lastNameInput.fill(\"LastNew\")\n      await profilePage.nameSaveButton.click()\n      await profilePage.nameSuccessMessage.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"update the saved profile phone number\", async () => {\n      await expect(profilePage.savedPhone).toHaveText(\"null\")\n      await profilePage.phoneEditButton.click()\n      await profilePage.phoneInput.fill(\"8888888888\")\n      await profilePage.phoneSaveButton.click()\n      await expect(profilePage.phoneSuccessMessage).toBeVisible()\n      await expect(profilePage.savedPhone).toHaveText(\"8888888888\")\n    })\n\n    await test.step(\"enter in the billing address\", async () => {\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"No billing address\"\n      )\n      await profilePage.billingAddressEditButton.click()\n      await profilePage.billingFirstNameInput.fill(\"First\")\n      await profilePage.billingLastNameInput.fill(\"Last\")\n      await profilePage.billingAddress1Input.fill(\"123 Fake Street\")\n      await profilePage.billingPostcalCodeInput.fill(\"11111\")\n      await profilePage.billingCityInput.fill(\"Springdale\")\n      await profilePage.billingProvinceInput.fill(\"IL\")\n      await profilePage.billingCountryCodeSelect.selectOption({\n        label: \"United States\",\n      })\n      await profilePage.billingAddressSaveButton.click()\n      await expect(profilePage.billingAddressSuccessMessage).toBeVisible()\n    })\n\n    await test.step(\"Refresh page and verify information saved is still there\", async () => {\n      await page.reload()\n      await expect(profilePage.savedName).toContainText(\"FirstNew\")\n      await expect(profilePage.savedName).toContainText(\"LastNew\")\n      await expect(profilePage.savedPhone).toContainText(\"8888888888\")\n\n      await expect(profilePage.savedBillingAddress).toContainText(\"First Last\")\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"123 Fake Street\"\n      )\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"11111, Springdale\"\n      )\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"United States\"\n      )\n    })\n\n    await test.step(\"Log out and log back in\", async () => {\n      await profilePage.logoutLink.click()\n      await expect(loginPage.container).toBeVisible()\n      await loginPage.emailInput.fill(\"test@example.com\")\n      await loginPage.passwordInput.fill(\"password\")\n      await loginPage.signInButton.click()\n      await overviewPage.overviewWrapper.waitFor({ state: \"visible\" })\n      await overviewPage.profileLink.click()\n      await profilePage.profileWrapper.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Verify the saved profile information is correct\", async () => {\n      await expect(profilePage.savedName).toContainText(\"FirstNew\")\n      await expect(profilePage.savedName).toContainText(\"LastNew\")\n      await expect(profilePage.savedPhone).toContainText(\"8888888888\")\n\n      await expect(profilePage.savedBillingAddress).toContainText(\"First Last\")\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"123 Fake Street\"\n      )\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"11111, Springdale\"\n      )\n      await expect(profilePage.savedBillingAddress).toContainText(\n        \"United States\"\n      )\n    })\n  })\n\n  test(\"Verifies password changes work correctly\", async ({\n    loginPage,\n    accountProfilePage: profilePage,\n    accountOverviewPage: overviewPage,\n  }) => {\n    await test.step(\"Navigate to the account Profile page\", async () => {\n      await overviewPage.goto()\n      await profilePage.profileLink.click()\n    })\n\n    await test.step(\"Update the password\", async () => {\n      await profilePage.passwordEditButton.click()\n      await profilePage.oldPasswordInput.fill(\"password\")\n      await profilePage.newPasswordInput.fill(\"updated-password\")\n      await profilePage.confirmPasswordInput.fill(\"updated-password\")\n      await profilePage.passwordSaveButton.click()\n      await expect(profilePage.passwordSuccessMessage).toBeVisible()\n    })\n\n    await test.step(\"logout and log back in\", async () => {\n      await profilePage.logoutLink.click()\n      await expect(loginPage.container).toBeVisible()\n      await loginPage.emailInput.fill(\"test@example.com\")\n      await loginPage.passwordInput.fill(\"updated-password\")\n      await loginPage.signInButton.click()\n      await expect(overviewPage.container).toBeVisible()\n    })\n  })\n\n  test(\"Check if changing email address updates user correctly\", async ({\n    loginPage,\n    accountProfilePage: profilePage,\n    accountOverviewPage: accountPage,\n  }) => {\n    await test.step(\"Update the user email\", async () => {\n      await accountPage.goto()\n      await accountPage.welcomeMessage.waitFor({ state: \"visible\" })\n      await accountPage.profileLink.click()\n      await profilePage.profileWrapper.waitFor({ state: \"visible\" })\n      await profilePage.emailEditButton.click()\n      await profilePage.emailInput.fill(\"test-111@example.com\")\n      await profilePage.emailSaveButton.click()\n      await profilePage.emailSuccessMessage.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Try logging in again with the old email\", async () => {\n      await profilePage.logoutLink.click()\n      await loginPage.container.waitFor({ state: \"visible\" })\n      await loginPage.emailInput.fill(\"test@example.com\")\n      await loginPage.passwordInput.fill(\"password\")\n      await loginPage.signInButton.click()\n      await loginPage.errorMessage.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Login with the new email\", async () => {\n      await loginPage.emailInput.fill(\"test-111@example.com\")\n      await loginPage.signInButton.click()\n      await accountPage.welcomeMessage.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Set the email back to test@example.com\", async () => {\n      await accountPage.profileLink.click()\n      await profilePage.profileWrapper.waitFor({ state: \"visible\" })\n      await profilePage.emailEditButton.click()\n      await profilePage.emailInput.fill(\"test@example.com\")\n      await profilePage.emailSaveButton.click()\n      await profilePage.emailSuccessMessage.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Try logging out and logging in with the first email\", async () => {\n      await profilePage.logoutLink.click()\n      await loginPage.container.waitFor({ state: \"visible\" })\n      await loginPage.emailInput.fill(\"test@example.com\")\n      await loginPage.passwordInput.fill(\"password\")\n      await loginPage.signInButton.click()\n      await accountPage.welcomeMessage.waitFor({ state: \"visible\" })\n    })\n  })\n})\n"
  },
  {
    "path": "storefront/e2e/tests/global/public-setup.ts",
    "content": "import { test as setup } from \"@playwright/test\"\nimport { seedData } from \"../../data/seed\"\n\nsetup(\"Seed data\", async () => {\n  await seedData()\n})\n"
  },
  {
    "path": "storefront/e2e/tests/global/setup.ts",
    "content": "import { test as setup } from \"@playwright/test\"\nimport { seedData } from \"../../data/seed\"\nimport { OverviewPage as AccountOverviewPage } from \"../../fixtures/account/overview-page\"\nimport { LoginPage } from \"../../fixtures/account/login-page\"\nimport { STORAGE_STATE } from \"../../../playwright.config\"\n\nsetup(\n  \"Seed data and create session for authenticated user\",\n  async ({ page }) => {\n    const seed = await seedData()\n    const user = seed.user\n\n    const loginPage = new LoginPage(page)\n    const accountPage = new AccountOverviewPage(page)\n    await loginPage.goto()\n    await loginPage.emailInput.fill(user?.email!)\n    await loginPage.passwordInput.fill(user?.password!)\n    await loginPage.signInButton.click()\n    await accountPage.welcomeMessage.waitFor({ state: \"visible\" })\n\n    await page.context().storageState({\n      path: STORAGE_STATE,\n    })\n  }\n)\n"
  },
  {
    "path": "storefront/e2e/tests/global/teardown.ts",
    "content": "import { test as teardown } from \"@playwright/test\"\nimport { dropTemplate, resetDatabase } from \"../../data/reset\"\n\nteardown(\"Reset the database and the drop the template database\", async () => {\n  await resetDatabase()\n  await dropTemplate()\n})\n"
  },
  {
    "path": "storefront/e2e/tests/public/cart.spec.ts",
    "content": "/*\nTest List\n- login from the sign in page redirects you page to the cart\n*/\nimport { test, expect } from \"../../index\"\nimport { compareFloats, getFloatValue } from \"../../utils\"\n\ntest.describe(\"Cart tests\", async () => {\n  test(\"Ensure adding multiple items from a product page adjusts the cart accordingly\", async ({\n    page,\n    cartPage,\n    productPage,\n    storePage,\n  }) => {\n    // Assuming we have access to our page objects here\n    const cartDropdown = cartPage.cartDropdown\n\n    await test.step(\"Navigate to the product page\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the small size to the cart and verify the data\", async () => {\n      await productPage.selectOption(\"S\")\n      await productPage.addProductButton.click()\n      await expect(cartDropdown.navCartLink).toContainText(\"(1)\")\n      const cartItem = await cartDropdown.getCartItem(\"Sweatshirt\", \"S\")\n      await expect(cartItem.locator).toBeVisible()\n      await expect(cartItem.variant).toContainText(\"S\")\n      await expect(cartItem.quantity).toContainText(\"1\")\n      await cartDropdown.goToCartButton.click()\n      await cartDropdown.close()\n      await cartPage.container.waitFor({ state: \"visible\" })\n      const productInCart = await cartPage.getProduct(\"Sweatshirt\", \"S\")\n      await expect(productInCart.productRow).toBeVisible()\n      await expect(productInCart.quantitySelect).toHaveValue(\"1\")\n      await page.goBack()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the small size to the cart again and verify the data\", async () => {\n      await productPage.selectOption(\"S\")\n      await productPage.addProductButton.click()\n      await expect(cartDropdown.navCartLink).toContainText(\"(2)\")\n      const cartItem = await cartDropdown.getCartItem(\"Sweatshirt\", \"S\")\n      await expect(cartItem.locator).toBeVisible()\n      await expect(cartItem.variant).toContainText(\"S\")\n      await expect(cartItem.quantity).toContainText(\"2\")\n      await cartDropdown.goToCartButton.click()\n      await cartDropdown.close()\n      await cartPage.container.waitFor({ state: \"visible\" })\n      const productInCart = await cartPage.getProduct(\"Sweatshirt\", \"S\")\n      await expect(productInCart.productRow).toBeVisible()\n      await expect(productInCart.quantitySelect).toHaveValue(\"2\")\n      await page.goBack()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the medium size to the cart and verify the data\", async () => {\n      await productPage.selectOption(\"M\")\n      await productPage.addProductButton.click()\n      await expect(cartDropdown.navCartLink).toContainText(\"(3)\")\n      const mediumCartItem = await cartDropdown.getCartItem(\"Sweatshirt\", \"M\")\n      await expect(mediumCartItem.locator).toBeVisible()\n      await expect(mediumCartItem.variant).toContainText(\"M\")\n      await expect(mediumCartItem.quantity).toContainText(\"1\")\n      await cartDropdown.goToCartButton.click()\n      await cartDropdown.close()\n      await cartPage.container.waitFor({ state: \"visible\" })\n      const mediumProductInCart = await cartPage.getProduct(\"Sweatshirt\", \"M\")\n      await expect(mediumProductInCart.productRow).toBeVisible()\n      await expect(mediumProductInCart.quantitySelect).toHaveValue(\"1\")\n      const smallProductInCart = await cartPage.getProduct(\"Sweatshirt\", \"S\")\n      await expect(smallProductInCart.productRow).toBeVisible()\n      await expect(smallProductInCart.quantitySelect).toHaveValue(\"2\")\n    })\n  })\n\n  test(\"Ensure adding two products into the cart and verify the quantities\", async ({\n    cartPage,\n    productPage,\n    storePage,\n  }) => {\n    const cartDropdown = cartPage.cartDropdown\n\n    await test.step(\"Navigate to the product page - go to the store page and click on the Sweatshirt product\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the small sweatshirt to the cart\", async () => {\n      await productPage.selectOption(\"S\")\n      await productPage.addProductButton.click()\n      await expect(cartDropdown.navCartLink).toContainText(\"(1)\")\n      const sweatshirtItem = await cartDropdown.getCartItem(\"Sweatshirt\", \"S\")\n      await expect(sweatshirtItem.locator).toBeVisible()\n      await expect(sweatshirtItem.variant).toHaveText(\"Variant: S\")\n      await expect(sweatshirtItem.quantity).toContainText(\"1\")\n      await cartDropdown.close()\n    })\n\n    await test.step(\"Navigate to another product - Sweatpants\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatpants\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the small sweatpants to the cart\", async () => {\n      await productPage.selectOption(\"S\")\n      await productPage.addProductButton.click()\n      await expect(cartDropdown.navCartLink).toContainText(\"(2)\")\n      const sweatpantsItem = await cartDropdown.getCartItem(\"Sweatpants\", \"S\")\n      await expect(sweatpantsItem.locator).toBeVisible()\n      await expect(sweatpantsItem.variant).toHaveText(\"Variant: S\")\n      await expect(sweatpantsItem.quantity).toContainText(\"1\")\n      const sweatshirtItem = await cartDropdown.getCartItem(\"Sweatshirt\", \"S\")\n      await expect(sweatshirtItem.locator).toBeVisible()\n      await expect(sweatshirtItem.quantity).toContainText(\"1\")\n      await cartDropdown.goToCartButton.click()\n      await cartDropdown.close()\n      await cartPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Verify the quantities in the cart\", async () => {\n      const sweatpantsProduct = await cartPage.getProduct(\"Sweatpants\", \"S\")\n      await expect(sweatpantsProduct.productRow).toBeVisible()\n      await expect(sweatpantsProduct.quantitySelect).toHaveValue(\"1\")\n      const sweatshirtProduct = await cartPage.getProduct(\"Sweatshirt\", \"S\")\n      await expect(sweatshirtProduct.productRow).toBeVisible()\n      await expect(sweatshirtProduct.quantitySelect).toHaveValue(\"1\")\n    })\n  })\n\n  test(\"Verify the prices carries over to checkout\", async ({\n    cartPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Navigate to the product page - go to the store page and click on the Hoodie product\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Hoodie\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    let hoodieSmallPrice = 0\n    let hoodieMediumPrice = 0\n    await test.step(\"Add the hoodie to the cart\", async () => {\n      await productPage.selectOption(\"S\")\n      hoodieSmallPrice = getFloatValue(\n        (await productPage.productPrice.getAttribute(\"data-value\")) || \"0\"\n      )\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.close()\n      await productPage.selectOption(\"M\")\n      hoodieMediumPrice = getFloatValue(\n        (await productPage.productPrice.getAttribute(\"data-value\")) || \"0\"\n      )\n      await productPage.clickAddProduct()\n\n      await productPage.cartDropdown.close()\n    })\n\n    await test.step(\"Navigate to another product - Longsleeve\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Longsleeve\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    let longsleeveSmallPrice = 0\n    await test.step(\"Add the small longsleeve to the cart\", async () => {\n      await productPage.selectOption(\"S\")\n      longsleeveSmallPrice = getFloatValue(\n        (await productPage.productPrice.getAttribute(\"data-value\")) || \"0\"\n      )\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.close()\n      await productPage.selectOption(\"S\")\n      await productPage.clickAddProduct()\n      await productPage.selectOption(\"S\")\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.goToCartButton.click()\n      await productPage.cartDropdown.close()\n      await cartPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Verify the price in the cart is the expected value\", async () => {\n      const total = getFloatValue(\n        (await cartPage.cartSubtotal.getAttribute(\"data-value\")) || \"0\"\n      )\n      const calculatedTotal =\n        3 * longsleeveSmallPrice + hoodieSmallPrice + hoodieMediumPrice\n      expect(compareFloats(total, calculatedTotal)).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "storefront/e2e/tests/public/checkout.spec.ts",
    "content": "import { test, expect } from \"../../index\"\nimport { compareFloats, getFloatValue } from \"../../utils\"\n\ntest.describe(\"Checkout flow tests\", async () => {\n  test(\"Default checkout flow\", async ({\n    cartPage,\n    checkoutPage,\n    orderPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Navigate to a product page\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.highlight()\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the product to the cart and goto checkout\", async () => {\n      await productPage.selectOption(\"M\")\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.navCartLink.click()\n      await productPage.cartDropdown.goToCartButton.click()\n      await cartPage.container.waitFor({ state: \"visible\" })\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Enter in the first step of the checkout process\", async () => {\n      await test.step(\"Enter in the shipping address info\", async () => {\n        await checkoutPage.shippingFirstNameInput.fill(\"First\")\n        await checkoutPage.shippingLastNameInput.fill(\"Last\")\n        await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n        await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n        await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n        await checkoutPage.shippingCityInput.fill(\"Denver\")\n        await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n        await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n      })\n\n      await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n        await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n        await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n        await checkoutPage.billingAddressCheckbox.uncheck()\n      })\n\n      await test.step(\"Enter in the billing address info\", async () => {\n        await checkoutPage.billingFirstNameInput.fill(\"First\")\n        await checkoutPage.billingLastNameInput.fill(\"Last\")\n        await checkoutPage.billingCompanyInput.fill(\"MyCorp\")\n        await checkoutPage.billingAddressInput.fill(\"123 Fake street\")\n        await checkoutPage.billingPostalInput.fill(\"80010\")\n        await checkoutPage.billingCityInput.fill(\"Denver\")\n        await checkoutPage.billingProvinceInput.fill(\"Colorado\")\n        await checkoutPage.billingCountrySelect.selectOption(\"United States\")\n        await checkoutPage.submitAddressButton.click()\n      })\n    })\n\n    await test.step(\"Complete the rest of the payment process\", async () => {\n      await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n      await checkoutPage.submitDeliveryOptionButton.click()\n      await checkoutPage.submitPaymentButton.click()\n      await checkoutPage.submitOrderButton.click()\n      await orderPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Verify the products ordered are correct\", async () => {\n      const product = await orderPage.getProduct(\"Sweatshirt\", \"M\")\n      await expect(product.name).toContainText(\"Sweatshirt\")\n      await expect(product.variant).toContainText(\"M\")\n      await expect(product.quantity).toContainText(\"1\")\n    })\n\n    await test.step(\"Verify the shipping info is correct\", async () => {\n      const address = orderPage.shippingAddressSummary\n      await expect(address).toContainText(\"First\")\n      await expect(address).toContainText(\"Last\")\n      await expect(address).toContainText(\"123 Fake street\")\n      await expect(address).toContainText(\"80010\")\n      await expect(address).toContainText(\"Denver\")\n      await expect(address).toContainText(\"US\")\n\n      const contact = orderPage.shippingContactSummary\n      await expect(contact).toContainText(\"test@example.com\")\n      await expect(contact).toContainText(\"3031112222\")\n\n      const method = orderPage.shippingMethodSummary\n      await expect(method).toContainText(\"FakeEx Standard\")\n    })\n  })\n\n  test(\"Editing checkout steps works as expected\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Navigate to a product page\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.highlight()\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the product to the cart and goto checkout\", async () => {\n      await productPage.selectOption(\"M\")\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.navCartLink.click()\n      await productPage.cartDropdown.goToCartButton.click()\n      await cartPage.container.waitFor({ state: \"visible\" })\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Enter in the first step of the checkout process\", async () => {\n      await test.step(\"Enter in the shipping address info\", async () => {\n        await checkoutPage.shippingFirstNameInput.fill(\"First\")\n        await checkoutPage.shippingLastNameInput.fill(\"Last\")\n        await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n        await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n        await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n        await checkoutPage.shippingCityInput.fill(\"Denver\")\n        await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n        await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n      })\n\n      await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n        await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n        await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n        await checkoutPage.billingAddressCheckbox.uncheck()\n      })\n\n      await test.step(\"Enter in the billing address info\", async () => {\n        await checkoutPage.billingFirstNameInput.fill(\"First\")\n        await checkoutPage.billingLastNameInput.fill(\"Last\")\n        await checkoutPage.billingCompanyInput.fill(\"MyCorp\")\n        await checkoutPage.billingAddressInput.fill(\"123 Fake street\")\n        await checkoutPage.billingPostalInput.fill(\"80010\")\n        await checkoutPage.billingCityInput.fill(\"Denver\")\n        await checkoutPage.billingProvinceInput.fill(\"Colorado\")\n        await checkoutPage.billingCountrySelect.selectOption(\"United States\")\n        await checkoutPage.submitAddressButton.click()\n      })\n    })\n\n    await test.step(\"Submit the delivery and payment options\", async () => {\n      await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n      await checkoutPage.submitDeliveryOptionButton.click()\n      await checkoutPage.submitPaymentButton.click()\n    })\n\n    await test.step(\"Edit the shipping info\", async () => {\n      await checkoutPage.editAddressButton.click()\n      await test.step(\"Edit the shipping address\", async () => {\n        await checkoutPage.shippingFirstNameInput.fill(\"First1\")\n        await checkoutPage.shippingLastNameInput.fill(\"Last1\")\n        await checkoutPage.shippingCompanyInput.fill(\"MeCorp\")\n        await checkoutPage.shippingAddressInput.fill(\"123 Fake Road\")\n        await checkoutPage.shippingPostalCodeInput.fill(\"80011\")\n        await checkoutPage.shippingCityInput.fill(\"Donver\")\n        await checkoutPage.shippingProvinceInput.fill(\"CO\")\n        await checkoutPage.shippingCountrySelect.selectOption(\"Canada\")\n      })\n\n      await test.step(\"Edit the shipping contact info\", async () => {\n        await checkoutPage.shippingEmailInput.fill(\"tester@example.com\")\n        await checkoutPage.shippingPhoneInput.fill(\"3231112222\")\n      })\n\n      await test.step(\"Edit the billing info\", async () => {\n        await checkoutPage.billingFirstNameInput.fill(\"Farst\")\n        await checkoutPage.billingLastNameInput.fill(\"List\")\n        await checkoutPage.billingCompanyInput.fill(\"MistCorp\")\n        await checkoutPage.billingAddressInput.fill(\"321 Fake street\")\n        await checkoutPage.billingPostalInput.fill(\"80110\")\n        await checkoutPage.billingCityInput.fill(\"Denvur\")\n        await checkoutPage.billingProvinceInput.fill(\"AB\")\n        await checkoutPage.billingCountrySelect.selectOption(\"Canada\")\n      })\n      await checkoutPage.submitAddressButton.click()\n    })\n\n    await test.step(\"Make sure the edits are reflected in the container\", async () => {\n      await test.step(\"Check shipping address summary\", async () => {\n        const shippingColumn = checkoutPage.shippingAddressSummary\n        await expect(shippingColumn).toContainText(\"First1\")\n        await expect(shippingColumn).toContainText(\"Last1\")\n        await expect(shippingColumn).toContainText(\"123 Fake Road\")\n        await expect(shippingColumn).toContainText(\"80011\")\n        await expect(shippingColumn).toContainText(\"Donver\")\n        await expect(shippingColumn).toContainText(\"CA\")\n      })\n\n      await test.step(\"Check shipping contact summary\", async () => {\n        const contactColumn = checkoutPage.shippingContactSummary\n        await expect(contactColumn).toContainText(\"tester@example.com\")\n        await expect(contactColumn).toContainText(\"3231112222\")\n      })\n\n      await test.step(\"Check billing summary\", async () => {\n        const billingColumn = checkoutPage.billingAddressSummary\n        await expect(billingColumn).toContainText(\"Farst\")\n        await expect(billingColumn).toContainText(\"List\")\n        await expect(billingColumn).toContainText(\"321 Fake street\")\n        await expect(billingColumn).toContainText(\"Denvur\")\n        await expect(billingColumn).toContainText(\"CA\")\n      })\n    })\n  })\n\n  test(\"Shipping info saved is filled back into the forms after clicking edit\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Navigate to a product page\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.highlight()\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the product to the cart and goto checkout\", async () => {\n      await productPage.selectOption(\"M\")\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.navCartLink.click()\n      await productPage.cartDropdown.goToCartButton.click()\n      await cartPage.container.waitFor({ state: \"visible\" })\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Enter in the first step of the checkout process\", async () => {\n      await test.step(\"Enter in the shipping address info\", async () => {\n        await checkoutPage.shippingFirstNameInput.fill(\"First\")\n        await checkoutPage.shippingLastNameInput.fill(\"Last\")\n        await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n        await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n        await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n        await checkoutPage.shippingCityInput.fill(\"Denver\")\n        await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n        await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n      })\n\n      await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n        await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n        await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n        await checkoutPage.billingAddressCheckbox.uncheck()\n      })\n\n      await test.step(\"Enter in the billing address info\", async () => {\n        await checkoutPage.billingFirstNameInput.fill(\"First\")\n        await checkoutPage.billingLastNameInput.fill(\"Last\")\n        await checkoutPage.billingCompanyInput.fill(\"MyCorp\")\n        await checkoutPage.billingAddressInput.fill(\"123 Fake street\")\n        await checkoutPage.billingPostalInput.fill(\"80010\")\n        await checkoutPage.billingCityInput.fill(\"Denver\")\n        await checkoutPage.billingProvinceInput.fill(\"Colorado\")\n        await checkoutPage.billingCountrySelect.selectOption(\"United States\")\n        await checkoutPage.submitAddressButton.click()\n      })\n    })\n\n    await test.step(\"Click the edit address form and ensure the fields are filled correctly\", async () => {\n      await checkoutPage.editAddressButton.click()\n      await test.step(\"Check the shipping address\", async () => {\n        await expect(checkoutPage.shippingFirstNameInput).toHaveValue(\"First\")\n        await expect(checkoutPage.shippingLastNameInput).toHaveValue(\"Last\")\n        await expect(checkoutPage.shippingCompanyInput).toHaveValue(\"MyCorp\")\n        await expect(checkoutPage.shippingAddressInput).toHaveValue(\n          \"123 Fake street\"\n        )\n        await expect(checkoutPage.shippingPostalCodeInput).toHaveValue(\"80010\")\n        await expect(checkoutPage.shippingCityInput).toHaveValue(\"Denver\")\n        await expect(checkoutPage.shippingProvinceInput).toHaveValue(\"Colorado\")\n        await expect(checkoutPage.shippingCountrySelect).toHaveValue(\"us\")\n      })\n\n      await test.step(\"Check the shipping contact\", async () => {\n        await expect(checkoutPage.shippingEmailInput).toHaveValue(\n          \"test@example.com\"\n        )\n        await expect(checkoutPage.shippingPhoneInput).toHaveValue(\"3031112222\")\n      })\n\n      await test.step(\"Check the billing address\", async () => {\n        await expect(checkoutPage.billingFirstNameInput).toHaveValue(\"First\")\n        await expect(checkoutPage.billingLastNameInput).toHaveValue(\"Last\")\n        await expect(checkoutPage.billingCompanyInput).toHaveValue(\"MyCorp\")\n        await expect(checkoutPage.billingAddressInput).toHaveValue(\n          \"123 Fake street\"\n        )\n        await expect(checkoutPage.billingPostalInput).toHaveValue(\"80010\")\n        await expect(checkoutPage.billingCityInput).toHaveValue(\"Denver\")\n        await expect(checkoutPage.billingProvinceInput).toHaveValue(\"Colorado\")\n        await expect(checkoutPage.billingCountrySelect).toHaveValue(\"us\")\n      })\n    })\n\n    await test.step(\"Set the billing info to the same as checked and perform checks\", async () => {\n      await checkoutPage.billingAddressCheckbox.check()\n      await checkoutPage.submitAddressButton.click()\n      await checkoutPage.editAddressButton.click()\n      await expect(checkoutPage.billingAddressCheckbox).toBeChecked()\n    })\n  })\n\n  test(\"Shipping info in the checkout page is correctly reflected in the summary\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Navigate to a product page\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.highlight()\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the product to the cart and goto checkout\", async () => {\n      await productPage.selectOption(\"M\")\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.navCartLink.click()\n      await productPage.cartDropdown.goToCartButton.click()\n      await cartPage.container.waitFor({ state: \"visible\" })\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Enter in the first step of the checkout process\", async () => {\n      await test.step(\"Enter in the shipping address info\", async () => {\n        await checkoutPage.shippingFirstNameInput.fill(\"First\")\n        await checkoutPage.shippingLastNameInput.fill(\"Last\")\n        await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n        await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n        await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n        await checkoutPage.shippingCityInput.fill(\"Denver\")\n        await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n        await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n      })\n\n      await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n        await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n        await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n        await checkoutPage.billingAddressCheckbox.uncheck()\n      })\n\n      await test.step(\"Enter in the billing address info\", async () => {\n        await checkoutPage.billingFirstNameInput.fill(\"First\")\n        await checkoutPage.billingLastNameInput.fill(\"Last\")\n        await checkoutPage.billingCompanyInput.fill(\"MyCorp\")\n        await checkoutPage.billingAddressInput.fill(\"123 Fake street\")\n        await checkoutPage.billingPostalInput.fill(\"80010\")\n        await checkoutPage.billingCityInput.fill(\"Denver\")\n        await checkoutPage.billingProvinceInput.fill(\"Colorado\")\n        await checkoutPage.billingCountrySelect.selectOption(\"United States\")\n        await checkoutPage.submitAddressButton.click()\n      })\n    })\n\n    await test.step(\"Ensure the shipping column reflects the entered data\", async () => {\n      const shippingColumn = checkoutPage.shippingAddressSummary\n      await expect(shippingColumn).toContainText(\"First\")\n      await expect(shippingColumn).toContainText(\"Last\")\n      await expect(shippingColumn).toContainText(\"123 Fake street\")\n      await expect(shippingColumn).toContainText(\"80010\")\n      await expect(shippingColumn).toContainText(\"Denver\")\n      await expect(shippingColumn).toContainText(\"US\")\n    })\n\n    await test.step(\"Ensure the contact column reflects the entered data\", async () => {\n      const contactColumn = checkoutPage.shippingContactSummary\n      await expect(contactColumn).toContainText(\"test@example.com\")\n      await expect(contactColumn).toContainText(\"3031112222\")\n    })\n\n    await test.step(\"Ensure the billing column reflects the entered data\", async () => {\n      const billingColumn = checkoutPage.billingAddressSummary\n      await expect(billingColumn).toContainText(\"First\")\n      await expect(billingColumn).toContainText(\"Last\")\n      await expect(billingColumn).toContainText(\"123 Fake street\")\n      await expect(billingColumn).toContainText(\"Denver\")\n      await expect(billingColumn).toContainText(\"US\")\n    })\n\n    await test.step(\"Edit the billing info so it is the same as the billing address\", async () => {\n      await checkoutPage.editAddressButton.click()\n      await checkoutPage.billingAddressCheckbox.check()\n      await checkoutPage.submitAddressButton.click()\n      const billingColumn = checkoutPage.billingAddressSummary\n      await expect(billingColumn).toContainText(\"are the same.\")\n    })\n  })\n\n  test(\"Entering checkout, leaving, then returning takes you back to the correct checkout spot\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Navigate to a product page\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.highlight()\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Add the product to the cart and goto checkout\", async () => {\n      await productPage.selectOption(\"M\")\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.navCartLink.click()\n      await productPage.cartDropdown.goToCartButton.click()\n      await cartPage.container.waitFor({ state: \"visible\" })\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Navigate away and back to the checkout page\", async () => {\n      await checkoutPage.backToCartLink.click()\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      await expect(checkoutPage.submitAddressButton).toBeVisible()\n    })\n\n    await test.step(\"Enter in the first step of the checkout process\", async () => {\n      await test.step(\"Enter in the shipping address info\", async () => {\n        await checkoutPage.shippingFirstNameInput.fill(\"First\")\n        await checkoutPage.shippingLastNameInput.fill(\"Last\")\n        await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n        await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n        await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n        await checkoutPage.shippingCityInput.fill(\"Denver\")\n        await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n        await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n      })\n\n      await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n        await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n        await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n        await checkoutPage.billingAddressCheckbox.uncheck()\n      })\n\n      await test.step(\"Enter in the billing address info\", async () => {\n        await checkoutPage.billingFirstNameInput.fill(\"First\")\n        await checkoutPage.billingLastNameInput.fill(\"Last\")\n        await checkoutPage.billingCompanyInput.fill(\"MyCorp\")\n        await checkoutPage.billingAddressInput.fill(\"123 Fake street\")\n        await checkoutPage.billingPostalInput.fill(\"80010\")\n        await checkoutPage.billingCityInput.fill(\"Denver\")\n        await checkoutPage.billingProvinceInput.fill(\"Colorado\")\n        await checkoutPage.billingCountrySelect.selectOption(\"United States\")\n      })\n      await checkoutPage.submitAddressButton.click()\n      await checkoutPage.deliveryOptionRadio\n        .first()\n        .waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Navigate away and back to the checkout page\", async () => {\n      await checkoutPage.backToCartLink.click()\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      await expect(checkoutPage.submitDeliveryOptionButton).toBeVisible()\n    })\n\n    await test.step(\"Submit the delivery choice and navigate back and forth\", async () => {\n      await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n      await checkoutPage.submitDeliveryOptionButton.click()\n      await checkoutPage.submitPaymentButton.waitFor({ state: \"visible\" })\n      await checkoutPage.backToCartLink.click()\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      await expect(checkoutPage.submitPaymentButton).toBeVisible()\n    })\n\n    await test.step(\"Submit the payment info and navigate back and forth\", async () => {\n      await checkoutPage.submitPaymentButton.click()\n      await checkoutPage.submitOrderButton.waitFor({ state: \"visible\" })\n      await checkoutPage.backToCartLink.click()\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      await expect(checkoutPage.submitPaymentButton).toBeVisible()\n    })\n\n    await test.step(\"Click edit on the shipping info and navigate back and forth\", async () => {\n      await checkoutPage.editAddressButton.click()\n      await checkoutPage.backToCartLink.click()\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      await expect(checkoutPage.submitPaymentButton).toBeVisible()\n    })\n\n    await test.step(\"Click edit on the shipping choice and navigate back and forth\", async () => {\n      await checkoutPage.editDeliveryButton.click()\n      await checkoutPage.backToCartLink.click()\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      await expect(checkoutPage.submitPaymentButton).toBeVisible()\n    })\n  })\n\n  test(\"Verify the prices carries over to checkout\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Navigate to the product page - go to the store page and click on the Sweatshirt product\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    let sweatshirtSmallPrice = 0\n    let sweatshirtMediumPrice = 0\n    await test.step(\"Add the sweatshirts to the cart\", async () => {\n      await productPage.selectOption(\"S\")\n      sweatshirtSmallPrice = getFloatValue(\n        (await productPage.productPrice.getAttribute(\"data-value\")) || \"0\"\n      )\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.close()\n      await productPage.selectOption(\"M\")\n      sweatshirtMediumPrice = getFloatValue(\n        (await productPage.productPrice.getAttribute(\"data-value\")) || \"0\"\n      )\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.close()\n    })\n\n    await test.step(\"Navigate to another product - Sweatpants\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatpants\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n    })\n\n    let sweatpantsSmallPrice = 0\n    await test.step(\"Add the small sweatpants to the cart\", async () => {\n      await productPage.selectOption(\"S\")\n      sweatpantsSmallPrice = getFloatValue(\n        (await productPage.productPrice.getAttribute(\"data-value\")) || \"0\"\n      )\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.close()\n      await productPage.selectOption(\"S\")\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.goToCartButton.click()\n      await productPage.cartDropdown.close()\n      await cartPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Verify the price in the cart is the expected value\", async () => {\n      const total = getFloatValue(\n        (await cartPage.cartSubtotal.getAttribute(\"data-value\")) || \"0\"\n      )\n      const calculatedTotal =\n        2 * sweatpantsSmallPrice + sweatshirtSmallPrice + sweatshirtMediumPrice\n      expect(compareFloats(total, calculatedTotal)).toBe(0)\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n    })\n\n    await test.step(\"Go to checkout and verify the price in the checkout is the expected value\", async () => {\n      const total = getFloatValue(\n        (await checkoutPage.cartSubtotal.getAttribute(\"data-value\")) || \"0\"\n      )\n      const calculatedTotal =\n        2 * sweatpantsSmallPrice + sweatshirtSmallPrice + sweatpantsSmallPrice\n      expect(compareFloats(total, calculatedTotal)).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "storefront/e2e/tests/public/discount.spec.ts",
    "content": "import { seedDiscount, seedUser } from \"../../data/seed\"\nimport { test, expect } from \"../../index\"\n\ntest.describe(\"Discount tests\", async () => {\n  let discount = {\n    id: \"\",\n    code: \"\",\n    rule_id: \"\",\n    amount: 0,\n  }\n  test.beforeEach(async () => {\n    discount = await seedDiscount()\n  })\n\n  test(\"Make sure discount works during transaction\", async ({\n    cartPage,\n    checkoutPage,\n    orderPage,\n    productPage,\n    storePage,\n  }) => {\n    let cartSubtotal = 0\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n        cartSubtotal = Number(\n          (await cartPage.cartTotal.getAttribute(\"data-value\")) || \"\"\n        )\n      })\n      await test.step(\"Navigate to the checkout page\", async () => {\n        await cartPage.checkoutButton.click()\n        await checkoutPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n\n    await test.step(\"Enter in the discount and assert value works\", async () => {\n      await checkoutPage.discountButton.click()\n      await expect(checkoutPage.discountInput).toBeVisible()\n      await checkoutPage.discountInput.fill(discount.code)\n      await checkoutPage.discountApplyButton.click()\n      const paymentDiscount = await checkoutPage.getDiscount(discount.code)\n      await expect(paymentDiscount.locator).toBeVisible()\n      await expect(paymentDiscount.code).toHaveText(discount.code)\n      expect(paymentDiscount.amountValue).toBe(discount.amount.toString())\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n    })\n\n    let shippingTotal = 0\n    await test.step(\"Go through checkout process\", async () => {\n      await test.step(\"Enter in the first step of the checkout process\", async () => {\n        await test.step(\"Enter in the shipping address info\", async () => {\n          await checkoutPage.shippingFirstNameInput.fill(\"First\")\n          await checkoutPage.shippingLastNameInput.fill(\"Last\")\n          await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n          await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n          await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n          await checkoutPage.shippingCityInput.fill(\"Denver\")\n          await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n          await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n        })\n\n        await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n          await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n          await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n          await checkoutPage.submitAddressButton.click()\n        })\n      })\n\n      await test.step(\"Complete the rest of the payment process\", async () => {\n        await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n        await checkoutPage.submitDeliveryOptionButton.click()\n        shippingTotal = Number(\n          (await checkoutPage.cartShipping.getAttribute(\"data-value\")) || \"0\"\n        )\n        await checkoutPage.submitPaymentButton.click()\n      })\n\n      await test.step(\"Make sure the cart total is the expected value after selecting shipping\", async () => {\n        expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n          (cartSubtotal - discount.amount + shippingTotal).toString()\n        )\n      })\n\n      await test.step(\"Finish completing the order\", async () => {\n        await checkoutPage.submitOrderButton.click()\n        await orderPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n    const cartTotal = Number(cartSubtotal) + Number(shippingTotal)\n\n    await test.step(\"Assert the order page shows the total was 0\", async () => {\n      expect(await orderPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartTotal - discount.amount).toString()\n      )\n      expect(await orderPage.cartSubtotal.getAttribute(\"data-value\")).toBe(\n        cartSubtotal.toString()\n      )\n      expect(await orderPage.cartDiscount.getAttribute(\"data-value\")).toBe(\n        discount.amount.toString()\n      )\n    })\n  })\n\n  test(\"Make sure discount can be used when entered in from cart\", async ({\n    cartPage,\n    checkoutPage,\n    orderPage,\n    productPage,\n    storePage,\n  }) => {\n    let cartSubtotal = 0\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n        cartSubtotal = Number(\n          (await cartPage.cartTotal.getAttribute(\"data-value\")) || \"\"\n        )\n      })\n    })\n\n    await test.step(\"Enter in the discount and assert value works\", async () => {\n      await cartPage.discountButton.click()\n      await expect(cartPage.discountInput).toBeVisible()\n      await cartPage.discountInput.fill(discount.code)\n      await cartPage.discountApplyButton.click()\n      const paymentDiscount = await cartPage.getDiscount(discount.code)\n      await expect(paymentDiscount.locator).toBeVisible()\n      await expect(paymentDiscount.code).toHaveText(discount.code)\n      expect(paymentDiscount.amountValue).toBe(discount.amount.toString())\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n    })\n\n    await test.step(\"Go to checkout and assert the value is still discounted\", async () => {\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n    })\n\n    let shippingTotal = 0\n    await test.step(\"Go through checkout process\", async () => {\n      await test.step(\"Enter in the first step of the checkout process\", async () => {\n        await test.step(\"Enter in the shipping address info\", async () => {\n          await checkoutPage.shippingFirstNameInput.fill(\"First\")\n          await checkoutPage.shippingLastNameInput.fill(\"Last\")\n          await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n          await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n          await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n          await checkoutPage.shippingCityInput.fill(\"Denver\")\n          await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n          await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n        })\n\n        await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n          await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n          await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n          await checkoutPage.submitAddressButton.click()\n        })\n      })\n\n      await test.step(\"Complete the rest of the payment process\", async () => {\n        await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n        await checkoutPage.submitDeliveryOptionButton.click()\n        shippingTotal = Number(\n          (await checkoutPage.cartShipping.getAttribute(\"data-value\")) || \"0\"\n        )\n        await checkoutPage.submitPaymentButton.click()\n        await checkoutPage.submitOrderButton.click()\n        await orderPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n    const cartTotal = Number(cartSubtotal) + Number(shippingTotal)\n\n    await test.step(\"Assert the order page shows the total was 0\", async () => {\n      expect(await orderPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartTotal - discount.amount).toString()\n      )\n      expect(await orderPage.cartSubtotal.getAttribute(\"data-value\")).toBe(\n        cartSubtotal.toString()\n      )\n      expect(await orderPage.cartDiscount.getAttribute(\"data-value\")).toBe(\n        discount.amount.toString()\n      )\n    })\n  })\n\n  test(\"Ensure adding and removing a discout does not impact checkout amount\", async ({\n    cartPage,\n    checkoutPage,\n    orderPage,\n    productPage,\n    storePage,\n  }) => {\n    let cartSubtotal = 0\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n        cartSubtotal = Number(\n          (await cartPage.cartTotal.getAttribute(\"data-value\")) || \"\"\n        )\n      })\n    })\n\n    await test.step(\"Enter in the discount and assert value works\", async () => {\n      await cartPage.discountButton.click()\n      await expect(cartPage.discountInput).toBeVisible()\n      await cartPage.discountInput.fill(discount.code)\n      await cartPage.discountApplyButton.click()\n      const paymentDiscount = await cartPage.getDiscount(discount.code)\n      await expect(paymentDiscount.locator).toBeVisible()\n      await expect(paymentDiscount.code).toHaveText(discount.code)\n      expect(paymentDiscount.amountValue).toBe(discount.amount.toString())\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n    })\n\n    await test.step(\"Go to checkout and assert the value is still discounted\", async () => {\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n      const paymentDiscount = await checkoutPage.getDiscount(discount.code)\n      await paymentDiscount.removeButton.click()\n      await expect(paymentDiscount.locator).not.toBeVisible()\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).not.toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n    })\n\n    let shippingTotal = \"\"\n    await test.step(\"Go through checkout process\", async () => {\n      await test.step(\"Enter in the first step of the checkout process\", async () => {\n        await test.step(\"Enter in the shipping address info\", async () => {\n          await checkoutPage.shippingFirstNameInput.fill(\"First\")\n          await checkoutPage.shippingLastNameInput.fill(\"Last\")\n          await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n          await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n          await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n          await checkoutPage.shippingCityInput.fill(\"Denver\")\n          await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n          await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n        })\n\n        await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n          await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n          await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n          await checkoutPage.submitAddressButton.click()\n        })\n      })\n\n      await test.step(\"Complete the rest of the payment process\", async () => {\n        await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n        await checkoutPage.submitDeliveryOptionButton.click()\n        shippingTotal =\n          (await checkoutPage.cartShipping.getAttribute(\"data-value\")) || \"\"\n        await checkoutPage.submitPaymentButton.click()\n        await checkoutPage.submitOrderButton.click()\n        await orderPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n    const cartTotal = (Number(cartSubtotal) + Number(shippingTotal)).toString()\n\n    await test.step(\"Assert the order page shows the total was not discounted\", async () => {\n      expect(await orderPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        cartTotal\n      )\n      expect(await orderPage.cartSubtotal.getAttribute(\"data-value\")).toBe(\n        cartSubtotal.toString()\n      )\n    })\n  })\n\n  test(\"Make sure a fake discount displays an error message on the cart page\", async ({\n    cartPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n      })\n    })\n    await test.step(\"Enter in the fake discount\", async () => {\n      await cartPage.discountButton.click()\n      await expect(cartPage.discountInput).toBeVisible()\n      await cartPage.discountInput.fill(\"__FAKE_DISCOUNT_DNE_1111111\")\n      await cartPage.discountApplyButton.click()\n      await expect(cartPage.discountErrorMessage).toBeVisible()\n    })\n  })\n\n  test(\"Make sure a fake discount displays an error message on the checkout page\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.highlight()\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n        await cartPage.checkoutButton.click()\n        await checkoutPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n    await test.step(\"Enter in the fake discount\", async () => {\n      await checkoutPage.discountButton.click()\n      await expect(checkoutPage.discountInput).toBeVisible()\n      await checkoutPage.discountInput.fill(\"__FAKE_DISCOUNT_DNE_1111111\")\n      await checkoutPage.discountApplyButton.click()\n      await expect(checkoutPage.discountErrorMessage).toBeVisible()\n    })\n  })\n\n  test(\"Adding a discount and then accessing the cart at a later point keeps the discount amount\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    let cartSubtotal = 0\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n        cartSubtotal = Number(\n          (await cartPage.cartTotal.getAttribute(\"data-value\")) || \"\"\n        )\n      })\n    })\n\n    await test.step(\"Enter in the giftcard and assert value works\", async () => {\n      await cartPage.discountButton.click()\n      await cartPage.discountInput.fill(discount.code)\n      await cartPage.discountApplyButton.click()\n      const paymentDiscount = await cartPage.getDiscount(discount.code)\n      expect(paymentDiscount.amountValue).toBe(discount.amount.toString())\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n    })\n\n    await test.step(\"Navigate away from the cart page and return to it\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatpants\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n      await cartPage.goto()\n      await cartPage.cartDropdown.close()\n    })\n\n    await test.step(\"Verify the giftcard is still on the cart page\", async () => {\n      const paymentDiscount = await cartPage.getDiscount(discount.code)\n      await expect(paymentDiscount.locator).toBeVisible()\n      await expect(paymentDiscount.code).toContainText(discount.code)\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n    })\n\n    await test.step(\"Verify the giftcard is still on the checkout page\", async () => {\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      const paymentDiscount = await checkoutPage.getDiscount(discount.code)\n      await expect(paymentDiscount.locator).toBeVisible()\n      await expect(paymentDiscount.code).toContainText(discount.code)\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n      expect(paymentDiscount.amountValue).toBe(discount.amount.toString())\n    })\n  })\n\n  test(\"Adding a discount and then adding another item to the cart keeps the discount\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    let cartSubtotal = 0\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n        cartSubtotal = Number(\n          (await cartPage.cartTotal.getAttribute(\"data-value\")) || \"\"\n        )\n      })\n    })\n\n    await test.step(\"Enter in the giftcard and assert value works\", async () => {\n      await cartPage.discountButton.click()\n      await cartPage.discountInput.fill(discount.code)\n      await cartPage.discountApplyButton.click()\n      const paymentDiscount = await cartPage.getDiscount(discount.code)\n      expect(paymentDiscount.amountValue).toBe(discount.amount.toString())\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n    })\n\n    await test.step(\"Navigate away from the cart page and return to it\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatpants\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n      await productPage.selectOption(\"XL\")\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.close()\n      await cartPage.goto()\n      cartSubtotal = Number(\n        (await cartPage.cartSubtotal.getAttribute(\"data-value\")) || \"\"\n      )\n    })\n\n    await test.step(\"Verify the giftcard is still on the cart page\", async () => {\n      const paymentDiscount = await cartPage.getDiscount(discount.code)\n      await expect(paymentDiscount.locator).toBeVisible()\n      await expect(paymentDiscount.code).toContainText(discount.code)\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n    })\n\n    await test.step(\"Verify the giftcard is still on the checkout page\", async () => {\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      const paymentDiscount = await checkoutPage.getDiscount(discount.code)\n      await expect(paymentDiscount.locator).toBeVisible()\n      await expect(paymentDiscount.code).toContainText(discount.code)\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        (cartSubtotal - discount.amount).toString()\n      )\n      expect(paymentDiscount.amountValue).toBe(discount.amount.toString())\n    })\n  })\n})\n"
  },
  {
    "path": "storefront/e2e/tests/public/giftcard.spec.ts",
    "content": "import { first } from \"lodash\"\nimport { seedGiftcard, seedUser } from \"../../data/seed\"\nimport { test, expect } from \"../../index\"\n\ntest.describe(\"Gift card tests\", async () => {\n  let giftcard = {\n    id: \"\",\n    code: \"\",\n    value: 0,\n    amount: \"0\",\n    balance: \"\",\n  }\n  test.beforeEach(async () => {\n    giftcard = await seedGiftcard()\n  })\n\n  test(\"Make sure giftcard can be used to pay for transaction\", async ({\n    cartPage,\n    checkoutPage,\n    orderPage,\n    productPage,\n    storePage,\n  }) => {\n    let cartSubtotal = \"\"\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n        cartSubtotal =\n          (await cartPage.cartTotal.getAttribute(\"data-value\")) || \"\"\n      })\n      await test.step(\"Navigate to the checkout page\", async () => {\n        await cartPage.checkoutButton.click()\n        await checkoutPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n\n    await test.step(\"Enter in the giftcard and assert value works\", async () => {\n      await checkoutPage.discountButton.click()\n      await expect(checkoutPage.discountInput).toBeVisible()\n      await checkoutPage.discountInput.fill(giftcard.code)\n      await checkoutPage.discountApplyButton.click()\n      const paymentGiftcard = await checkoutPage.getGiftCard(giftcard.code)\n      await expect(paymentGiftcard.locator).toBeVisible()\n      await expect(paymentGiftcard.code).toHaveText(giftcard.code)\n      expect(paymentGiftcard.amountValue).toBe(giftcard.amount)\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n    })\n\n    let shippingTotal = \"\"\n    await test.step(\"Go through checkout process\", async () => {\n      await test.step(\"Enter in the first step of the checkout process\", async () => {\n        await test.step(\"Enter in the shipping address info\", async () => {\n          await checkoutPage.shippingFirstNameInput.fill(\"First\")\n          await checkoutPage.shippingLastNameInput.fill(\"Last\")\n          await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n          await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n          await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n          await checkoutPage.shippingCityInput.fill(\"Denver\")\n          await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n          await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n        })\n\n        await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n          await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n          await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n          await checkoutPage.submitAddressButton.click()\n        })\n      })\n\n      await test.step(\"Complete the rest of the payment process\", async () => {\n        await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n        await checkoutPage.submitDeliveryOptionButton.click()\n        shippingTotal =\n          (await checkoutPage.cartShipping.getAttribute(\"data-value\")) || \"\"\n        await checkoutPage.submitPaymentButton.click()\n      })\n\n      await test.step(\"Make sure the giftcard still has the total as zero after selecting shipping\", async () => {\n        expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n          \"0\"\n        )\n      })\n\n      await test.step(\"Finish completing the order\", async () => {\n        await checkoutPage.submitOrderButton.click()\n        await orderPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n    const cartTotal = (Number(cartSubtotal) + Number(shippingTotal)).toString()\n\n    await test.step(\"Assert the order page shows the total was 0\", async () => {\n      expect(await orderPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n      expect(await orderPage.cartSubtotal.getAttribute(\"data-value\")).toBe(\n        cartSubtotal\n      )\n      expect(\n        await orderPage.cartGiftCardAmount.getAttribute(\"data-value\")\n      ).toBe(cartTotal)\n    })\n  })\n\n  test(\"Make sure giftcard can be used when entered in from cart\", async ({\n    cartPage,\n    checkoutPage,\n    orderPage,\n    productPage,\n    storePage,\n  }) => {\n    let cartSubtotal = \"\"\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n        cartSubtotal =\n          (await cartPage.cartTotal.getAttribute(\"data-value\")) || \"\"\n      })\n    })\n\n    await test.step(\"Enter in the giftcard and assert value works\", async () => {\n      await cartPage.discountButton.click()\n      await expect(cartPage.discountInput).toBeVisible()\n      await cartPage.discountInput.fill(giftcard.code)\n      await cartPage.discountApplyButton.click()\n      const paymentGiftcard = await cartPage.getGiftCard(giftcard.code)\n      await expect(paymentGiftcard.locator).toBeVisible()\n      await expect(paymentGiftcard.code).toHaveText(giftcard.code)\n      expect(paymentGiftcard.amountValue).toBe(giftcard.amount)\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n    })\n\n    await test.step(\"Go to checkout and assert the value is still 0\", async () => {\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n    })\n\n    let shippingTotal = \"\"\n    await test.step(\"Go through checkout process\", async () => {\n      await test.step(\"Enter in the first step of the checkout process\", async () => {\n        await test.step(\"Enter in the shipping address info\", async () => {\n          await checkoutPage.shippingFirstNameInput.fill(\"First\")\n          await checkoutPage.shippingLastNameInput.fill(\"Last\")\n          await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n          await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n          await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n          await checkoutPage.shippingCityInput.fill(\"Denver\")\n          await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n          await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n        })\n\n        await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n          await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n          await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n          await checkoutPage.submitAddressButton.click()\n        })\n      })\n\n      await test.step(\"Complete the rest of the payment process\", async () => {\n        await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n        await checkoutPage.submitDeliveryOptionButton.click()\n        shippingTotal =\n          (await checkoutPage.cartShipping.getAttribute(\"data-value\")) || \"\"\n        await checkoutPage.submitPaymentButton.click()\n        await checkoutPage.submitOrderButton.click()\n        await orderPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n    const cartTotal = (Number(cartSubtotal) + Number(shippingTotal)).toString()\n\n    await test.step(\"Assert the order page shows the total was 0\", async () => {\n      expect(await orderPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n      expect(await orderPage.cartSubtotal.getAttribute(\"data-value\")).toBe(\n        cartSubtotal\n      )\n      expect(\n        await orderPage.cartGiftCardAmount.getAttribute(\"data-value\")\n      ).toBe(cartTotal)\n    })\n  })\n\n  test(\"Ensure adding and removing a giftcard does not impact checkout amount\", async ({\n    cartPage,\n    checkoutPage,\n    orderPage,\n    productPage,\n    storePage,\n  }) => {\n    let cartSubtotal = \"\"\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n        cartSubtotal =\n          (await cartPage.cartTotal.getAttribute(\"data-value\")) || \"\"\n      })\n    })\n\n    await test.step(\"Enter in the giftcard and assert value works\", async () => {\n      await cartPage.discountButton.click()\n      await expect(cartPage.discountInput).toBeVisible()\n      await cartPage.discountInput.fill(giftcard.code)\n      await cartPage.discountApplyButton.click()\n      const paymentGiftcard = await cartPage.getGiftCard(giftcard.code)\n      await expect(paymentGiftcard.locator).toBeVisible()\n      await expect(paymentGiftcard.code).toHaveText(giftcard.code)\n      expect(paymentGiftcard.amountValue).toBe(giftcard.amount)\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n    })\n\n    await test.step(\"Go to checkout and assert the value is still 0\", async () => {\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n      const paymentGiftcard = await checkoutPage.getGiftCard(giftcard.code)\n      await paymentGiftcard.removeButton.click()\n      await expect(paymentGiftcard.locator).not.toBeVisible()\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).not.toBe(\n        \"0\"\n      )\n    })\n\n    let shippingTotal = \"\"\n    await test.step(\"Go through checkout process\", async () => {\n      await test.step(\"Enter in the first step of the checkout process\", async () => {\n        await test.step(\"Enter in the shipping address info\", async () => {\n          await checkoutPage.shippingFirstNameInput.fill(\"First\")\n          await checkoutPage.shippingLastNameInput.fill(\"Last\")\n          await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n          await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n          await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n          await checkoutPage.shippingCityInput.fill(\"Denver\")\n          await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n          await checkoutPage.shippingCountrySelect.selectOption(\"United States\")\n        })\n\n        await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n          await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n          await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n          await checkoutPage.submitAddressButton.click()\n        })\n      })\n\n      await test.step(\"Complete the rest of the payment process\", async () => {\n        await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n        await checkoutPage.submitDeliveryOptionButton.click()\n        shippingTotal =\n          (await checkoutPage.cartShipping.getAttribute(\"data-value\")) || \"\"\n        await checkoutPage.submitPaymentButton.click()\n        await checkoutPage.submitOrderButton.click()\n        await orderPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n    const cartTotal = (Number(cartSubtotal) + Number(shippingTotal)).toString()\n\n    await test.step(\"Assert the order page shows the total was 0\", async () => {\n      expect(await orderPage.cartTotal.getAttribute(\"data-value\")).toBe(\n        cartTotal\n      )\n      expect(await orderPage.cartSubtotal.getAttribute(\"data-value\")).toBe(\n        cartSubtotal\n      )\n    })\n  })\n\n  test(\"Make sure a fake gift card displays an error message on the cart page\", async ({\n    cartPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n      })\n    })\n    await test.step(\"Enter in the fake giftcard\", async () => {\n      await cartPage.discountButton.click()\n      await expect(cartPage.discountInput).toBeVisible()\n      await cartPage.discountInput.fill(\"__FAKE_GIFT_CARD_DNE_1111111\")\n      await cartPage.discountApplyButton.click()\n      await expect(cartPage.discountErrorMessage).toBeVisible()\n    })\n  })\n\n  test(\"Make sure a fake gift card displays an error message on the checkout page\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.highlight()\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n        await cartPage.checkoutButton.click()\n        await checkoutPage.container.waitFor({ state: \"visible\" })\n      })\n    })\n    await test.step(\"Enter in the fake giftcard\", async () => {\n      await checkoutPage.discountButton.click()\n      await expect(checkoutPage.discountInput).toBeVisible()\n      await checkoutPage.discountInput.fill(\"__FAKE_GIFT_CARD_DNE_1111111\")\n      await checkoutPage.discountApplyButton.click()\n      await expect(checkoutPage.discountErrorMessage).toBeVisible()\n    })\n  })\n\n  test(\"Adding a giftcard and then accessing the cart at a later point keeps the giftcard amount\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n      })\n    })\n\n    await test.step(\"Enter in the giftcard and assert value works\", async () => {\n      await cartPage.discountButton.click()\n      await cartPage.discountInput.fill(giftcard.code)\n      await cartPage.discountApplyButton.click()\n      const paymentGiftcard = await cartPage.getGiftCard(giftcard.code)\n      expect(paymentGiftcard.amountValue).toBe(giftcard.amount)\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n    })\n\n    await test.step(\"Navigate away from the cart page and return to it\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatpants\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n      await cartPage.goto()\n    })\n\n    await test.step(\"Verify the giftcard is still on the cart page\", async () => {\n      const paymentGiftcard = await cartPage.getGiftCard(giftcard.code)\n      await expect(paymentGiftcard.locator).toBeVisible()\n      await expect(paymentGiftcard.code).toContainText(giftcard.code)\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n    })\n\n    await test.step(\"Verify the giftcard is still on the checkout page\", async () => {\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      const paymentGiftcard = await checkoutPage.getGiftCard(giftcard.code)\n      await expect(paymentGiftcard.locator).toBeVisible()\n      await expect(paymentGiftcard.code).toContainText(giftcard.code)\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n      expect(paymentGiftcard.amountValue).toBe(giftcard.amount)\n    })\n  })\n\n  test(\"Adding a giftcard and then adding another item to the cart keeps the giftcard\", async ({\n    cartPage,\n    checkoutPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n      })\n    })\n\n    await test.step(\"Enter in the giftcard and assert value works\", async () => {\n      await cartPage.discountButton.click()\n      await cartPage.discountInput.fill(giftcard.code)\n      await cartPage.discountApplyButton.click()\n      const paymentGiftcard = await cartPage.getGiftCard(giftcard.code)\n      expect(paymentGiftcard.amountValue).toBe(giftcard.amount)\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n    })\n\n    await test.step(\"Navigate away from the cart page and return to it\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatpants\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n      await productPage.selectOption(\"XL\")\n      await productPage.clickAddProduct()\n      await productPage.cartDropdown.close()\n      await cartPage.goto()\n    })\n\n    await test.step(\"Verify the giftcard is still on the cart page\", async () => {\n      const paymentGiftcard = await cartPage.getGiftCard(giftcard.code)\n      await expect(paymentGiftcard.locator).toBeVisible()\n      await expect(paymentGiftcard.code).toContainText(giftcard.code)\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n    })\n\n    await test.step(\"Verify the giftcard is still on the checkout page\", async () => {\n      await cartPage.checkoutButton.click()\n      await checkoutPage.container.waitFor({ state: \"visible\" })\n      const paymentGiftcard = await checkoutPage.getGiftCard(giftcard.code)\n      await expect(paymentGiftcard.locator).toBeVisible()\n      await expect(paymentGiftcard.code).toContainText(giftcard.code)\n      expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n      expect(paymentGiftcard.amountValue).toBe(giftcard.amount)\n    })\n  })\n\n  test(\"Applying a giftcard, deleting cookies, and then reapplying the giftcard works\", async ({\n    cartPage,\n    productPage,\n    storePage,\n  }) => {\n    await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        await storePage.goto()\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n      })\n    })\n\n    await test.step(\"Enter in the giftcard and assert value works\", async () => {\n      await cartPage.discountButton.click()\n      await cartPage.discountInput.fill(giftcard.code)\n      await cartPage.discountApplyButton.click()\n      const paymentGiftcard = await cartPage.getGiftCard(giftcard.code)\n      expect(paymentGiftcard.amountValue).toBe(giftcard.amount)\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n    })\n\n    await test.step(\"Navigate away from the cart page and delete cookies\", async () => {\n      const context = storePage.page.context()\n      await context.clearCookies()\n      await storePage.page.reload()\n      await storePage.goto()\n    })\n\n    await test.step(\"Recreate the cart\", async () => {\n      await test.step(\"Navigate to a product page\", async () => {\n        const product = await storePage.getProduct(\"Sweatshirt\")\n        await product.locator.click()\n        await productPage.container.waitFor({ state: \"visible\" })\n      })\n\n      await test.step(\"Add the product to the cart and goto checkout\", async () => {\n        await productPage.selectOption(\"M\")\n        await productPage.clickAddProduct()\n        await productPage.cartDropdown.navCartLink.click()\n        await productPage.cartDropdown.goToCartButton.click()\n        await cartPage.container.waitFor({ state: \"visible\" })\n        await cartPage.cartDropdown.close()\n      })\n    })\n    await test.step(\"Re-enter in the giftcard and assert value works\", async () => {\n      await cartPage.discountButton.click()\n      await cartPage.discountInput.fill(giftcard.code)\n      await cartPage.discountApplyButton.click()\n      const paymentGiftcard = await cartPage.getGiftCard(giftcard.code)\n      expect(paymentGiftcard.amountValue).toBe(giftcard.amount)\n      expect(await cartPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n    })\n  })\n\n  test(\"Gift card balance works as expected across transactions\", async ({\n    cartPage,\n    checkoutPage,\n    orderPage,\n    productPage,\n    storePage,\n  }) => {\n    let firstTransactionTotal = 0\n    await test.step(\"Complete first transaction using the giftcard\", async () => {\n      let cartSubtotal = \"\"\n      await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n        await test.step(\"Navigate to a product page\", async () => {\n          await storePage.goto()\n          const product = await storePage.getProduct(\"Sweatshirt\")\n          await product.locator.click()\n          await productPage.container.waitFor({ state: \"visible\" })\n        })\n\n        await test.step(\"Add the product to the cart and goto checkout\", async () => {\n          await productPage.selectOption(\"M\")\n          await productPage.clickAddProduct()\n          await productPage.cartDropdown.navCartLink.click()\n          await productPage.cartDropdown.goToCartButton.click()\n          await cartPage.container.waitFor({ state: \"visible\" })\n          await cartPage.cartDropdown.close()\n          cartSubtotal =\n            (await cartPage.cartTotal.getAttribute(\"data-value\")) || \"\"\n        })\n        await test.step(\"Navigate to the checkout page\", async () => {\n          await cartPage.checkoutButton.click()\n          await checkoutPage.container.waitFor({ state: \"visible\" })\n        })\n      })\n\n      await test.step(\"Enter in the giftcard and assert value works\", async () => {\n        await checkoutPage.discountButton.click()\n        await expect(checkoutPage.discountInput).toBeVisible()\n        await checkoutPage.discountInput.fill(giftcard.code)\n        await checkoutPage.discountApplyButton.click()\n        const paymentGiftcard = await checkoutPage.getGiftCard(giftcard.code)\n        await expect(paymentGiftcard.locator).toBeVisible()\n        await expect(paymentGiftcard.code).toHaveText(giftcard.code)\n        expect(paymentGiftcard.amountValue).toBe(giftcard.amount)\n        expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n          \"0\"\n        )\n      })\n\n      let shippingTotal = \"\"\n      await test.step(\"Go through checkout process\", async () => {\n        await test.step(\"Enter in the first step of the checkout process\", async () => {\n          await test.step(\"Enter in the shipping address info\", async () => {\n            await checkoutPage.shippingFirstNameInput.fill(\"First\")\n            await checkoutPage.shippingLastNameInput.fill(\"Last\")\n            await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n            await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n            await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n            await checkoutPage.shippingCityInput.fill(\"Denver\")\n            await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n            await checkoutPage.shippingCountrySelect.selectOption(\n              \"United States\"\n            )\n          })\n\n          await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n            await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n            await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n            await checkoutPage.submitAddressButton.click()\n          })\n        })\n\n        await test.step(\"Complete the rest of the payment process\", async () => {\n          await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n          await checkoutPage.submitDeliveryOptionButton.click()\n          shippingTotal =\n            (await checkoutPage.cartShipping.getAttribute(\"data-value\")) || \"\"\n          await checkoutPage.submitPaymentButton.click()\n        })\n\n        await test.step(\"Make sure the giftcard still has the total as zero after selecting shipping\", async () => {\n          expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n            \"0\"\n          )\n        })\n\n        await test.step(\"Finish completing the order\", async () => {\n          await checkoutPage.submitOrderButton.click()\n          await orderPage.container.waitFor({ state: \"visible\" })\n        })\n      })\n      const cartTotal = Number(cartSubtotal) + Number(shippingTotal)\n      firstTransactionTotal = cartTotal\n\n      await test.step(\"Assert the order page shows the total was 0\", async () => {\n        expect(await orderPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n        expect(await orderPage.cartSubtotal.getAttribute(\"data-value\")).toBe(\n          cartSubtotal\n        )\n        expect(\n          await orderPage.cartGiftCardAmount.getAttribute(\"data-value\")\n        ).toBe(cartTotal.toString())\n      })\n    })\n    await test.step(\"Setup the second transaction with the same giftcard\", async () => {\n      let cartSubtotal = \"\"\n      await test.step(\"Go through purchasing process, upto the cart page\", async () => {\n        await test.step(\"Navigate to a product page\", async () => {\n          await storePage.goto()\n          const product = await storePage.getProduct(\"Sweatshirt\")\n          await product.locator.click()\n          await productPage.container.waitFor({ state: \"visible\" })\n        })\n\n        await test.step(\"Add the product to the cart and goto checkout\", async () => {\n          await productPage.selectOption(\"M\")\n          await productPage.clickAddProduct()\n          await productPage.cartDropdown.navCartLink.click()\n          await productPage.cartDropdown.goToCartButton.click()\n          await cartPage.container.waitFor({ state: \"visible\" })\n          await cartPage.cartDropdown.close()\n          cartSubtotal =\n            (await cartPage.cartTotal.getAttribute(\"data-value\")) || \"\"\n        })\n        await test.step(\"Navigate to the checkout page\", async () => {\n          await cartPage.checkoutButton.click()\n          await checkoutPage.container.waitFor({ state: \"visible\" })\n        })\n      })\n\n      await test.step(\"Enter in the giftcard and assert value works\", async () => {\n        await checkoutPage.discountButton.click()\n        await expect(checkoutPage.discountInput).toBeVisible()\n        await checkoutPage.discountInput.fill(giftcard.code)\n        await checkoutPage.discountApplyButton.click()\n        const paymentGiftcard = await checkoutPage.getGiftCard(giftcard.code)\n        await expect(paymentGiftcard.locator).toBeVisible()\n        await expect(paymentGiftcard.code).toHaveText(giftcard.code)\n        expect(paymentGiftcard.amountValue).toBe(\n          (giftcard.value - firstTransactionTotal).toString()\n        )\n        expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n          \"0\"\n        )\n      })\n\n      let shippingTotal = \"\"\n      await test.step(\"Go through checkout process\", async () => {\n        await test.step(\"Enter in the first step of the checkout process\", async () => {\n          await test.step(\"Enter in the shipping address info\", async () => {\n            await checkoutPage.shippingFirstNameInput.fill(\"First\")\n            await checkoutPage.shippingLastNameInput.fill(\"Last\")\n            await checkoutPage.shippingCompanyInput.fill(\"MyCorp\")\n            await checkoutPage.shippingAddressInput.fill(\"123 Fake street\")\n            await checkoutPage.shippingPostalCodeInput.fill(\"80010\")\n            await checkoutPage.shippingCityInput.fill(\"Denver\")\n            await checkoutPage.shippingProvinceInput.fill(\"Colorado\")\n            await checkoutPage.shippingCountrySelect.selectOption(\n              \"United States\"\n            )\n          })\n\n          await test.step(\"Enter in the contact info and open the billing info form\", async () => {\n            await checkoutPage.shippingEmailInput.fill(\"test@example.com\")\n            await checkoutPage.shippingPhoneInput.fill(\"3031112222\")\n            await checkoutPage.submitAddressButton.click()\n          })\n        })\n\n        await test.step(\"Complete the rest of the payment process\", async () => {\n          await checkoutPage.selectDeliveryOption(\"FakeEx Standard\")\n          await checkoutPage.submitDeliveryOptionButton.click()\n          shippingTotal =\n            (await checkoutPage.cartShipping.getAttribute(\"data-value\")) || \"\"\n          await checkoutPage.submitPaymentButton.click()\n        })\n\n        await test.step(\"Make sure the giftcard still has the total as zero after selecting shipping\", async () => {\n          expect(await checkoutPage.cartTotal.getAttribute(\"data-value\")).toBe(\n            \"0\"\n          )\n        })\n\n        await test.step(\"Finish completing the order\", async () => {\n          await checkoutPage.submitOrderButton.click()\n          await orderPage.container.waitFor({ state: \"visible\" })\n        })\n      })\n      const cartTotal = (\n        Number(cartSubtotal) + Number(shippingTotal)\n      ).toString()\n\n      await test.step(\"Assert the order page shows the total was 0\", async () => {\n        expect(await orderPage.cartTotal.getAttribute(\"data-value\")).toBe(\"0\")\n        expect(await orderPage.cartSubtotal.getAttribute(\"data-value\")).toBe(\n          cartSubtotal\n        )\n        expect(\n          await orderPage.cartGiftCardAmount.getAttribute(\"data-value\")\n        ).toBe(cartTotal)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "storefront/e2e/tests/public/login.spec.ts",
    "content": "import { test, expect } from \"../../index\"\n\ntest.describe(\"Login Page functionality\", async () => {\n  test(\"access login page from nav menu and submit (partially) empty form\", async ({\n    loginPage,\n  }) => {\n    await loginPage.accountLink.click()\n    await loginPage.container.waitFor({ state: \"visible\" })\n    await loginPage.signInButton.click()\n    await expect(loginPage.emailInput).toBeFocused()\n\n    await loginPage.emailInput.fill(\"test-dne@example.com\")\n    await loginPage.signInButton.click()\n    await expect(loginPage.passwordInput).toBeFocused()\n  })\n\n  test(\"enter incorrect creds and verify error message\", async ({\n    loginPage,\n  }) => {\n    await loginPage.accountLink.click()\n    await loginPage.container.waitFor({ state: \"visible\" })\n    await loginPage.emailInput.fill(\"test-dne@example.com\")\n    await loginPage.passwordInput.fill(\"password\")\n    await loginPage.signInButton.click()\n    await expect(loginPage.errorMessage).toBeVisible()\n  })\n\n  test(\"enter different incorrect creds and verify error message\", async ({\n    loginPage,\n  }) => {\n    await loginPage.accountLink.click()\n    await loginPage.container.waitFor({ state: \"visible\" })\n    await loginPage.emailInput.fill(\"test@example.com\")\n    await loginPage.passwordInput.fill(\"passwrong\")\n    await loginPage.signInButton.click()\n    await expect(loginPage.errorMessage).toBeVisible()\n  })\n\n  test(\"successful login redirects to account page\", async ({\n    accountOverviewPage,\n    loginPage,\n  }) => {\n    await loginPage.accountLink.click()\n    await loginPage.container.waitFor({ state: \"visible\" })\n    await loginPage.emailInput.fill(\"test@example.com\")\n    await loginPage.passwordInput.fill(\"password\")\n    await loginPage.signInButton.click()\n    await expect(accountOverviewPage.welcomeMessage).toBeVisible()\n  })\n\n  test(\"logging out works correctly\", async ({\n    page,\n    accountOverviewPage,\n    loginPage,\n  }) => {\n    await loginPage.accountLink.click()\n    await loginPage.container.waitFor({ state: \"visible\" })\n    await loginPage.emailInput.fill(\"test@example.com\")\n    await loginPage.passwordInput.fill(\"password\")\n    await loginPage.signInButton.click()\n    await expect(accountOverviewPage.welcomeMessage).toBeVisible()\n\n    await accountOverviewPage.logoutLink.highlight()\n    await accountOverviewPage.logoutLink.click()\n    await loginPage.container.waitFor({ state: \"visible\" })\n\n    await loginPage.accountLink.click()\n    await loginPage.container.waitFor({ state: \"visible\" })\n  })\n})\n"
  },
  {
    "path": "storefront/e2e/tests/public/register.spec.ts",
    "content": "import { test, expect } from \"../../index\"\n\ntest.describe(\"User registration functionality\", async () => {\n  test(\"registration with existing user shows error message\", async ({\n    loginPage,\n    registerPage,\n  }) => {\n    await loginPage.accountLink.click()\n    await registerPage.container.isVisible()\n    await loginPage.registerButton.click()\n\n    await registerPage.firstNameInput.fill(\"first\")\n    await registerPage.lastNameInput.fill(\"last\")\n    await registerPage.emailInput.fill(\"test@example.com\")\n    await registerPage.passwordInput.fill(\"password\")\n    await registerPage.registerButton.click()\n\n    await expect(registerPage.registerError).toBeVisible()\n  })\n\n  test(\"registration with empty form data highlights corresponding input\", async ({\n    accountOverviewPage,\n    loginPage,\n    registerPage,\n  }) => {\n    await loginPage.accountLink.click()\n    await registerPage.container.isVisible()\n    await loginPage.registerButton.click()\n\n    await registerPage.registerButton.click()\n    await expect(registerPage.firstNameInput).toBeFocused()\n    await registerPage.firstNameInput.fill(\"first\")\n\n    await registerPage.registerButton.click()\n    await expect(registerPage.lastNameInput).toBeFocused()\n    await registerPage.lastNameInput.fill(\"last\")\n\n    await registerPage.registerButton.click()\n    await expect(registerPage.emailInput).toBeFocused()\n    await registerPage.emailInput.fill(\"test-reg-new@example.com\")\n\n    await registerPage.registerButton.click()\n    await expect(registerPage.passwordInput).toBeFocused()\n    await registerPage.passwordInput.fill(\"password\")\n\n    await registerPage.registerButton.click()\n    await expect(accountOverviewPage.welcomeMessage).toBeVisible()\n  })\n\n  test(\"successful registration and navigation to account overview\", async ({\n    loginPage,\n    registerPage,\n    accountOverviewPage,\n  }) => {\n    await loginPage.accountLink.click()\n    await registerPage.container.isVisible()\n    await loginPage.registerButton.click()\n\n    await registerPage.firstNameInput.fill(\"first\")\n    await registerPage.lastNameInput.fill(\"last\")\n    await registerPage.emailInput.fill(\"test-reg@example.com\")\n    await registerPage.passwordInput.fill(\"password\")\n    await registerPage.registerButton.click()\n\n    await expect(accountOverviewPage.welcomeMessage).toBeVisible()\n  })\n})\n"
  },
  {
    "path": "storefront/e2e/tests/public/search.spec.ts",
    "content": "import { test, expect } from \"../../index\"\n\ntest.describe(\"Search tests\", async () => {\n  test(\"Searching for a specific product returns the correct product page\", async ({\n    productPage,\n  }) => {\n    const searchModal = productPage.searchModal\n    await searchModal.open()\n    await searchModal.searchInput.fill(\"Sweatshirt\")\n    await searchModal.searchResult\n      .filter({ hasText: \"Sweatshirt\" })\n      .first()\n      .click()\n    await productPage.container.waitFor({ state: \"visible\" })\n    await expect(productPage.productTitle).toContainText(\"Sweatshirt\")\n  })\n\n  test(\"An erroneous search returns an empty result\", async ({\n    productPage,\n  }) => {\n    const searchModal = productPage.searchModal\n    await searchModal.open()\n    await searchModal.searchInput.fill(\"Does Not Sweatshirt\")\n    await expect(searchModal.noSearchResultsContainer).toBeVisible()\n  })\n\n  test(\"User can search after an empty search result\", async ({\n    productPage,\n  }) => {\n    const searchModal = productPage.searchModal\n\n    await searchModal.open()\n    await searchModal.searchInput.fill(\"Does Not Sweatshirt\")\n    await expect(searchModal.noSearchResultsContainer).toBeVisible()\n\n    await searchModal.searchInput.fill(\"Sweat\")\n    await expect(searchModal.searchResults).toBeVisible()\n    await expect(searchModal.searchResult.first()).toBeVisible()\n  })\n\n  test(\"Closing the search page returns user back to their current page\", async ({\n    storePage,\n    productPage,\n    loginPage,\n  }) => {\n    const searchModal = storePage.searchModal\n    await test.step(\"Navigate to the store page and open and close search modal\", async () => {\n      await storePage.goto()\n      await searchModal.open()\n      await searchModal.close()\n      await expect(storePage.container).toBeVisible()\n    })\n\n    await test.step(\"Navigate to the product page and open and close search modal\", async () => {\n      await storePage.goto()\n      const product = await storePage.getProduct(\"Sweatshirt\")\n      await product.locator.click()\n      await productPage.container.waitFor({ state: \"visible\" })\n      await searchModal.open()\n      await searchModal.close()\n      await expect(productPage.container).toBeVisible()\n    })\n\n    await test.step(\"Navigate to the login page and open and close search modal\", async () => {\n      await loginPage.goto()\n      await searchModal.open()\n      await searchModal.close()\n      await expect(loginPage.container).toBeVisible()\n    })\n  })\n})\n"
  },
  {
    "path": "storefront/e2e/utils/index.ts",
    "content": "export function getFloatValue(s: string) {\n  return parseFloat(parseFloat(s).toFixed(2))\n}\n\nexport function compareFloats(f1: number, f2: number) {\n  const diff = f1 - f2\n  if (Math.abs(diff) < 0.01) {\n    return 0\n  } else if (diff < 0) {\n    return -1\n  } else {\n    return 1\n  }\n}\n"
  },
  {
    "path": "storefront/e2e/utils/locators.ts",
    "content": "import { Page, Locator} from '@playwright/test'\n\nexport async function getSelectedOptionText(page: Page, select: Locator) {\n  const handle = await select.elementHandle()\n  return await page.evaluate(\n    (opts) => {\n      if (!opts || !opts[0]) { return \"\" }\n      const select = opts[0] as HTMLSelectElement\n      return select.options[select.selectedIndex].textContent\n    },\n    [handle]\n  )\n}\n"
  },
  {
    "path": "storefront/eslint.config.cjs",
    "content": "const { FlatCompat } = require(\"@eslint/eslintrc\")\n\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n})\n\nmodule.exports = [\n  ...compat.config({\n    extends: [\"next/core-web-vitals\", \"next/typescript\", \"prettier\"],\n    ignorePatterns: [\n      // Dependencies\n      \"node_modules/\",\n\n      // Build and output directories\n      \".next/\",\n      \"out/\",\n\n      // Coverage\n      \"coverage/\",\n\n      // Static and public assets\n      \"public/\",\n\n      // Test directories\n      \"e2e/\",\n      \"integration-tests/\",\n\n      // Type definitions\n      \"**/*.d.ts\",\n\n      // Environment and config files\n      \".env\",\n      \".env.local\",\n      \".env.*.local\",\n\n      // Linting cache\n      \".eslintcache\",\n\n      // Package lock files (yarn only)\n      \"yarn.lock\",\n    ],\n    rules: {\n      // General best practices\n      \"no-console\": [\n        \"warn\",\n        {\n          allow: [\"warn\", \"error\", \"info\"],\n        },\n      ],\n      \"no-debugger\": \"error\",\n      \"no-duplicate-imports\": \"error\",\n      \"no-var\": \"error\",\n      \"prefer-const\": \"error\",\n      \"prefer-arrow-callback\": \"warn\",\n      \"object-shorthand\": \"warn\",\n      eqeqeq: [\"error\", \"always\"],\n\n      // React best practices\n      \"react/no-unescaped-entities\": \"warn\",\n      \"react/self-closing-comp\": \"error\",\n      \"react/prefer-es6-class\": \"error\",\n      \"react/no-array-index-key\": \"warn\",\n      \"react/no-danger\": \"warn\",\n\n      // React hooks\n      \"react-hooks/rules-of-hooks\": \"error\",\n      \"react-hooks/exhaustive-deps\": \"warn\",\n\n      // Next.js\n      \"@next/next/no-img-element\": \"warn\",\n      \"@next/next/no-html-link-for-pages\": \"error\",\n    },\n  }),\n  // Override for config files that legitimately use require()\n  {\n    files: [\"**/*.config.js\", \"**/*.config.cjs\", \"check-env-variables.js\"],\n    rules: {\n      \"@typescript-eslint/no-require-imports\": \"off\",\n    },\n  },\n]\n"
  },
  {
    "path": "storefront/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n/// <reference path=\"./.next/types/routes.d.ts\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "storefront/next-sitemap.js",
    "content": "const excludedPaths = [\"/checkout\", \"/account/*\"]\n\nmodule.exports = {\n  siteUrl: process.env.NEXT_PUBLIC_VERCEL_URL,\n  generateRobotsTxt: true,\n  exclude: excludedPaths + [\"/[sitemap]\"],\n  robotsTxtOptions: {\n    policies: [\n      {\n        userAgent: \"*\",\n        allow: \"/\",\n      },\n      {\n        userAgent: \"*\",\n        disallow: excludedPaths,\n      },\n    ],\n  },\n}\n"
  },
  {
    "path": "storefront/next.config.js",
    "content": "const checkEnvVariables = require(\"./check-env-variables\")\n\ncheckEnvVariables()\n\n/**\n * @type {import('next').NextConfig}\n */\nconst nextConfig = {\n  reactStrictMode: true,\n  experimental: {\n    staticGenerationRetryCount: 3,\n    staticGenerationMaxConcurrency: 1,\n  },\n  images: {\n    remotePatterns: [\n      {\n        protocol: \"http\",\n        hostname: \"localhost\",\n      },\n      {\n        protocol: \"https\",\n        hostname: \"fashion-starter-demo.s3.eu-central-1.amazonaws.com\",\n      },\n    ],\n  },\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "storefront/package.json",
    "content": "{\n  \"name\": \"fashion-starter\",\n  \"version\": \"2.0.0\",\n  \"private\": true,\n  \"author\": \"Ante Primorac <ante@agilo.com>\",\n  \"description\": \"Next.js Fashion E-Commerce Starter to be used with Medusa 2.0\",\n  \"keywords\": [\n    \"medusa-storefront\"\n  ],\n  \"scripts\": {\n    \"dev\": \"next dev -p 8000 --turbo\",\n    \"build\": \"next build\",\n    \"start\": \"next start -p 8000\",\n    \"lint\": \"eslint . --ext .js,.jsx,.ts,.tsx\",\n    \"lint:fix\": \"eslint . --ext .js,.jsx,.ts,.tsx --fix\",\n    \"analyze\": \"ANALYZE=true next build\",\n    \"test-e2e\": \"playwright test e2e\"\n  },\n  \"resolutions\": {\n    \"webpack\": \"^5\"\n  },\n  \"dependencies\": {\n    \"@hookform/resolvers\": \"^5.2.2\",\n    \"@medusajs/icons\": \"^2.13.1\",\n    \"@medusajs/js-sdk\": \"2.13.1\",\n    \"@medusajs/types\": \"2.13.1\",\n    \"@paypal/paypal-js\": \"^8.4.2\",\n    \"@paypal/react-paypal-js\": \"^8.9.2\",\n    \"@stripe/react-stripe-js\": \"^5.6.0\",\n    \"@stripe/stripe-js\": \"^8.7.0\",\n    \"@tanstack/react-query\": \"^5.70.2\",\n    \"@vercel/speed-insights\": \"^1.2.0\",\n    \"axios\": \"^1.13.5\",\n    \"embla-carousel-react\": \"^8.6.0\",\n    \"lodash\": \"^4.17.23\",\n    \"meilisearch\": \"^0.55.0\",\n    \"next\": \"15.5.10\",\n    \"pg\": \"^8.13.3\",\n    \"qs\": \"^6.14.2\",\n    \"react\": \"^19.0.0\",\n    \"react-aria-components\": \"^1.15.1\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-hook-form\": \"^7.71.1\",\n    \"react-stately\": \"^3.44.0\",\n    \"server-only\": \"^0.0.1\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"tailwindcss-radix\": \"^4.0.2\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.58.2\",\n    \"@tailwindcss/postcss\": \"^4\",\n    \"@types/lodash\": \"^4.17.16\",\n    \"@types/node\": \"^20\",\n    \"@types/pg\": \"^8.11.11\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"ansi-colors\": \"^4.1.3\",\n    \"autoprefixer\": \"^10.4.21\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"15.2.1\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"postcss\": \"^8.5.3\",\n    \"prettier\": \"^3.8.1\",\n    \"tailwindcss\": \"^3.4.17\",\n    \"typescript\": \"^5\",\n    \"webpack\": \"^5\"\n  },\n  \"packageManager\": \"yarn@1.22.19\"\n}\n"
  },
  {
    "path": "storefront/playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\"\nimport path from \"path\"\nimport \"dotenv/config.js\"\n\nexport const STORAGE_STATE = path.join(__dirname, \"playwright/.auth/user.json\")\n\nexport default defineConfig({\n  testDir: \"./e2e\",\n  /* Run tests in files in parallel */\n  fullyParallel: false,\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n  /* Retry on CI only */\n  retries: process.env.CI ? 2 : 0,\n  /* Opt out of parallel tests on CI. */\n  workers: 1,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: \"html\",\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    baseURL: process.env.NEXT_PUBLIC_BASE_URL,\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: \"retain-on-failure\",\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    {\n      name: \"setup\",\n      testMatch: /global\\/setup\\.ts/,\n      teardown: \"cleanup test database\",\n    },\n    {\n      name: \"public setup\",\n      testMatch: /global\\/public-setup\\.ts/,\n      teardown: \"cleanup test database\",\n    },\n    {\n      name: \"cleanup test database\",\n      testMatch: /global\\/teardown\\.ts/,\n    },\n    {\n      name: \"chromium auth\",\n      dependencies: [\"setup\"],\n      testIgnore: \"public/*.spec.ts\",\n      use: { ...devices[\"Desktop Chrome\"], storageState: STORAGE_STATE },\n    },\n\n    {\n      name: \"chromium public\",\n      dependencies: [\"public setup\"],\n      testMatch: \"public/*.spec.ts\",\n      use: { ...devices[\"Desktop Chrome\"] },\n    },\n\n    /*\n    {\n      name: \"firefox\",\n      use: { ...devices[\"Desktop Firefox\"] },\n    },\n\n    {\n      name: \"webkit\",\n      use: { ...devices[\"Desktop Safari\"] },\n    },\n    */\n  ],\n\n  /* Run your local dev server before starting the tests */\n  webServer: {\n     command: 'yarn start',\n     url: process.env.NEXT_PUBLIC_BASE_URL,\n  //   reuseExistingServer: !process.env.CI,\n  },\n})\n"
  },
  {
    "path": "storefront/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(checkout)/checkout/loading.tsx",
    "content": "import { Icon } from \"@/components/Icon\"\n\nexport default function Loading() {\n  return (\n    <div className=\"absolute left-0 top-20 md:top-40 lg:top-0 w-[100vw] lg:max-w-[calc(100vw-((50vw-50%)+448px))] xl:max-w-[calc(100vw-((50vw-50%)+540px))] -ml-[calc(50vw-50%)] h-screen lg:w-full flex items-center justify-center\">\n      <Icon name=\"loader\" className=\"w-10 md:w-20 animate-spin\" />\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(checkout)/checkout/page.tsx",
    "content": "import React from \"react\"\nimport { Metadata } from \"next\"\nimport { notFound } from \"next/navigation\"\nimport { getCartId } from \"@lib/data/cookies\"\nimport { CheckoutForm } from \"@modules/checkout/components/checkout-form\"\n\nexport const metadata: Metadata = {\n  title: \"Checkout\",\n}\n\nexport default async function Checkout({\n  params,\n  searchParams,\n}: {\n  params: Promise<{ countryCode: string }>\n  searchParams: Promise<{ step?: string }>\n}) {\n  const cart = await getCartId()\n  if (!cart) {\n    return notFound()\n  }\n\n  const { countryCode } = await params\n  const { step } = await searchParams\n\n  return <CheckoutForm countryCode={countryCode} step={step} />\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(checkout)/layout.tsx",
    "content": "import * as React from \"react\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport dynamic from \"next/dynamic\"\n\nconst CheckoutSummaryWrapper = dynamic(\n  () => import(\"@modules/checkout/components/checkout-summary-wrapper\"),\n  { loading: () => <></> }\n)\n\nconst  MobileCheckoutSummaryWrapper= dynamic(\n  () => import(\"@modules/checkout/components/mobile-checkout-summary-wrapper\"),\n  { loading: () => <></> }\n)\nexport default function CheckoutLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return (\n    <>\n      <Layout className=\"lg:hidden\">\n        <LayoutColumn>\n          <div className=\"flex justify-between items-center h-18\">\n            <LocalizedLink href=\"/\" className=\"text-md font-medium\">\n              SofaSocietyCo.\n            </LocalizedLink>\n            <div>\n              <p className=\"font-semibold\">Checkout</p>\n            </div>\n          </div>\n        </LayoutColumn>\n      </Layout>\n      <div className=\"w-full bg-grayscale-50 lg:hidden\">\n        <Layout>\n          <LayoutColumn>\n            <MobileCheckoutSummaryWrapper />\n          </LayoutColumn>\n        </Layout>\n      </div>\n      <Layout>\n        <LayoutColumn className=\"flex max-lg:flex-col-reverse lg:justify-between relative\">\n          <div className=\"flex-1 pt-8 lg:max-w-125 xl:max-w-150 pb-9 lg:pb-40\">\n            <LocalizedLink\n              href=\"/\"\n              className=\"text-md font-medium mb-16 inline-block max-lg:hidden\"\n            >\n              SofaSocietyCo.\n            </LocalizedLink>\n            {children}\n          </div>\n          <div className=\"sticky top-0 lg:max-w-100 xl:max-w-123 flex-1 py-32 max-lg:hidden z-10 self-start\">\n            <CheckoutSummaryWrapper />\n          </div>\n          <div className=\"absolute right-0 top-0 lg:max-w-[calc((50vw-50%)+448px)] xl:max-w-[calc((50vw-50%)+540px)] -mr-[calc(50vw-50%)] bg-grayscale-50 h-full w-full max-lg:hidden\" />\n        </LayoutColumn>\n      </Layout>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(checkout)/not-found.tsx",
    "content": "import { Metadata } from \"next\"\n\nimport NotFoundPage from \"app/not-found\"\n\nexport const metadata: Metadata = {\n  title: \"404\",\n  description: \"Something went wrong\",\n}\n\nexport default async function NotFound() {\n  return <NotFoundPage />\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/about/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport Image from \"next/image\"\nimport { StoreRegion } from \"@medusajs/types\"\nimport { listRegions } from \"@lib/data/regions\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\n\nexport const metadata: Metadata = {\n  title: \"About\",\n  description: \"Learn more about Sofa Society\",\n}\n\nexport async function generateStaticParams() {\n  const countryCodes = await listRegions().then((regions: StoreRegion[]) =>\n    regions.flatMap((r) =>\n      r.countries\n        ? r.countries\n            .map((c) => c.iso_2)\n            .filter(\n              (value): value is string =>\n                typeof value === \"string\" && Boolean(value)\n            )\n        : []\n    )\n  )\n\n  const staticParams = countryCodes.map((countryCode) => ({\n    countryCode,\n  }))\n\n  return staticParams\n}\n\nexport default function AboutPage() {\n  return (\n    <>\n      <div className=\"max-md:pt-18\">\n        <Image\n          src=\"/images/content/living-room-gray-three-seater-sofa.png\"\n          width={2880}\n          height={1500}\n          alt=\"Living room with gray three-seater sofa\"\n          className=\"md:h-screen md:object-cover\"\n        />\n      </div>\n      <div className=\"pt-8 md:pt-26 pb-26 md:pb-36\">\n        <Layout>\n          <LayoutColumn start={1} end={{ base: 13, lg: 7 }}>\n            <h3 className=\"text-md max-lg:mb-6 md:text-2xl\">\n              At Sofa Society, we believe that a sofa is the heart of every\n              home.\n            </h3>\n          </LayoutColumn>\n          <LayoutColumn start={{ base: 1, lg: 8 }} end={13}>\n            <div className=\"md:text-md lg:mt-18\">\n              <p className=\"mb-5 lg:mb-9\">\n                Welcome to Sofa Society, where we believe that comfort and style\n                should be effortlessly intertwined. Our mission is to help you\n                create beautiful, functional spaces that bring warmth and\n                relaxation into your home.\n              </p>\n              <p>\n                Every piece in our collection is designed with care, blending\n                timeless craftsmanship with modern aesthetics to offer you the\n                perfect balance between form and function.\n              </p>\n            </div>\n          </LayoutColumn>\n          <LayoutColumn>\n            <Image\n              src=\"/images/content/living-room-black-armchair-dark-gray-sofa.png\"\n              width={2496}\n              height={1404}\n              alt=\"Living room with black armchair and dark gray sofa\"\n              className=\"mt-26 lg:mt-36 mb-8 lg:mb-26\"\n            />\n          </LayoutColumn>\n          <LayoutColumn start={1} end={{ base: 13, lg: 8 }}>\n            <h3 className=\"text-md lg:mb-10 mb-6 md:text-2xl\">\n              We are here to make your living space a true reflection of your\n              personal style.\n            </h3>\n          </LayoutColumn>\n          <LayoutColumn start={1} end={{ base: 13, lg: 6 }}>\n            <div className=\"mb-16 lg:mb-26\">\n              <p className=\"mb-5 md:mb-9\">\n                At the heart of our brand is a deep commitment to quality. We\n                understand that a sofa isn&apos;t just another piece of\n                furniture; it&apos;s where you unwind, gather with loved ones,\n                and make memories. That&apos;s why we source only the finest\n                materials and fabrics, ensuring that every sofa we offer is\n                built to last.\n              </p>\n              <p>\n                From luxurious leathers and soft linens to high-performance\n                textiles, each fabric is carefully selected for its durability\n                and beauty. Our attention to detail extends to every stitch and\n                seam, guaranteeing that your sofa will not only look stunning\n                but will also withstand the test of time.\n              </p>\n            </div>\n          </LayoutColumn>\n          <LayoutColumn start={{ base: 2, lg: 1 }} end={{ base: 12, lg: 7 }}>\n            <Image\n              src=\"/images/content/gray-one-seater-sofa-wooden-coffee-table.png\"\n              width={1200}\n              height={1600}\n              alt=\"Gray one-seater sofa and wooden coffee table\"\n              className=\"mb-16 lg:mb-46\"\n            />\n          </LayoutColumn>\n          <LayoutColumn start={{ base: 1, lg: 8 }} end={13}>\n            <div className=\"mb-6 lg:mb-20 xl:mb-36\">\n              <p>\n                Our design philosophy revolves around creating pieces that are\n                both beautiful and practical. Inspired by Scandinavian\n                simplicity, modern luxury, and timeless classics, our\n                collections are curated to suit a wide variety of tastes and\n                lifestyles. We understand that every home is different, so we\n                offer a diverse range of styles, colors, and textures to help\n                you find the perfect fit. Whether you prefer sleek modern lines\n                or soft, inviting silhouettes, we have something to suit every\n                space and personality.\n              </p>\n            </div>\n            <div className=\"md:text-md max-lg:mb-26\">\n              <p>\n                We believe that great design should be environmentally\n                conscious, which is why we strive to minimise our environmental\n                footprint through responsible sourcing and production practices.\n                Our commitment to sustainability ensures that our products are\n                not only beautiful but also kind to the planet.\n              </p>\n            </div>\n          </LayoutColumn>\n        </Layout>\n        <Image\n          src=\"/images/content/living-room-gray-three-seater-puffy-sofa.png\"\n          width={2880}\n          height={1618}\n          alt=\"Living room with gray three-seater puffy sofa\"\n          className=\"mb-8 lg:mb-26\"\n        />\n        <Layout>\n          <LayoutColumn start={1} end={{ base: 13, lg: 7 }}>\n            <h3 className=\"text-md max-lg:mb-6 md:text-2xl\">\n              Our customers are at the center of everything we do!\n            </h3>\n          </LayoutColumn>\n          <LayoutColumn start={{ base: 1, lg: 8 }} end={13}>\n            <div className=\"md:text-md lg:mt-18\">\n              <p className=\"mb-5 lg:mb-9\">\n                Our team is here to help guide you through the process, offering\n                personalised support to ensure that you find exactly what\n                you&apos;re looking for.\n              </p>\n              <p>\n                We&apos;re not just selling sofas - we&apos;re helping you\n                create spaces where you can relax, recharge, and make lasting\n                memories. Thank you for choosing Sofa Society to be a part of\n                your home!\n              </p>\n            </div>\n          </LayoutColumn>\n        </Layout>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/account/layout.tsx",
    "content": "import * as React from \"react\"\n\nimport { SignOutButton } from \"@modules/account/components/SignOutButton\"\nimport { SidebarNav } from \"@modules/account/components/SidebarNav\"\n\nexport default function AccountLayout(props: { children: React.ReactNode }) {\n  return (\n    <div className=\"flex max-md:flex-col\">\n      <div className=\"sticky left-0 shrink-0 z-30 top-0 md:max-w-75 lg:max-w-93 py-2 max-md:mt-18 max-md:top-18 md:pt-45 md:pb-9 max-md:border-b max-md:border-grayscale-200 max-md:overflow-x-auto bg-white md:bg-grayscale-50 w-full md:h-screen\">\n        <div className=\"md:max-w-54 mx-auto flex max-md:items-center md:flex-col md:justify-between h-full max-md:px-4 max-md:sm:container max-md:mx-auto\">\n          <div className=\"max-md:flex max-md:gap-22\">\n            <h1 className=\"text-lg mb-14 max-md:hidden\">My account</h1>\n            <SidebarNav />\n          </div>\n          <SignOutButton\n            variant=\"ghost\"\n            className=\"justify-start px-0 py-3 max-md:hidden\"\n          >\n            Log out\n          </SignOutButton>\n        </div>\n      </div>\n      <div className=\"max-md:px-4 overflow-hidden max-md:sm:container max-md:mx-auto md:px-10 pt-10 md:pt-45 pb-26 md:pb-36 w-full lg:max-w-200 xl:mx-auto 2xl:ml-30\">\n        {props.children}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/account/loading.tsx",
    "content": "import SkeletonAccountPage from \"@modules/skeletons/templates/skeleton-account-page\"\n\nexport default function Loading() {\n  return <SkeletonAccountPage />\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/account/my-orders/[orderId]/page.tsx",
    "content": "import * as React from \"react\"\nimport { Metadata } from \"next\"\nimport Image from \"next/image\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nimport { convertToLocale } from \"@lib/util/money\"\nimport { retrieveOrder } from \"@lib/data/orders\"\nimport { OrderTotals } from \"@modules/order/components/OrderTotals\"\nimport { UiTag } from \"@/components/ui/Tag\"\nimport { UiTagList, UiTagListDivider } from \"@/components/ui/TagList\"\nimport { Icon } from \"@/components/Icon\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { getCustomer } from \"@lib/data/customer\"\nimport { redirect } from \"next/navigation\"\n\nexport const metadata: Metadata = {\n  title: \"Account - Order\",\n  description: \"Check your order history\",\n}\n\nconst OrderStatus: React.FC<{ order: HttpTypes.StoreOrder }> = ({ order }) => {\n  if (order.fulfillment_status === \"canceled\") {\n    return (\n      <UiTagList>\n        <UiTag iconName=\"close\" isActive className=\"self-start mt-auto\">\n          Canceled\n        </UiTag>\n      </UiTagList>\n    )\n  }\n\n  if (order.fulfillment_status === \"delivered\") {\n    return (\n      <UiTagList>\n        <UiTag isActive iconName=\"package\" className=\"self-start mt-auto\">\n          Packing\n        </UiTag>\n        <UiTagListDivider />\n        <UiTag isActive iconName=\"truck\" className=\"self-start mt-auto\">\n          Delivering\n        </UiTag>\n        <UiTagListDivider />\n        <UiTag isActive iconName=\"check\" className=\"self-start mt-auto\">\n          Delivered\n        </UiTag>\n      </UiTagList>\n    )\n  }\n\n  if (\n    order.fulfillment_status === \"shipped\" ||\n    order.fulfillment_status === \"partially_delivered\"\n  ) {\n    return (\n      <UiTagList>\n        <UiTag isActive iconName=\"package\" className=\"self-start mt-auto\">\n          Packing\n        </UiTag>\n        <UiTagListDivider />\n        <UiTag isActive iconName=\"truck\" className=\"self-start mt-auto\">\n          Delivering\n        </UiTag>\n        <UiTagListDivider />\n        <UiTag iconName=\"check\" className=\"self-start mt-auto\">\n          Delivered\n        </UiTag>\n      </UiTagList>\n    )\n  }\n\n  return (\n    <UiTagList>\n      <UiTag isActive iconName=\"package\" className=\"self-start mt-auto\">\n        Packing\n      </UiTag>\n      <UiTagListDivider />\n      <UiTag iconName=\"truck\" className=\"self-start mt-auto\">\n        Delivering\n      </UiTag>\n      <UiTagListDivider />\n      <UiTag iconName=\"check\" className=\"self-start mt-auto\">\n        Delivered\n      </UiTag>\n    </UiTagList>\n  )\n}\n\nexport default async function AccountOrderPage({\n  params,\n}: {\n  params: Promise<{ orderId: string }>\n}) {\n  const customer = await getCustomer().catch(() => null)\n\n  if (!customer) {\n    redirect(`/`)\n  }\n\n  const { orderId } = await params\n  const order = await retrieveOrder(orderId)\n\n  return (\n    <>\n      <h1 className=\"text-md md:text-lg mb-8 md:mb-16\">\n        Order: {order.display_id}\n      </h1>\n      <div className=\"flex flex-col gap-6\">\n        <div className=\"rounded-xs border border-grayscale-200 flex flex-wrap justify-between p-4\">\n          <div className=\"flex gap-4 items-center\">\n            <Icon name=\"calendar\" />\n            <p className=\"text-grayscale-500\">Order date</p>\n          </div>\n          <div>\n            <p>{new Date(order.created_at).toLocaleDateString()}</p>\n          </div>\n        </div>\n        <div className=\"rounded-xs border border-grayscale-200 p-4\">\n          <div className=\"flex flex-wrap gap-x-10 gap-y-8 justify-between items-end w-full\">\n            <OrderStatus order={order} />\n          </div>\n        </div>\n        <div className=\"flex max-sm:flex-col gap-x-4 gap-y-6 md:flex-col lg:flex-row\">\n          <div className=\"flex-1 overflow-hidden rounded-xs border border-grayscale-200 p-4\">\n            <div className=\"flex gap-4 items-center mb-8\">\n              <Icon name=\"map-pin\" />\n              <p className=\"text-grayscale-500\">Delivery address</p>\n            </div>\n            <div>\n              <p>\n                {[\n                  order.shipping_address?.first_name,\n                  order.shipping_address?.last_name,\n                ]\n                  .filter(Boolean)\n                  .join(\" \")}\n              </p>\n              {Boolean(order.shipping_address?.company) && (\n                <p>{order.shipping_address?.company}</p>\n              )}\n              <p>\n                {[\n                  order.shipping_address?.address_1,\n                  order.shipping_address?.address_2,\n                  [\n                    order.shipping_address?.postal_code,\n                    order.shipping_address?.city,\n                  ]\n                    .filter(Boolean)\n                    .join(\" \"),\n                  order.shipping_address?.country?.display_name,\n                ]\n                  .filter(Boolean)\n                  .join(\", \")}\n              </p>\n              {Boolean(order.shipping_address?.phone) && (\n                <p>{order.shipping_address?.phone}</p>\n              )}\n            </div>\n          </div>\n          <div className=\"flex-1 overflow-hidden rounded-xs border border-grayscale-200 p-4\">\n            <div className=\"flex gap-4 items-center mb-8\">\n              <Icon name=\"receipt\" />\n              <p className=\"text-grayscale-500\">Billing address</p>\n            </div>\n            <div>\n              <p>\n                {[\n                  order.billing_address?.first_name,\n                  order.billing_address?.last_name,\n                ]\n                  .filter(Boolean)\n                  .join(\" \")}\n              </p>\n              {Boolean(order.billing_address?.company) && (\n                <p>{order.billing_address?.company}</p>\n              )}\n              <p>\n                {[\n                  order.billing_address?.address_1,\n                  order.billing_address?.address_2,\n                  [\n                    order.billing_address?.postal_code,\n                    order.billing_address?.city,\n                  ]\n                    .filter(Boolean)\n                    .join(\" \"),\n                  order.billing_address?.country?.display_name,\n                ]\n                  .filter(Boolean)\n                  .join(\", \")}\n              </p>\n              {Boolean(order.billing_address?.phone) && (\n                <p>{order.billing_address?.phone}</p>\n              )}\n            </div>\n          </div>\n        </div>\n        <div className=\"rounded-xs border border-grayscale-200 p-4 flex flex-col gap-6\">\n          {order.items?.map((item) => (\n            <div\n              key={item.id}\n              className=\"flex gap-x-4 sm:gap-x-8 gap-y-6 pb-6 border-b border-grayscale-100 last:border-0 last:pb-0\"\n            >\n              {item.thumbnail && (\n                <LocalizedLink\n                  href={`/products/${item.product_handle}`}\n                  className=\"max-w-25 sm:max-w-37 aspect-[3/4] w-full relative overflow-hidden\"\n                >\n                  <Image\n                    src={item.thumbnail}\n                    alt={item.title}\n                    fill\n                    className=\"object-cover\"\n                  />\n                </LocalizedLink>\n              )}\n              <div className=\"flex flex-col flex-1\">\n                <p className=\"mb-2 sm:text-md\">\n                  <LocalizedLink href={`/products/${item.product_handle}`}>\n                    {item.product_title}\n                  </LocalizedLink>\n                </p>\n                <div className=\"text-xs flex flex-col flex-1\">\n                  <div>\n                    {item.variant?.options?.map((option) => (\n                      <p className=\"mb-1\" key={option.id}>\n                        <span className=\"text-grayscale-500 mr-2\">\n                          {option.option?.title}:\n                        </span>\n                        {option.value}\n                      </p>\n                    ))}\n                  </div>\n                  <div className=\"mt-auto flex max-xs:flex-col gap-x-10 gap-y-6.5 xs:items-center justify-between relative\">\n                    <div className=\"xs:self-end sm:mb-1\">\n                      <p>\n                        <span className=\"text-grayscale-500 mr-2\">\n                          Quantity:\n                        </span>\n                        {item.quantity}\n                      </p>\n                    </div>\n                    <div className=\"sm:text-md\">\n                      <p>\n                        {convertToLocale({\n                          currency_code: order.currency_code,\n                          amount: item.total,\n                        })}\n                      </p>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          ))}\n        </div>\n        <div className=\"rounded-xs border border-grayscale-200 p-4 flex max-sm:flex-col gap-y-4 gap-x-10 md:flex-wrap justify-between\">\n          <div className=\"flex items-center self-baseline gap-4\">\n            <Icon name=\"credit-card\" />\n            <div>\n              <p className=\"text-grayscale-500\">Payment</p>\n            </div>\n          </div>\n          <OrderTotals order={order} />\n        </div>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/account/my-orders/page.tsx",
    "content": "import * as React from \"react\"\nimport { Metadata } from \"next\"\nimport Image from \"next/image\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport { twMerge } from \"tailwind-merge\"\n\nimport { listOrders } from \"@lib/data/orders\"\nimport { Pagination } from \"@modules/store/components/pagination\"\nimport { ButtonLink } from \"@/components/Button\"\nimport { UiTag } from \"@/components/ui/Tag\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { getCustomer } from \"@lib/data/customer\"\nimport { redirect } from \"next/navigation\"\n\nexport const metadata: Metadata = {\n  title: \"Account - Orders\",\n  description: \"Check your order history\",\n}\n\nconst OrderStatus: React.FC<{\n  order: HttpTypes.StoreOrder\n  className?: string\n}> = ({ order, className }) => {\n  if (order.fulfillment_status === \"canceled\") {\n    return (\n      <UiTag\n        iconName=\"close\"\n        isActive\n        className={twMerge(\"self-start mt-auto\", className)}\n      >\n        Canceled\n      </UiTag>\n    )\n  }\n\n  if (order.fulfillment_status === \"delivered\") {\n    return (\n      <UiTag\n        iconName=\"check\"\n        isActive\n        className={twMerge(\"self-start mt-auto\", className)}\n      >\n        Delivered\n      </UiTag>\n    )\n  }\n\n  if (\n    order.fulfillment_status === \"shipped\" ||\n    order.fulfillment_status === \"partially_delivered\"\n  ) {\n    return (\n      <UiTag\n        iconName=\"truck\"\n        isActive\n        className={twMerge(\"self-start mt-auto\", className)}\n      >\n        Delivering\n      </UiTag>\n    )\n  }\n\n  return (\n    <UiTag\n      iconName=\"package\"\n      isActive\n      className={twMerge(\"self-start mt-auto\", className)}\n    >\n      Packing\n    </UiTag>\n  )\n}\n\ntype PageProps = {\n  searchParams: Promise<{\n    page?: string\n  }>\n}\n\nconst ORDERS_PER_PAGE = 6\n\nexport default async function AccountMyOrdersPage({ searchParams }: PageProps) {\n  const { page } = await searchParams\n\n  const customer = await getCustomer().catch(() => null)\n\n  if (!customer) {\n    redirect(`/`)\n  }\n\n  const pageNumber = page ? parseInt(page, 10) : 1\n  const { orders, count } = await listOrders(\n    ORDERS_PER_PAGE,\n    (pageNumber - 1) * ORDERS_PER_PAGE\n  )\n  const totalPages = Math.ceil(count / ORDERS_PER_PAGE)\n\n  return (\n    <>\n      <h1 className=\"text-md md:text-lg mb-8 md:mb-13\">My orders</h1>\n      {orders.length > 0 ? (\n        <div className=\"flex flex-col gap-8\">\n          <div className=\"flex flex-col gap-8 sm:gap-4\">\n            {orders.map((order) => (\n              <div\n                key={order.id}\n                className=\"rounded-xs border border-grayscale-200 flex flex-col gap-6 sm:gap-8 md:gap-6 lg:gap-8 p-4\"\n              >\n                <div className=\"flex max-sm:flex-col-reverse md:flex-col-reverse lg:flex-row gap-y-6 gap-x-10 justify-between\">\n                  <div className=\"flex-shrink-0\">\n                    <OrderStatus order={order} className=\"sm:hidden mb-6\" />\n                    <div className=\"mb-2\">\n                      <LocalizedLink\n                        href={`/account/my-orders/${order.id}`}\n                        className=\"text-md\"\n                      >\n                        <span className=\"font-semibold\">Order:</span>{\" \"}\n                        {order.display_id}\n                      </LocalizedLink>\n                    </div>\n                    <p className=\"text-grayscale-500\">\n                      Order date:{\" \"}\n                      {new Date(order.created_at).toLocaleDateString()}\n                    </p>\n                  </div>\n                  <div className=\"flex gap-3 overflow-x-auto sm:max-w-91 md:max-w-full lg:max-w-91\">\n                    {order.items\n                      ?.filter((item) => item.thumbnail)\n                      .map((item) => (\n                        <LocalizedLink\n                          key={item.id}\n                          href={`/products/${item.product_handle}`}\n                          className=\"shrink-0 w-19 aspect-[3/4] rounded-2xs relative overflow-hidden\"\n                        >\n                          <Image\n                            src={item.thumbnail!}\n                            alt={item.title}\n                            fill\n                            className=\"object-cover\"\n                          />\n                        </LocalizedLink>\n                      ))}\n                  </div>\n                </div>\n                <div className=\"flex max-sm:flex-col justify-between gap-6\">\n                  <OrderStatus order={order} className=\"max-sm:hidden\" />\n                  <ButtonLink\n                    href={`/account/my-orders/${order.id}`}\n                    variant=\"outline\"\n                    size=\"md\"\n                    className=\"sm:self-end md:self-start lg:self-end sm:hidden\"\n                  >\n                    Check details\n                  </ButtonLink>\n                  <ButtonLink\n                    href={`/account/my-orders/${order.id}`}\n                    variant=\"outline\"\n                    size=\"sm\"\n                    className=\"sm:self-end md:self-start lg:self-end max-sm:hidden\"\n                  >\n                    Check details\n                  </ButtonLink>\n                </div>\n              </div>\n            ))}\n          </div>\n          {totalPages > 1 && (\n            <Pagination page={pageNumber} totalPages={totalPages} />\n          )}\n        </div>\n      ) : (\n        <p className=\"text-md mt-16\">You haven&apos;t ordered anything yet</p>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/account/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport { redirect } from \"next/navigation\"\nimport { getCustomer } from \"@lib/data/customer\"\nimport { getRegion, listRegions } from \"@lib/data/regions\"\nimport { UpsertAddressForm } from \"@modules/account/components/UpsertAddressForm\"\nimport { PersonalInfoForm } from \"@modules/account/components/PersonalInfoForm\"\nimport { SignOutButton } from \"@modules/account/components/SignOutButton\"\nimport { Icon } from \"@/components/Icon\"\nimport { Button } from \"@/components/Button\"\nimport { UiModal, UiModalOverlay } from \"@/components/ui/Modal\"\nimport { UiDialog, UiDialogTrigger } from \"@/components/Dialog\"\nimport { RequestPasswordResetButton } from \"@modules/account/components/RequestPasswordResetButton\"\nimport { AddressSingle } from \"@modules/account/components/AddressSingle\"\nimport { AddressMultiple } from \"@modules/account/components/AddressMultiple\"\nimport { DefaultShippingAddressSelect } from \"@modules/account/components/DefaultShippingAddressSelect\"\nimport { DefaultBillingAddressSelect } from \"@modules/account/components/DefaultBillingAddressSelect\"\nimport { UiRadioGroup } from \"@/components/ui/Radio\"\n\nexport const metadata: Metadata = {\n  title: \"Account - Personal & security\",\n  description: \"Manage your personal information and security settings\",\n}\n\nexport default async function AccountPersonalAndSecurityPage({\n  params,\n}: {\n  params: Promise<{ countryCode: string }>\n}) {\n  const { countryCode } = await params\n  const customer = await getCustomer().catch(() => null)\n\n  if (!customer) {\n    redirect(`/${countryCode}/auth/login`)\n  }\n\n  const [region, regions] = await Promise.all([\n    getRegion(countryCode),\n    listRegions(),\n  ])\n  const countries = regions.flatMap((region) => region.countries ?? [])\n\n  return (\n    <>\n      <h1 className=\"text-md md:text-lg mb-8 md:mb-16 max-md:font-semibold\">\n        Personal &amp; security\n      </h1>\n      <h2 className=\"text-md font-normal mb-6\">Personal information</h2>\n      <div className=\"w-full border border-grayscale-200 rounded-xs p-4 flex flex-wrap gap-8 max-sm:flex-col sm:items-center md:flex-col md:items-stretch lg:items-center lg:flex-row mb-16\">\n        <div className=\"flex gap-8 flex-1\">\n          <Icon name=\"user\" className=\"w-6 h-6 sm:mt-2.5\" />\n          <div className=\"flex max-sm:flex-col sm:flex-wrap gap-6 sm:gap-x-16\">\n            <div>\n              <p className=\"text-xs text-grayscale-500 mb-1.5\">Name</p>\n              <p>\n                {[customer.first_name, customer.last_name]\n                  .filter(Boolean)\n                  .join(\" \")}\n              </p>\n            </div>\n            <div>\n              <p className=\"text-xs text-grayscale-500 mb-1.5\">Number</p>\n              <p>{customer.phone || \"-\"}</p>\n            </div>\n          </div>\n        </div>\n        <UiDialogTrigger>\n          <Button variant=\"outline\">Change</Button>\n          <UiModalOverlay>\n            <UiModal>\n              <UiDialog>\n                <PersonalInfoForm\n                  defaultValues={{\n                    first_name: customer.first_name ?? \"\",\n                    last_name: customer.last_name ?? \"\",\n                    phone: customer.phone ?? undefined,\n                  }}\n                />\n              </UiDialog>\n            </UiModal>\n          </UiModalOverlay>\n        </UiDialogTrigger>\n      </div>\n      <h2 className=\"text-md font-normal mb-6\">Contact</h2>\n      <div className=\"w-full border border-grayscale-200 rounded-xs p-4 flex flex-wrap gap-y-6 gap-x-8 items-center mb-4\">\n        <Icon name=\"user\" className=\"w-6 h-6\" />\n        <div>\n          <p className=\"text-xs text-grayscale-500 mb-1.5\">Email</p>\n          <p>{customer.email}</p>\n        </div>\n      </div>\n      <p className=\"text-xs text-grayscale-500 mb-16\">\n        If you want to change your email please contact us via customer support.\n      </p>\n      <h2 className=\"text-md font-normal mb-6\">\n        {customer.addresses.length > 1 ? \"Addresses\" : \"Address\"}\n      </h2>\n      {customer.addresses.length === 0 && (\n        <p className=\"text-grayscale-500 mb-6\">\n          You don&apos;t have any addresses saved yet.\n        </p>\n      )}\n      {customer.addresses.length === 1 &&\n        customer.addresses.map((address) => (\n          <AddressSingle\n            key={address.id}\n            address={address}\n            countries={countries}\n            region={region}\n            className=\"mb-6\"\n          />\n        ))}\n      {customer.addresses.length > 1 && (\n        <>\n          <DefaultShippingAddressSelect\n            addresses={customer.addresses}\n            countries={countries}\n          />\n          <DefaultBillingAddressSelect\n            addresses={customer.addresses}\n            countries={countries}\n          />\n          <UiRadioGroup\n            className=\"flex flex-col sm:flex-row md:flex-col lg:flex-row sm:flex-wrap gap-x-6 gap-y-8 mb-6\"\n            aria-label=\"address\"\n          >\n            {customer.addresses\n              .sort(\n                (a, b) =>\n                  new Date(b.created_at).getTime() -\n                  new Date(a.created_at).getTime()\n              )\n              .map((address) => (\n                <AddressMultiple\n                  key={address.id}\n                  address={address}\n                  countries={countries}\n                  region={region}\n                  className=\"h-auto sm:max-w-[calc(50%-0.75rem)] md:max-w-full lg:max-w-[calc(50%-0.75rem)] w-full\"\n                />\n              ))}\n          </UiRadioGroup>\n        </>\n      )}\n      <UiDialogTrigger>\n        {customer.addresses.length > 0 ? (\n          <Button className=\"mb-16 max-sm:w-full\">Add another address</Button>\n        ) : (\n          <Button className=\"mb-16 max-sm:w-full\">Add address</Button>\n        )}\n        <UiModalOverlay>\n          <UiModal>\n            <UiDialog>\n              <UpsertAddressForm\n                region={region ?? undefined}\n                defaultValues={{\n                  country_code: countryCode,\n                }}\n              />\n            </UiDialog>\n          </UiModal>\n        </UiModalOverlay>\n      </UiDialogTrigger>\n      <h2 className=\"text-md font-normal mb-6 md:mb-4\">Change password</h2>\n      <p className=\"max-md:text-xs text-grayscale-500 mb-6\">\n        To change your password, we&apos;ll send you an email. Just click on the\n        reset button below.\n      </p>\n      <RequestPasswordResetButton />\n      <div className=\"mt-16 md:hidden\">\n        <p className=\"text-md mb-6\">Log out</p>\n        <SignOutButton variant=\"outline\" isFullWidth />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/auth/forgot-password/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport Image from \"next/image\"\nimport { ForgotPasswordForm } from \"@modules/auth/components/ForgotPasswordForm\"\n\nexport const metadata: Metadata = {\n  title: \"Forgot password\",\n  description: \"Reset your password\",\n}\n\nexport default function ForgotPasswordPage() {\n  return (\n    <div className=\"flex min-h-screen\">\n      <Image\n        src=\"/images/content/gray-backrest-sofa-wooden-coffee-table.png\"\n        width={1440}\n        height={1632}\n        alt=\"Gray backrest sofa and wooden coffee table\"\n        className=\"max-lg:hidden lg:w-1/2 shrink-0 object-cover\"\n      />\n      <div className=\"shrink-0 max-w-100 lg:max-w-96 w-full mx-auto pt-30 lg:pt-37 pb-16 max-sm:px-4\">\n        <ForgotPasswordForm />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/auth/forgot-password/reset/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport { notFound } from \"next/navigation\"\n\nimport { ChangePasswordForm } from \"@modules/auth/components/ResetPasswordForm\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\n\nexport const metadata: Metadata = {\n  title: \"Reset password\",\n  description: \"Reset your password\",\n}\n\nexport default async function ResetPasswordPage({\n  searchParams,\n}: {\n  searchParams: Promise<{ [key: string]: string | string[] | undefined }>\n}) {\n  const { email, token } = await searchParams\n\n  if (\n    typeof email !== \"string\" ||\n    typeof token !== \"string\" ||\n    !email ||\n    !token\n  ) {\n    notFound()\n  }\n\n  return (\n    <Layout className=\"py-26 md:pt-45 md:pb-36\">\n      <LayoutColumn\n        start={{ base: 1, sm: 3, lg: 4, xl: 5 }}\n        end={{ base: 13, sm: 11, lg: 10, xl: 9 }}\n      >\n        <ChangePasswordForm email={email} token={token} />\n      </LayoutColumn>\n    </Layout>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/auth/login/loading.tsx",
    "content": "import Image from \"next/image\"\n\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { Button } from \"@/components/Button\"\nimport { Input } from \"@/components/Forms\"\n\nexport default async function LoginLoadingPage() {\n  return (\n    <div className=\"flex min-h-screen\">\n      <Image\n        src=\"/images/content/gray-backrest-sofa-wooden-coffee-table.png\"\n        width={1440}\n        height={1632}\n        alt=\"Gray backrest sofa and wooden coffee table\"\n        className=\"max-lg:hidden lg:w-1/2 shrink-0 object-cover\"\n      />\n      <div className=\"shrink-0 max-w-100 lg:max-w-96 w-full mx-auto pt-30 lg:pt-37 pb-16 max-sm:px-4\">\n        <h1 className=\"text-xl md:text-2xl mb-10 md:mb-16\">\n          Welcome back to Sofa Society!\n        </h1>\n        <form className=\"flex flex-col gap-6 md:gap-8 mb-8 md:mb-16\">\n          <Input\n            placeholder=\"Email\"\n            name=\"email\"\n            required\n            wrapperClassName=\"flex-1\"\n            autoComplete=\"email\"\n            disabled\n          />\n          <Input\n            placeholder=\"Password\"\n            name=\"password\"\n            type=\"password\"\n            required\n            wrapperClassName=\"flex-1\"\n            autoComplete=\"current-password\"\n            disabled\n          />\n          <LocalizedLink\n            href=\"/auth/forgot-password\"\n            variant=\"underline\"\n            className=\"self-start !pb-0 text-grayscale-500 leading-none\"\n          >\n            Forgot password?\n          </LocalizedLink>\n          <Button isLoading>Log in</Button>\n        </form>\n        <p className=\"text-grayscale-500\">\n          Don&apos;t have an account yet? You can{\" \"}\n          <LocalizedLink\n            href=\"/auth/register\"\n            variant=\"underline\"\n            className=\"text-black md:pb-0.5\"\n          >\n            register here\n          </LocalizedLink>\n          .\n        </p>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/auth/login/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport Image from \"next/image\"\nimport { redirect } from \"next/navigation\"\n\nimport { getCustomer } from \"@lib/data/customer\"\nimport { LoginForm } from \"@modules/auth/components/LoginForm\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\n\nexport const metadata: Metadata = {\n  title: \"Login\",\n  description: \"Login to your account\",\n}\n\nexport default async function LoginPage({\n  params,\n}: {\n  params: Promise<{ countryCode: string }>\n}) {\n  const { countryCode } = await params\n  const customer = await getCustomer().catch(() => null)\n\n  if (customer) {\n    redirect(`/${countryCode}/account`)\n  }\n\n  return (\n    <div className=\"flex min-h-screen\">\n      <Image\n        src=\"/images/content/gray-backrest-sofa-wooden-coffee-table.png\"\n        width={1440}\n        height={1632}\n        alt=\"Gray backrest sofa and wooden coffee table\"\n        className=\"max-lg:hidden lg:w-1/2 shrink-0 object-cover\"\n      />\n      <div className=\"shrink-0 max-w-100 lg:max-w-96 w-full mx-auto pt-30 lg:pt-37 pb-16 max-sm:px-4\">\n        <h1 className=\"text-xl md:text-2xl mb-10 md:mb-16\">\n          Welcome back to Sofa Society!\n        </h1>\n        <LoginForm\n          className=\"mb-10 md:mb-15\"\n          redirectUrl={`/${countryCode}/account`}\n        />\n        <p className=\"text-grayscale-500\">\n          Don&apos;t have an account yet? You can{\" \"}\n          <LocalizedLink\n            href=\"/auth/register\"\n            variant=\"underline\"\n            className=\"text-black md:pb-0.5\"\n          >\n            register here\n          </LocalizedLink>\n          .\n        </p>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/auth/register/loading.tsx",
    "content": "import Image from \"next/image\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { Input } from \"@/components/Forms\"\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\n\nexport default function RegisterLoadingPage() {\n  return (\n    <div className=\"flex min-h-screen\">\n      <Image\n        src=\"/images/content/living-room-dark-gray-corner-sofa-coffee-table.png\"\n        width={1440}\n        height={1632}\n        alt=\"Living room with dark gray corner sofa and coffee table\"\n        className=\"max-lg:hidden lg:w-1/2 shrink-0 object-cover\"\n      />\n      <div className=\"shrink-0 max-w-100 lg:max-w-96 w-full mx-auto pt-30 lg:pt-37 pb-16 max-sm:px-4\">\n        <h1 className=\"text-xl md:text-2xl mb-10 md:mb-16\">\n          Hey, welcome to Sofa Society!\n        </h1>\n        <form className=\"flex flex-col gap-6 md:gap-8 mb-8 md:mb-16\">\n          <div className=\"flex gap-4 md:gap-6\">\n            <Input\n              placeholder=\"First name\"\n              name=\"first_name\"\n              required\n              wrapperClassName=\"flex-1\"\n              minLength={1}\n              disabled\n            />\n            <Input\n              placeholder=\"Last name\"\n              name=\"last_name\"\n              required\n              wrapperClassName=\"flex-1\"\n              minLength={1}\n              disabled\n            />\n          </div>\n          <Input\n            placeholder=\"Email\"\n            name=\"email\"\n            required\n            wrapperClassName=\"flex-1\"\n            type=\"email\"\n            disabled\n          />\n          <Input\n            placeholder=\"Phone\"\n            name=\"phone\"\n            wrapperClassName=\"flex-1\"\n            type=\"tel\"\n            disabled\n          />\n          <Input\n            placeholder=\"Password\"\n            name=\"password\"\n            type=\"password\"\n            required\n            wrapperClassName=\"flex-1\"\n            autoComplete=\"new-password\"\n            minLength={6}\n            disabled\n          />\n          <Input\n            placeholder=\"Confirm password\"\n            name=\"confirm_password\"\n            type=\"password\"\n            required\n            wrapperClassName=\"flex-1\"\n            autoComplete=\"new-password\"\n            minLength={6}\n            disabled\n          />\n          <SubmitButton isLoading>Register</SubmitButton>\n        </form>\n        <p className=\"text-grayscale-500\">\n          Already have an account? No worries, just{\" \"}\n          <LocalizedLink\n            href=\"/auth/login\"\n            variant=\"underline\"\n            className=\"text-black md:pb-0.5\"\n          >\n            log in\n          </LocalizedLink>\n          .\n        </p>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/auth/register/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport Image from \"next/image\"\nimport { redirect } from \"next/navigation\"\n\nimport { getCustomer } from \"@lib/data/customer\"\nimport { SignUpForm } from \"@modules/auth/components/SignUpForm\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\n\nexport const metadata: Metadata = {\n  title: \"Register\",\n  description: \"Create an account\",\n}\n\nexport default async function RegisterPage({\n  params,\n}: {\n  params: Promise<{ countryCode: string }>\n}) {\n  const customer = await getCustomer().catch(() => null)\n\n  if (customer) {\n    redirect(`/${(await params).countryCode}/account`)\n  }\n\n  return (\n    <div className=\"flex min-h-screen\">\n      <Image\n        src=\"/images/content/living-room-dark-gray-corner-sofa-coffee-table.png\"\n        width={1440}\n        height={1632}\n        alt=\"Living room with dark gray corner sofa and coffee table\"\n        className=\"max-lg:hidden lg:w-1/2 shrink-0 object-cover\"\n      />\n      <div className=\"shrink-0 max-w-100 lg:max-w-96 w-full mx-auto pt-30 lg:pt-37 pb-16 max-sm:px-4\">\n        <h1 className=\"text-xl md:text-2xl mb-10 md:mb-16\">\n          Hey, welcome to Sofa Society!\n        </h1>\n        <SignUpForm />\n        <p className=\"text-grayscale-500\">\n          Already have an account? No worries, just{\" \"}\n          <LocalizedLink\n            href=\"/auth/login\"\n            variant=\"underline\"\n            className=\"text-black md:pb-0.5\"\n          >\n            log in\n          </LocalizedLink>\n          .\n        </p>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/auth/reset-password/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport { notFound } from \"next/navigation\"\n\nimport { ChangePasswordForm } from \"@modules/auth/components/ResetPasswordForm\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\n\nexport const metadata: Metadata = {\n  title: \"Reset password\",\n  description: \"Reset your password\",\n}\n\nexport default async function ResetPasswordPage({\n  searchParams,\n}: {\n  searchParams: Promise<{ [key: string]: string | string[] | undefined }>\n}) {\n  const { email, token } = await searchParams\n\n  if (\n    typeof email !== \"string\" ||\n    typeof token !== \"string\" ||\n    !email ||\n    !token\n  ) {\n    notFound()\n  }\n\n  return (\n    <Layout className=\"py-26 md:pt-45 md:pb-36\">\n      <LayoutColumn\n        start={{ base: 1, sm: 3, lg: 4, xl: 5 }}\n        end={{ base: 13, sm: 11, lg: 10, xl: 9 }}\n      >\n        <ChangePasswordForm email={email} token={token} customer={true} />\n      </LayoutColumn>\n    </Layout>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/cart/loading.tsx",
    "content": "import SkeletonCartPage from \"@modules/skeletons/templates/skeleton-cart-page\"\n\nexport default function Loading() {\n  return <SkeletonCartPage />\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/cart/not-found.tsx",
    "content": "import { Metadata } from \"next\"\n\nimport NotFoundPage from \"app/not-found\"\n\nexport const metadata: Metadata = {\n  title: \"404\",\n  description: \"Something went wrong\",\n}\n\nexport default function NotFound() {\n  return <NotFoundPage />\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/cart/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport CartTemplate from \"@modules/cart/templates\"\n\nexport const metadata: Metadata = {\n  title: \"Cart\",\n  description: \"View your cart\",\n}\nexport default  function Cart() {\n\n  return <CartTemplate  />\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/collections/[handle]/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport { notFound } from \"next/navigation\"\n\nimport {\n  getCollectionByHandle,\n  getCollectionsList,\n} from \"@lib/data/collections\"\nimport { listRegions } from \"@lib/data/regions\"\nimport { StoreCollection, StoreRegion } from \"@medusajs/types\"\nimport CollectionTemplate from \"@modules/collections/templates\"\nimport { SortOptions } from \"@modules/store/components/refinement-list/sort-products\"\nimport { collectionMetadataCustomFieldsSchema } from \"@lib/util/collections\"\n\ntype Props = {\n  params: Promise<{ handle: string; countryCode: string }>\n  searchParams: Promise<{\n    category?: string | string[]\n    type?: string | string[]\n    page?: string\n    sortBy?: SortOptions\n  }>\n}\n\nexport async function generateStaticParams() {\n  const { collections } = await getCollectionsList()\n\n  if (!collections) {\n    return []\n  }\n\n  const countryCodes = await listRegions().then(\n    (regions: StoreRegion[]) =>\n      regions\n        ?.map((r) => r.countries?.map((c) => c.iso_2))\n        .flat()\n        .filter(Boolean) as string[]\n  )\n\n  const collectionHandles = collections.map(\n    (collection: StoreCollection) => collection.handle\n  )\n\n  const staticParams = countryCodes\n    ?.map((countryCode: string) =>\n      collectionHandles.map((handle: string | undefined) => ({\n        countryCode,\n        handle,\n      }))\n    )\n    .flat()\n\n  return staticParams\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { handle } = await params\n\n  const collection = await getCollectionByHandle(handle, [\n    \"id\",\n    \"title\",\n    \"metadata\",\n  ])\n\n  if (!collection) {\n    notFound()\n  }\n\n  const collectionDetails = collectionMetadataCustomFieldsSchema.safeParse(\n    collection.metadata ?? {}\n  )\n\n  const metadata = {\n    title: `${collection.title} | Medusa Store`,\n    description:\n      collectionDetails.success && collectionDetails.data.description\n        ? collectionDetails.data.description\n        : `${collection.title} collection`,\n  } as Metadata\n\n  return metadata\n}\n\nexport default async function CollectionPage({ params, searchParams }: Props) {\n  const { handle, countryCode } = await params\n  const { sortBy, page, category, type } = await searchParams\n\n  const collection = await getCollectionByHandle(handle, [\n    \"id\",\n    \"title\",\n    \"metadata\",\n  ])\n\n  if (!collection) {\n    notFound()\n  }\n\n  return (\n    <CollectionTemplate\n      collection={collection}\n      page={page}\n      sortBy={sortBy}\n      countryCode={countryCode}\n      category={\n        !category ? undefined : Array.isArray(category) ? category : [category]\n      }\n      type={!type ? undefined : Array.isArray(type) ? type : [type]}\n    />\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/cookie-policy/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport { StoreRegion } from \"@medusajs/types\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { listRegions } from \"@lib/data/regions\"\n\nexport const metadata: Metadata = {\n  title: \"Cookie Policy\",\n}\n\nexport async function generateStaticParams() {\n  const countryCodes = await listRegions().then((regions: StoreRegion[]) =>\n    regions.flatMap((r) =>\n      r.countries\n        ? r.countries\n            .map((c) => c.iso_2)\n            .filter(\n              (value): value is string =>\n                typeof value === \"string\" && Boolean(value)\n            )\n        : []\n    )\n  )\n\n  const staticParams = countryCodes.map((countryCode) => ({\n    countryCode,\n  }))\n\n  return staticParams\n}\n\nexport default function CookiePolicyPage() {\n  return (\n    <Layout className=\"pt-30 pb-20 md:pt-47 md:pb-32\">\n      <LayoutColumn\n        start={{ base: 1, lg: 2, xl: 3 }}\n        end={{ base: 13, lg: 11, xl: 10 }}\n      >\n        <h1 className=\"text-lg md:text-2xl mb-16 md:mb-25\">\n          Cookie Policy for Sofa Society\n        </h1>\n      </LayoutColumn>\n      <LayoutColumn\n        start={{ base: 1, lg: 2, xl: 3 }}\n        end={{ base: 13, lg: 10, xl: 9 }}\n        className=\"article\"\n      >\n        <p>\n          This Cookie Policy explains how Sofa Society uses cookies and similar\n          technologies on our website. By using our website, you consent to the\n          use of cookies as described in this policy.\n        </p>\n        <h2>1. What Are Cookies:</h2>\n        <p>\n          Cookies are small text files that are placed on your computer or\n          device when you visit a website. They are widely used to make websites\n          work more efficiently and provide a better browsing experience.\n          Cookies also enable website owners to collect certain information\n          about visitors.\n        </p>\n        <h2>2. Types of Cookies We Use:</h2>\n        <p>We use the following types of cookies on our website:</p>\n        <ul>\n          <li>\n            Essential Cookies: These cookies are necessary for the operation of\n            our website and enable you to navigate and use its features. They\n            are typically set in response to your actions, such as setting your\n            privacy preferences, logging in, or filling out forms.\n          </li>\n          <li>\n            Analytics and Performance Cookies: These cookies help us understand\n            how visitors interact with our website by collecting information\n            such as the number of visitors, pages visited, and sources of\n            traffic. This data helps us improve our website&apos;s performance\n            and usability.\n          </li>\n          <li>\n            Functionality Cookies: These cookies allow our website to remember\n            choices you make (such as language preferences) and provide enhanced\n            features. They may also be used to provide personalized content\n            based on your browsing history.\n          </li>\n          <li>\n            Advertising and Targeting Cookies: These cookies are used to deliver\n            advertisements that are relevant to your interests. They may also be\n            used to limit the number of times you see an advertisement and\n            measure the effectiveness of advertising campaigns.\n          </li>\n        </ul>\n        <h2>3. Third-Party Cookies:</h2>\n        <p>\n          We may allow third-party service providers, such as analytics and\n          advertising companies, to place cookies on our website. These third\n          parties may collect information about your online activities over time\n          and across different websites.\n        </p>\n        <h2>4. Cookie Management:</h2>\n        <p>\n          You can manage and control cookies through your browser settings. Most\n          web browsers allow you to block or delete cookies. However, please\n          note that blocking or deleting certain cookies may impact the\n          functionality and user experience of our website.\n        </p>\n        <p>\n          For more information on how to manage cookies, you can visit the help\n          or settings section of your browser.\n        </p>\n        <h2>5. Updates to the Cookie Policy:</h2>\n        <p>\n          We may update this Cookie Policy from time to time to reflect changes\n          in our use of cookies or for other operational, legal, or regulatory\n          reasons. We will notify you of any material changes by posting a\n          prominent notice on our website.\n        </p>\n        <h2>6. Contact Us:</h2>\n        <p>\n          If you have any questions, concerns, or requests regarding this\n          Privacy Policy or how we handle your personal information, please\n          contact us at:\n        </p>\n        <p>\n          Email: privacy@sofasociety.com\n          <br />\n          Address: Skärgårdsvägen 12, 124 55 Stockholm\n        </p>\n      </LayoutColumn>\n    </Layout>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/inspiration/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport Image from \"next/image\"\nimport { StoreRegion } from \"@medusajs/types\"\nimport { listRegions } from \"@lib/data/regions\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { CollectionsSection } from \"@/components/CollectionsSection\"\n\nexport const metadata: Metadata = {\n  title: \"Inspiration\",\n  description: \"Get inspired by our latest collections\",\n}\n\nexport async function generateStaticParams() {\n  const countryCodes = await listRegions().then((regions: StoreRegion[]) =>\n    regions.flatMap((r) =>\n      r.countries\n        ? r.countries\n            .map((c) => c.iso_2)\n            .filter(\n              (value): value is string =>\n                typeof value === \"string\" && Boolean(value)\n            )\n        : []\n    )\n  )\n\n  const staticParams = countryCodes.map((countryCode) => ({\n    countryCode,\n  }))\n\n  return staticParams\n}\n\nexport default function InspirationPage() {\n  return (\n    <>\n      <div className=\"max-md:pt-18\">\n        <Image\n          src=\"/images/content/living-room-dark-green-three-seater-sofa.png\"\n          width={2880}\n          height={1500}\n          alt=\"Living room with dark green three-seater sofa\"\n          className=\"md:h-screen md:object-cover mb-8 md:mb-26\"\n        />\n      </div>\n      <div className=\"pb-26 md:pb-36\">\n        <Layout>\n          <LayoutColumn start={1} end={{ base: 13, md: 8 }}>\n            <h3 className=\"text-md mb-6 md:mb-16 md:text-2xl\">\n              The Astrid Curve sofa is a masterpiece of minimalism and luxury.\n            </h3>\n            <div className=\"md:text-md max-md:mb-16 max-w-135\">\n              <p>\n                Our design philosophy revolves around creating pieces that are\n                both beautiful and practical. Inspired by Scandinavian\n                simplicity, modern luxury, and timeless classics.\n              </p>\n            </div>\n          </LayoutColumn>\n          <LayoutColumn start={{ base: 1, md: 9 }} end={13}>\n            <LocalizedLink href=\"/products/astrid-curve\">\n              <Image\n                src=\"/images/content/dark-gray-three-seater-sofa.png\"\n                width={768}\n                height={572}\n                alt=\"Dark gray three-seater sofa\"\n                className=\"mb-4 md:mb-6\"\n              />\n              <div className=\"flex justify-between\">\n                <div>\n                  <p className=\"mb-1\">Astrid Curve</p>\n                  <p className=\"text-grayscale-500 text-xs\">\n                    Scandinavian Simplicity\n                  </p>\n                </div>\n                <div>\n                  <p className=\"font-semibold\">1500€</p>\n                </div>\n              </div>\n            </LocalizedLink>\n          </LayoutColumn>\n          <LayoutColumn>\n            <Image\n              src=\"/images/content/living-room-brown-armchair-gray-corner-sofa.png\"\n              width={2496}\n              height={1404}\n              alt=\"Living room with brown armchair and gray corner sofa\"\n              className=\"mt-26 md:mt-36 mb-8 md:mb-26\"\n            />\n          </LayoutColumn>\n          <LayoutColumn start={1} end={{ base: 13, md: 8 }}>\n            <h3 className=\"text-md mb-6 md:mb-16 md:text-2xl\">\n              Haven Sofas have minimalistic designs, neutral colors, and\n              high-quality textures.\n            </h3>\n            <div className=\"md:text-md max-md:mb-16 max-w-135\">\n              <p>\n                Perfect for those who seek comfort with a clean and understated\n                aesthetic. This collection brings the essence of Scandinavian\n                elegance to your living room.\n              </p>\n            </div>\n          </LayoutColumn>\n          <LayoutColumn start={{ base: 1, md: 9 }} end={13}>\n            <LocalizedLink\n              href=\"/products/nordic-haven\"\n              className=\"mb-8 md:mb-16 inline-block\"\n            >\n              <Image\n                src=\"/images/content/gray-three-seater-sofa.png\"\n                width={768}\n                height={572}\n                alt=\"Gray three-seater sofa\"\n                className=\"mb-4 md:mb-6\"\n              />\n              <div className=\"flex justify-between\">\n                <div>\n                  <p className=\"mb-1\">Nordic Haven</p>\n                  <p className=\"text-grayscale-500 text-xs\">\n                    Scandinavian Simplicity\n                  </p>\n                </div>\n                <div>\n                  <p className=\"font-semibold\">1500€</p>\n                </div>\n              </div>\n            </LocalizedLink>\n            <LocalizedLink href=\"/products/nordic-breeze\">\n              <Image\n                src=\"/images/content/gray-arm-chair.png\"\n                width={768}\n                height={572}\n                alt=\"Gray arm chair\"\n                className=\"mb-4 md:mb-6\"\n              />\n              <div className=\"flex justify-between\">\n                <div>\n                  <p className=\"mb-1\">Nordic Breeze</p>\n                  <p className=\"text-grayscale-500 text-xs\">\n                    Scandinavian Simplicity\n                  </p>\n                </div>\n                <div>\n                  <p className=\"font-semibold\">1200€</p>\n                </div>\n              </div>\n            </LocalizedLink>\n          </LayoutColumn>\n        </Layout>\n        <Image\n          src=\"/images/content/living-room-gray-two-seater-puffy-sofa.png\"\n          width={2880}\n          height={1618}\n          alt=\"Living room with gray two-seater puffy sofa\"\n          className=\"md:h-screen md:object-cover mt-26 md:mt-36 mb-8 md:mb-26\"\n        />\n        <Layout>\n          <LayoutColumn start={1} end={{ base: 13, md: 8 }}>\n            <h3 className=\"text-md mb-6 md:mb-16 md:text-2xl\">\n              Oslo Drift is infused with playful textures and vibrant patterns\n              with eclectic vibes.\n            </h3>\n            <div className=\"md:text-md max-md:mb-16 max-w-135\">\n              <p>\n                Whether you&apos;re looking for bold statement pieces or subtle\n                elegance, this collection elevates your home with a touch of\n                glamour, sophistication, and unmatched coziness.\n              </p>\n            </div>\n          </LayoutColumn>\n          <LayoutColumn start={{ base: 1, md: 9 }} end={13}>\n            <LocalizedLink href=\"/products/oslo-drift\">\n              <Image\n                src=\"/images/content/white-two-seater-sofa.png\"\n                width={768}\n                height={572}\n                alt=\"White two-seater sofa\"\n                className=\"mb-4 md:mb-6\"\n              />\n              <div className=\"flex justify-between\">\n                <div>\n                  <p className=\"mb-1\">Oslo Drift</p>\n                  <p className=\"text-grayscale-500 text-xs\">\n                    Scandinavian Simplicity\n                  </p>\n                </div>\n                <div>\n                  <p className=\"font-semibold\">1500€</p>\n                </div>\n              </div>\n            </LocalizedLink>\n          </LayoutColumn>\n        </Layout>\n        <CollectionsSection className=\"mt-26 md:mt-36\" />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/layout.tsx",
    "content": "import { Metadata } from \"next\"\nimport { getBaseURL } from \"@lib/util/env\"\nimport { Header } from \"@/components/Header\"\nimport { Footer } from \"@/components/Footer\"\n\nexport const metadata: Metadata = {\n  metadataBase: new URL(getBaseURL()),\n}\n\nexport default async function PageLayout(props: { children: React.ReactNode }) {\n  return (\n    <>\n      <Header />\n      {props.children}\n      <Footer />\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/not-found.tsx",
    "content": "import { Metadata } from \"next\"\n\nimport NotFoundPage from \"app/not-found\"\n\nexport const metadata: Metadata = {\n  title: \"404\",\n  description: \"Something went wrong\",\n}\n\nexport default function NotFound() {\n  return <NotFoundPage />\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/order/confirmed/[id]/loading.tsx",
    "content": "import SkeletonOrderConfirmed from \"@modules/skeletons/templates/skeleton-order-confirmed\"\n\nexport default function Loading() {\n  return <SkeletonOrderConfirmed />\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/order/confirmed/[id]/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport { notFound } from \"next/navigation\"\n\nimport OrderCompletedTemplate from \"@modules/order/templates/order-completed-template\"\nimport { retrieveOrder } from \"@lib/data/orders\"\n\ntype Props = {\n  params: Promise<{ id: string }>\n}\n\nexport const metadata: Metadata = {\n  title: \"Order Confirmed\",\n  description: \"You purchase was successful\",\n}\n\nexport default async function OrderConfirmedPage({ params }: Props) {\n  const { id } = await params\n  const order = await retrieveOrder(id)\n  if (!order) {\n    return notFound()\n  }\n\n  return <OrderCompletedTemplate order={order} />\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport Image from \"next/image\"\nimport { getRegion } from \"@lib/data/regions\"\nimport { getProductTypesList } from \"@lib/data/product-types\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { CollectionsSection } from \"@/components/CollectionsSection\"\n\nexport const metadata: Metadata = {\n  title: \"Medusa Next.js Starter Template\",\n  description:\n    \"A performant frontend ecommerce starter template with Next.js 14 and Medusa.\",\n}\n\nconst ProductTypesSection: React.FC = async () => {\n  const productTypes = await getProductTypesList(0, 20, [\n    \"id\",\n    \"value\",\n    \"metadata\",\n  ])\n\n  if (!productTypes) {\n    return null\n  }\n\n  return (\n    <Layout className=\"mb-26 md:mb-36 max-md:gap-x-2\">\n      <LayoutColumn>\n        <h3 className=\"text-md md:text-2xl mb-8 md:mb-15\">Our products</h3>\n      </LayoutColumn>\n      {productTypes.productTypes.map((productType, index) => (\n        <LayoutColumn\n          key={productType.id}\n          start={index % 2 === 0 ? 1 : 7}\n          end={index % 2 === 0 ? 7 : 13}\n        >\n          <LocalizedLink href={`/store?type=${productType.value}`}>\n            {typeof productType.metadata?.image === \"object\" &&\n              productType.metadata.image &&\n              \"url\" in productType.metadata.image &&\n              typeof productType.metadata.image.url === \"string\" && (\n                <Image\n                  src={productType.metadata.image.url}\n                  width={1200}\n                  height={900}\n                  alt={productType.value}\n                  className=\"mb-2 md:mb-8\"\n                />\n              )}\n            <p className=\"text-xs md:text-md\">{productType.value}</p>\n          </LocalizedLink>\n        </LayoutColumn>\n      ))}\n    </Layout>\n  )\n}\n\nexport default async function Home({\n  params,\n}: {\n  params: Promise<{ countryCode: string }>\n}) {\n  const { countryCode } = await params\n  const region = await getRegion(countryCode)\n\n  if (!region) {\n    return null\n  }\n\n  return (\n    <>\n      <div className=\"max-md:pt-18\">\n        <Image\n          src=\"/images/content/living-room-gray-armchair-two-seater-sofa.png\"\n          width={2880}\n          height={1500}\n          alt=\"Living room with gray armchair and two-seater sofa\"\n          className=\"md:h-screen md:object-cover\"\n        />\n      </div>\n      <div className=\"pt-8 pb-26 md:pt-26 md:pb-36\">\n        <Layout className=\"mb-26 md:mb-36\">\n          <LayoutColumn start={1} end={{ base: 13, md: 8 }}>\n            <h3 className=\"text-md max-md:mb-6 md:text-2xl\">\n              Elevate Your Living Space with Unmatched Comfort & Style\n            </h3>\n          </LayoutColumn>\n          <LayoutColumn start={{ base: 1, md: 9 }} end={13}>\n            <div className=\"flex items-center h-full\">\n              <div className=\"md:text-md\">\n                <p>Discover Your Perfect Sofa Today</p>\n                <LocalizedLink href=\"/store\" variant=\"underline\">\n                  Explore Now\n                </LocalizedLink>\n              </div>\n            </div>\n          </LayoutColumn>\n        </Layout>\n        <ProductTypesSection />\n        <CollectionsSection className=\"mb-22 md:mb-36\" />\n        <Layout>\n          <LayoutColumn className=\"col-span-full\">\n            <h3 className=\"text-md md:text-2xl mb-8 md:mb-16\">\n              About Sofa Society\n            </h3>\n            <Image\n              src=\"/images/content/gray-sofa-against-concrete-wall.png\"\n              width={2496}\n              height={1400}\n              alt=\"Gray sofa against concrete wall\"\n              className=\"mb-8 md:mb-16 max-md:aspect-[3/2] max-md:object-cover\"\n            />\n          </LayoutColumn>\n          <LayoutColumn start={1} end={{ base: 13, md: 7 }}>\n            <h2 className=\"text-md md:text-2xl\">\n              At Sofa Society, we believe that a sofa is the heart of every\n              home.\n            </h2>\n          </LayoutColumn>\n          <LayoutColumn\n            start={{ base: 1, md: 8 }}\n            end={13}\n            className=\"mt-6 md:mt-19\"\n          >\n            <div className=\"md:text-md\">\n              <p className=\"mb-5 md:mb-9\">\n                We are dedicated to delivering high-quality, thoughtfully\n                designed sofas that merge comfort and style effortlessly.\n              </p>\n              <p className=\"mb-5 md:mb-3\">\n                Our mission is to transform your living space into a sanctuary\n                of relaxation and beauty, with products built to last.\n              </p>\n              <LocalizedLink href=\"/about\" variant=\"underline\">\n                Read more about Sofa Society\n              </LocalizedLink>\n            </div>\n          </LayoutColumn>\n        </Layout>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/privacy-policy/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport { StoreRegion } from \"@medusajs/types\"\nimport { listRegions } from \"@lib/data/regions\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\n\nexport const metadata: Metadata = {\n  title: \"Privacy Policy\",\n  description: \"Learn how we protect your privacy\",\n}\nexport async function generateStaticParams() {\n  const countryCodes = await listRegions().then((regions: StoreRegion[]) =>\n    regions.flatMap((r) =>\n      r.countries\n        ? r.countries\n            .map((c) => c.iso_2)\n            .filter(\n              (value): value is string =>\n                typeof value === \"string\" && Boolean(value)\n            )\n        : []\n    )\n  )\n\n  const staticParams = countryCodes.map((countryCode) => ({\n    countryCode,\n  }))\n\n  return staticParams\n}\n\nexport default function PrivacyPolicyPage() {\n  return (\n    <Layout className=\"pt-30 pb-20 md:pt-47 md:pb-32\">\n      <LayoutColumn\n        start={{ base: 1, lg: 2, xl: 3 }}\n        end={{ base: 13, lg: 11, xl: 10 }}\n      >\n        <h1 className=\"text-lg md:text-2xl mb-16 md:mb-25\">\n          Privacy Policy for Sofa Society\n        </h1>\n      </LayoutColumn>\n      <LayoutColumn\n        start={{ base: 1, lg: 2, xl: 3 }}\n        end={{ base: 13, lg: 10, xl: 9 }}\n        className=\"article\"\n      >\n        <p>\n          At Sofa Society, we value your privacy and are committed to protecting\n          your personal information. This Privacy Policy outlines how we\n          collect, use, disclose, and safeguard your data when you interact with\n          our website, services, and products. By using our platform, you\n          consent to the practices described in this policy.\n        </p>\n        <h2>1. Information We Collect:</h2>\n        <p>\n          We may collect personal information you provide directly to us, such\n          as:\n        </p>\n        <ul>\n          <li>\n            Name, email address, and contact details when you sign up for an\n            account.\n          </li>\n          <li>Billing and shipping addresses when you make a purchase.</li>\n          <li>\n            Payment information (credit/debit card details) for completing\n            transactions securely.\n          </li>\n          <li>Personal preferences and fashion interests you share with us.</li>\n        </ul>\n        <p>\n          Additionally, we may automatically collect certain information when\n          you access or use our website, including:\n        </p>\n        <ul>\n          <li>\n            IP address, browser type, operating system, and device information.\n          </li>\n          <li>\n            Usage data, such as pages visited, time spent on our platform, and\n            referring website.\n          </li>\n        </ul>\n        <h2>2. How We Use Your Information:</h2>\n        <p>\n          We may use your personal information for various purposes, including\n          but not limited to:\n        </p>\n        <ul>\n          <li>Providing and managing your account, purchases, and orders.</li>\n          <li>\n            Customizing your shopping experience and suggesting relevant\n            products.\n          </li>\n          <li>\n            Sending you updates, newsletters, and marketing communications (you\n            can opt-out anytime).\n          </li>\n          <li>Analyzing user behavior to improve our website and services.</li>\n          <li>\n            Complying with legal obligations and enforcing our Terms of Service.\n          </li>\n        </ul>\n        <h2>3. Cookies and Similar Technologies:</h2>\n        <p>\n          We use cookies and similar technologies to collect information about\n          your browsing activity on our website. These technologies help us\n          analyze usage patterns and enhance user experience. You can manage\n          your cookie preferences through your browser settings.\n        </p>\n        <h2>4. Data Sharing and Disclosure:</h2>\n        <p>\n          We may share your personal information with third parties under\n          certain circumstances, including:\n        </p>\n        <ul>\n          <li>\n            Service providers who assist us in operating our business and\n            delivering services.\n          </li>\n          <li>Legal authorities or government agencies as required by law.</li>\n        </ul>\n        <p>\n          We do not sell or rent your personal information to third parties for\n          their marketing purposes.\n        </p>\n        <h2>5. Data Security:</h2>\n        <p>\n          We implement reasonable security measures to protect your personal\n          information from unauthorized access, alteration, or disclosure.\n          However, no method of transmission over the internet or electronic\n          storage is completely secure.\n        </p>\n        <h2>6. Your Choices:</h2>\n        <p>You have the right to:</p>\n        <ul>\n          <li>\n            Review and update your personal information in your account\n            settings.\n          </li>\n          <li>Opt-out of receiving marketing communications.</li>\n          <li>\n            Delete your account (subject to applicable laws and regulations).\n          </li>\n        </ul>\n        <h2>7. Children&apos;s Privacy:</h2>\n        <p>\n          Our services are not intended for individuals under the age of 16. If\n          we become aware that we have collected personal information from\n          children without parental consent, we will take prompt action to\n          delete such data.\n        </p>\n        <h2>8. Changes to this Privacy Policy:</h2>\n        <p>\n          We may update this Privacy Policy from time to time to reflect changes\n          in our practices or for other operational, legal, or regulatory\n          reasons. We will notify you of any material changes via email or by\n          prominently posting a notice on our website.\n        </p>\n        <h2>9. Contact Us:</h2>\n        <p>\n          If you have any questions, concerns, or requests regarding this\n          Privacy Policy or how we handle your personal information, please\n          contact us at:\n        </p>\n        <p>\n          Email: privacy@sofasociety.com\n          <br />\n          Address: Skärgårdsvägen 12, 124 55 Stockholm\n        </p>\n      </LayoutColumn>\n    </Layout>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/products/[handle]/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport { notFound } from \"next/navigation\"\n\nimport { sdk } from \"@lib/config\"\nimport { getRegion, listRegions } from \"@lib/data/regions\"\nimport {\n  getProductByHandle,\n  getProductFashionDataByHandle,\n} from \"@lib/data/products\"\nimport ProductTemplate from \"@modules/products/templates\"\n\ntype Props = {\n  params: Promise<{ countryCode: string; handle: string }>\n}\n\nexport async function generateStaticParams() {\n  try {\n    const countryCodes = await listRegions().then(\n      (regions) =>\n        regions\n          ?.map((r) => r.countries?.map((c) => c.iso_2))\n          .flat()\n          .filter(Boolean) as string[]\n    )\n\n    if (!countryCodes) {\n      return []\n    }\n\n    const { products } = await sdk.store.product.list(\n      { fields: \"handle\" },\n      { next: { tags: [\"products\"] } }\n    )\n\n    const staticParams = countryCodes\n      ?.map((countryCode) =>\n        products.map((product) => ({\n          countryCode,\n          handle: product.handle,\n        }))\n      )\n      .flat()\n      .filter((product) => product.handle)\n\n    return staticParams\n  } catch (error) {\n    console.error(\n      `Failed to generate static paths for product pages: ${\n        error instanceof Error ? error.message : \"Unknown error\"\n      }.`\n    )\n    return []\n  }\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { handle, countryCode } = await params\n  const region = await getRegion(countryCode)\n\n  if (!region) {\n    notFound()\n  }\n\n  const product = await getProductByHandle(handle, region.id)\n\n  if (!product) {\n    notFound()\n  }\n\n  return {\n    title: `${product.title} | Medusa Store`,\n    description: `${product.title}`,\n    openGraph: {\n      title: `${product.title} | Medusa Store`,\n      description: `${product.title}`,\n      images: product.thumbnail ? [product.thumbnail] : [],\n    },\n  }\n}\n\nexport default async function ProductPage({ params }: Props) {\n  const { handle, countryCode } = await params\n  const region = await getRegion(countryCode)\n\n  if (!region) {\n    notFound()\n  }\n\n  const [pricedProduct, fashionData] = await Promise.all([\n    getProductByHandle(handle, region.id),\n    getProductFashionDataByHandle(handle),\n  ])\n\n  if (!pricedProduct) {\n    notFound()\n  }\n\n  return (\n    <ProductTemplate\n      product={pricedProduct}\n      materials={fashionData.materials}\n      region={region}\n      countryCode={countryCode}\n    />\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/search/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { Suspense } from \"react\"\nimport SkeletonProductGrid from \"@modules/skeletons/templates/skeleton-product-grid\"\nimport PaginatedProducts from \"@modules/store/templates/paginated-products\"\nimport { CollectionsSlider } from \"@modules/store/components/collections-slider\"\nimport { MeiliSearchProductHit, searchClient } from \"@lib/search-client\"\nimport { getRegion } from \"@lib/data/regions\"\n\ntype Props = {\n  params: Promise<{ countryCode: string }>\n  searchParams: Promise<{ query: string; page: string }>\n}\n\nexport const metadata: Metadata = {\n  title: \"Search\",\n  description: \"Search for products\",\n}\n\nexport default async function SearchPage({ params, searchParams }: Props) {\n  const { countryCode } = await params\n  const { query, page } = await searchParams\n\n  const pageNumber = page ? parseInt(page, 10) : 1\n\n  const results = await searchClient\n    .index(\"products\")\n    .search<MeiliSearchProductHit>(query)\n  const region = await getRegion(countryCode)\n\n  return (\n    <div className=\"md:pt-47 py-26 md:pb-36\">\n      <Layout>\n        <LayoutColumn>\n          <h2 className=\"mb-8 md:mb-16 text-lg md:text-2xl\">\n            Search results for &apos;{query}&apos;\n          </h2>\n        </LayoutColumn>\n      </Layout>\n      <Suspense fallback={<SkeletonProductGrid />}>\n        {region && (\n          <PaginatedProducts\n            sortBy=\"created_at\"\n            page={pageNumber}\n            countryCode={countryCode}\n            collectionId={undefined}\n            categoryId={undefined}\n            productsIds={results.hits.map((h) => h.id)}\n            typeId={undefined}\n          />\n        )}\n      </Suspense>\n      <CollectionsSlider\n        heading=\"Checkout our collections for more products\"\n        className=\"mt-26 md:mt-36 !mb-0\"\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/store/page.tsx",
    "content": "import { Metadata } from \"next\"\n\nimport { SortOptions } from \"@modules/store/components/refinement-list/sort-products\"\nimport StoreTemplate from \"@modules/store/templates\"\n\nexport const metadata: Metadata = {\n  title: \"Store\",\n  description: \"Explore all of our products.\",\n}\n\ntype Params = {\n  searchParams: Promise<{\n    sortBy?: SortOptions\n    collection?: string | string[]\n    category?: string | string[]\n    type?: string | string[]\n    page?: string\n  }>\n  params: Promise<{\n    countryCode: string\n  }>\n}\n\nexport default async function StorePage({ searchParams, params }: Params) {\n  const { countryCode } = await params\n  const { sortBy, page, collection, category, type } = await searchParams\n\n  return (\n    <StoreTemplate\n      sortBy={sortBy}\n      page={page}\n      countryCode={countryCode}\n      collection={\n        !collection\n          ? undefined\n          : Array.isArray(collection)\n            ? collection\n            : [collection]\n      }\n      category={\n        !category ? undefined : Array.isArray(category) ? category : [category]\n      }\n      type={!type ? undefined : Array.isArray(type) ? type : [type]}\n    />\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/[countryCode]/(main)/terms-of-use/page.tsx",
    "content": "import { Metadata } from \"next\"\nimport { StoreRegion } from \"@medusajs/types\"\nimport { listRegions } from \"@lib/data/regions\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\n\nexport const metadata: Metadata = {\n  title: \"Terms of Use\",\n  description: \"Learn about our terms of use\",\n}\nexport async function generateStaticParams() {\n  const countryCodes = await listRegions().then((regions: StoreRegion[]) =>\n    regions.flatMap((r) =>\n      r.countries\n        ? r.countries\n            .map((c) => c.iso_2)\n            .filter(\n              (value): value is string =>\n                typeof value === \"string\" && Boolean(value)\n            )\n        : []\n    )\n  )\n\n  const staticParams = countryCodes.map((countryCode) => ({\n    countryCode,\n  }))\n\n  return staticParams\n}\n\nexport default function TermsOfUsePage() {\n  return (\n    <Layout className=\"pt-30 pb-20 md:pt-47 md:pb-32\">\n      <LayoutColumn\n        start={{ base: 1, lg: 2, xl: 3 }}\n        end={{ base: 13, lg: 11, xl: 10 }}\n      >\n        <h1 className=\"text-lg md:text-2xl mb-16 md:mb-25\">\n          Terms of Use for Sofa Society\n        </h1>\n      </LayoutColumn>\n      <LayoutColumn\n        start={{ base: 1, lg: 2, xl: 3 }}\n        end={{ base: 13, lg: 10, xl: 9 }}\n        className=\"article\"\n      >\n        <p>\n          Welcome to Sofa Society. These Terms of Use govern your access to and\n          use of our website, products, and services. By accessing or using our\n          platform, you agree to be bound by these terms and conditions. If you\n          do not agree with any part of these terms, please do not use our\n          website.\n        </p>\n        <h2>1. Terms of Use:</h2>\n        <p>\n          All content and materials on our website, including text, graphics,\n          logos, images, videos, and trademarks, are the property of Sofa\n          Society or its licensors and are protected by intellectual property\n          laws. You may not use, reproduce, modify, or distribute any of our\n          content without our prior written permission.\n        </p>\n        <h2>2. Use of the Website:</h2>\n        <ol>\n          <li>\n            Eligibility: You must be at least 16 years old to use our website.\n            If you are under the age of 18, you should review these terms with a\n            parent or guardian to ensure their understanding and agreement.\n          </li>\n          <li>\n            User Account: Some features of our website may require you to create\n            an account. You are responsible for maintaining the confidentiality\n            of your account credentials and are solely responsible for any\n            activity that occurs under your account.\n          </li>\n          <li>\n            Prohibited Activities: You agree not to engage in any of the\n            following activities:\n            <ul>\n              <li>Violating any applicable laws or regulations.</li>\n              <li>\n                Impersonating any person or entity or falsely representing your\n                affiliation with any person or entity.\n              </li>\n              <li>\n                Interfering with or disrupting the functionality of our website\n                or servers.\n              </li>\n              <li>\n                Uploading or transmitting any viruses, malware, or other\n                malicious code.\n              </li>\n              <li>\n                Collecting or harvesting any information from our website\n                without our consent.\n              </li>\n            </ul>\n          </li>\n        </ol>\n        <h2>3. Third-Party Links and Content:</h2>\n        <p>\n          Our website may contain links to third-party websites or display\n          content from third parties. We do not endorse or control these\n          third-party websites or content, and your use of them is at your own\n          risk. We are not responsible for the accuracy, reliability, or\n          legality of any third-party websites or content.\n        </p>\n        <h2>4. Disclaimer of Warranties:</h2>\n        <p>\n          Our website is provided on an &quot;as is&quot; and &quot;as\n          available&quot; basis. We do not make any warranties, express or\n          implied, regarding the operation, availability, or accuracy of our\n          website or the content therein. Your use of our website is at your\n          sole risk.\n        </p>\n        <h2>5. Limitation of Liability:</h2>\n        <p>\n          To the maximum extent permitted by law, Sofa Society and its\n          affiliates, officers, directors, employees, and agents shall not be\n          liable for any direct, indirect, incidental, consequential, or special\n          damages arising out of or in connection with your use of our website,\n          even if advised of the possibility of such damages.\n        </p>\n        <h2>6. Indemnification:</h2>\n        <p>\n          You agree to indemnify, defend, and hold harmless Sofa Society and its\n          affiliates, officers, directors, employees, and agents from and\n          against any claims, liabilities, damages, losses, and expenses,\n          including reasonable attorney&apos;s fees, arising out of or in\n          connection with your use of our website or violation of these Terms of\n          Use.\n        </p>\n        <h2>7. Modifications to the Terms:</h2>\n        <p>\n          You agree to indemnify, defend, and hold harmless Sofa Society and its\n          affiliates, officers, directors, employees, and agents from and\n          against any claims, liabilities, damages, losses, and expenses,\n          including reasonable attorney&apos;s fees, arising out of or in\n          connection with your use of our website or violation of these Terms of\n          Use.\n        </p>\n        <h2>8. Governing Law and Jurisdiction:</h2>\n        <p>\n          These Terms of Use shall be governed by and construed in accordance\n          with the laws. Any disputes arising out of or in connection with these\n          terms shall be subject to the exclusive jurisdiction of the courts.\n        </p>\n      </LayoutColumn>\n    </Layout>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/layout.tsx",
    "content": "import { Metadata } from \"next\"\nimport { SpeedInsights } from \"@vercel/speed-insights/next\"\nimport { Mona_Sans } from \"next/font/google\"\nimport { getBaseURL } from \"@lib/util/env\"\n\nimport \"../styles/globals.css\"\nimport React from \"react\"\nimport { WebMCPProvider } from \"@lib/webmcp/WebMCPProvider\"\n\nexport const metadata: Metadata = {\n  metadataBase: new URL(getBaseURL()),\n}\n\nconst monaSans = Mona_Sans({\n  preload: true,\n  subsets: [\"latin\"],\n  style: [\"normal\", \"italic\"],\n  display: \"swap\",\n  weight: \"variable\",\n  variable: \"--font-mona-sans\",\n})\n\nexport default function RootLayout(props: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\" data-mode=\"light\" className=\"antialiased\">\n      <body className={`${monaSans.className}`}>\n        <main className=\"relative\">{props.children}</main>\n        <SpeedInsights />\n        <WebMCPProvider />\n      </body>\n    </html>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/not-found.tsx",
    "content": "import { Metadata } from \"next\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { LocalizedButtonLink } from \"@/components/LocalizedLink\"\nimport { Footer } from \"@/components/Footer\"\nimport { Header } from \"@/components/Header\"\n\nexport const metadata: Metadata = {\n  title: \"404\",\n  description: \"Something went wrong\",\n}\n\nexport default function NotFoundPage() {\n  return (\n    <>\n      <Header />\n      <Layout className=\"pt-30 pb-20 md:pt-47 md:pb-36\">\n        <LayoutColumn start={1} end={{ base: 13, lg: 7, xl: 8 }}>\n          <h1 className=\"text-xl md:text-3xl max-lg:mb-8 text-black\">\n            404\n            <br /> Page not found\n          </h1>\n        </LayoutColumn>\n        <LayoutColumn start={{ base: 1, lg: 7, xl: 8 }} end={13}>\n          <div className=\"md:text-md mb-8 lg:pt-18 text-black\">\n            <p>\n              The page you are looking for doesn&apos;t exist or an error\n              occurred. Go back, or head over to our home page.\n            </p>\n          </div>\n          <LocalizedButtonLink href=\"/\">Back to home</LocalizedButtonLink>\n        </LayoutColumn>\n      </Layout>\n      <Footer />\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/app/robots.ts",
    "content": "import { MetadataRoute } from \"next\"\n\nexport default function robots(): MetadataRoute.Robots {\n  if (process.env.DISALLOW_ROBOTS) {\n    return {\n      rules: {\n        userAgent: \"*\",\n        disallow: \"/\",\n      },\n    }\n  }\n\n  return {\n    rules: {\n      userAgent: \"*\",\n      allow: \"/\",\n      disallow: \"/private/\",\n    },\n  }\n}\n"
  },
  {
    "path": "storefront/src/components/Button.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\nimport * as ReactAria from \"react-aria-components\"\nimport Link, { LinkProps } from \"next/link\"\nimport { Icon, IconNames } from \"@/components/Icon\"\n\nexport type ButtonOwnProps = {\n  isFullWidth?: boolean\n  iconName?: IconNames\n  iconPosition?: \"start\" | \"end\"\n  isVisuallyDisabled?: boolean\n  isLoading?: boolean\n  loadingText?: string\n  size?: \"sm\" | \"md\"\n  spinnerPosition?: \"start\" | \"end\"\n  variant?: \"ghost\" | \"outline\" | \"solid\" | \"link\" | \"unstyled\"\n}\n\nexport const getButtonClassNames = ({\n  isFullWidth,\n  iconName,\n  iconPosition,\n  isVisuallyDisabled,\n  isLoading,\n  loadingText,\n  size,\n  spinnerPosition,\n  variant = \"solid\",\n}: ButtonOwnProps): string => {\n  const variantClasses = {\n    ghost: \"text-black h-auto disabled:text-grayscale-200\",\n    unstyled: \"text-black h-auto disabled:text-grayscale-200\",\n    outline:\n      \"text-black hover:text-grayscale-500 hover:border-grayscale-500 border border-black disabled:text-grayscale-200 disabled:border-grayscale-200\",\n    solid:\n      \"bg-black hover:bg-grayscale-500 text-white disabled:bg-grayscale-200\",\n    link: \"text-black h-auto border-b border-current px-0 rounded-none disabled:text-grayscale-200 hover:border-transparent\",\n  }\n\n  const visuallyDisabledClasses = isVisuallyDisabled\n    ? {\n        ghost: \"pointer-events-none text-grayscale-200\",\n        link: \"pointer-events-none text-grayscale-200\",\n        unstyled: \"pointer-events-none text-grayscale-200\",\n        outline: \"pointer-events-none border-grayscale-200 text-grayscale-200\",\n        solid: \"pointer-events-none bg-grayscale-200\",\n      }[variant]\n    : \"\"\n\n  const flexDirection =\n    iconPosition === \"end\" || spinnerPosition === \"end\"\n      ? \"flex-row-reverse\"\n      : \"\"\n  const hasGap = (isLoading && loadingText) || iconName\n  const sizeClasses =\n    size === \"sm\" ? \"px-4 h-8 text-xs\" : size === \"md\" ? \"px-6 h-12\" : \"\"\n\n  return twJoin(\n    \"inline-flex items-center focus-visible:outline-none rounded-xs justify-center transition-colors disabled:pointer-events-none\",\n    isFullWidth && \"w-full\",\n    flexDirection,\n    hasGap && \"gap-2\",\n    sizeClasses,\n    variantClasses[variant],\n    visuallyDisabledClasses\n  )\n}\n\nexport type ButtonProps = React.ComponentPropsWithoutRef<\"button\"> &\n  ButtonOwnProps &\n  ReactAria.ButtonProps\n\nexport const Button: React.FC<ButtonProps> = ({\n  isFullWidth,\n  isVisuallyDisabled,\n  iconName,\n  iconPosition = \"start\",\n  isLoading,\n  loadingText,\n  size = \"md\",\n  spinnerPosition = \"start\",\n  variant = \"solid\",\n  type = \"button\",\n  className,\n  children,\n  ...rest\n}) => (\n  <ReactAria.Button\n    {...rest}\n    type={type}\n    isPending={isLoading}\n    className={twMerge(\n      getButtonClassNames({\n        isFullWidth,\n        isVisuallyDisabled,\n        iconName,\n        iconPosition,\n        isLoading,\n        loadingText,\n        size,\n        spinnerPosition,\n        variant,\n      }),\n      className\n    )}\n  >\n    {Boolean(isLoading) && <Icon name=\"loader\" className=\"animate-spin\" />}\n    {iconName && !Boolean(isLoading) && <Icon name={iconName} />}\n    {Boolean(isLoading)\n      ? Boolean(loadingText)\n        ? loadingText\n        : null\n      : children}\n  </ReactAria.Button>\n)\n\nexport const ButtonAnchor: React.FC<\n  React.ComponentPropsWithoutRef<\"a\"> & ButtonOwnProps\n> = ({\n  isFullWidth,\n  isVisuallyDisabled,\n  iconName,\n  iconPosition = \"start\",\n  isLoading,\n  loadingText,\n  size = \"md\",\n  spinnerPosition = \"start\",\n  variant = \"solid\",\n  className,\n  children,\n  ...rest\n}) => (\n  <a\n    {...rest}\n    className={twMerge(\n      getButtonClassNames({\n        isFullWidth,\n        isVisuallyDisabled,\n        iconName,\n        iconPosition,\n        isLoading,\n        loadingText,\n        size,\n        spinnerPosition,\n        variant,\n      }),\n      className\n    )}\n  >\n    {Boolean(isLoading) && <Icon name=\"loader\" className=\"animate-spin\" />}\n    {iconName && !Boolean(isLoading) && <Icon name={iconName} />}\n    {Boolean(isLoading)\n      ? Boolean(loadingText)\n        ? loadingText\n        : null\n      : children}\n  </a>\n)\n\nexport const ButtonLink: React.FC<\n  Omit<LinkProps, \"passHref\"> &\n    ButtonOwnProps & {\n      className?: string\n      children?: React.ReactNode\n    }\n> = ({\n  isFullWidth,\n  isVisuallyDisabled,\n  iconName,\n  iconPosition = \"start\",\n  isLoading,\n  loadingText,\n  size = \"md\",\n  spinnerPosition = \"start\",\n  variant = \"solid\",\n  className,\n  children,\n  ...rest\n}) => (\n  <Link\n    {...rest}\n    className={twMerge(\n      getButtonClassNames({\n        isFullWidth,\n        isVisuallyDisabled,\n        iconName,\n        iconPosition,\n        isLoading,\n        loadingText,\n        size,\n        spinnerPosition,\n        variant,\n      }),\n      className\n    )}\n  >\n    {Boolean(isLoading) && <Icon name=\"loader\" className=\"animate-spin\" />}\n    {iconName && !Boolean(isLoading) && <Icon name={iconName} />}\n    {Boolean(isLoading)\n      ? Boolean(loadingText)\n        ? loadingText\n        : null\n      : children}\n  </Link>\n)\n"
  },
  {
    "path": "storefront/src/components/Carousel.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\nimport { EmblaCarouselType } from \"embla-carousel\"\nimport useEmblaCarousel from \"embla-carousel-react\"\nimport { Icon } from \"@/components/Icon\"\nimport { IconCircle } from \"@/components/IconCircle\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\n\nexport type CarouselProps = {\n  heading?: React.ReactNode\n  button?: React.ReactNode\n  arrows?: boolean\n} & React.ComponentPropsWithRef<\"div\">\n\nexport const Carousel: React.FC<CarouselProps> = ({\n  heading,\n  button,\n  arrows = true,\n  children,\n  className,\n}) => {\n  const [emblaRef, emblaApi] = useEmblaCarousel({\n    containScroll: \"trimSnaps\",\n    skipSnaps: true,\n    active: true,\n  })\n  const [prevBtnDisabled, setPrevBtnDisabled] = React.useState(true)\n  const [nextBtnDisabled, setNextBtnDisabled] = React.useState(true)\n\n  const scrollPrev = React.useCallback(\n    () => emblaApi && emblaApi.scrollPrev(),\n    [emblaApi]\n  )\n  const scrollNext = React.useCallback(\n    () => emblaApi && emblaApi.scrollNext(),\n    [emblaApi]\n  )\n  const onSelect = React.useCallback((emblaApi: EmblaCarouselType) => {\n    setPrevBtnDisabled(!emblaApi.canScrollPrev())\n    setNextBtnDisabled(!emblaApi.canScrollNext())\n  }, [])\n\n  React.useEffect(() => {\n    if (!emblaApi) return\n\n    onSelect(emblaApi)\n    emblaApi.on(\"reInit\", onSelect)\n    emblaApi.on(\"select\", onSelect)\n  }, [emblaApi, onSelect])\n\n  return (\n    <div className={twMerge(\"overflow-hidden\", className)}>\n      <Layout>\n        <LayoutColumn className=\"relative\">\n          <div className=\"mb-8 md:mb-15 flex max-sm:flex-col justify-between sm:items-center gap-x-10 gap-y-6\">\n            {heading}\n            {(arrows || button) && (\n              <div className=\"flex md:gap-6 shrink-0\">\n                {button}\n                {arrows && (\n                  <div className=\"flex gap-2\">\n                    <button\n                      type=\"button\"\n                      onClick={scrollPrev}\n                      disabled={prevBtnDisabled}\n                      className={twJoin(\n                        \"max-md:hidden transition-opacity\",\n                        prevBtnDisabled && \"opacity-50\"\n                      )}\n                      aria-label=\"Previous\"\n                    >\n                      <IconCircle>\n                        <Icon\n                          name=\"arrow-left\"\n                          className=\"w-6 h-6 text-black\"\n                        />\n                      </IconCircle>\n                    </button>\n                    <button\n                      type=\"button\"\n                      onClick={scrollNext}\n                      disabled={nextBtnDisabled}\n                      className={twJoin(\n                        \"max-md:hidden transition-opacity\",\n                        nextBtnDisabled && \"opacity-50\"\n                      )}\n                      aria-label=\"Next\"\n                    >\n                      <IconCircle>\n                        <Icon\n                          name=\"arrow-right\"\n                          className=\"w-6 h-6 text-black\"\n                        />\n                      </IconCircle>\n                    </button>\n                  </div>\n                )}\n              </div>\n            )}\n          </div>\n          <div ref={emblaRef}>\n            <div className=\"flex touch-pan-y gap-4 md:gap-10\">{children}</div>\n          </div>\n        </LayoutColumn>\n      </Layout>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/CartDrawer.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport Item from \"@modules/cart/components/item\"\nimport CartTotals from \"@modules/cart/components/cart-totals\"\nimport { LocalizedButtonLink, LocalizedLink } from \"@/components/LocalizedLink\"\nimport { Drawer } from \"@/components/Drawer\"\nimport { Button } from \"@/components/Button\"\nimport DiscountCode from \"@modules/cart/components/discount-code\"\nimport { Icon } from \"@/components/Icon\"\nimport { getCheckoutStep } from \"@modules/cart/utils/getCheckoutStep\"\nimport { useCart, useCartQuantity } from \"hooks/cart\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\nexport const CartDrawer = withReactQueryProvider(() => {\n  const [isCartDrawerOpen, setIsCartDrawerOpen] = React.useState(false)\n\n  const { data: cart, isPending } = useCart({ enabled: isCartDrawerOpen })\n\n  const step = getCheckoutStep(cart as HttpTypes.StoreCart)\n\n  const { data: quantity, isPending: pendingQuantity } = useCartQuantity()\n\n  return (\n    <>\n      <Button\n        onPress={() => setIsCartDrawerOpen(true)}\n        variant=\"ghost\"\n        className=\"p-1 group-data-[light=true]:md:text-white group-data-[sticky=true]:md:text-black\"\n        aria-label=\"Open cart\"\n      >\n        {pendingQuantity ? (\n          <Icon name=\"case\" className=\" w-6 h-6\" />\n        ) : (\n          <Icon\n            name=\"case\"\n            className=\" w-6 h-6\"\n            status={quantity && quantity > 0 ? quantity : undefined}\n          />\n        )}\n      </Button>\n      <Drawer\n        colorScheme=\"light\"\n        animateFrom=\"right\"\n        isOpen={isCartDrawerOpen}\n        onOpenChange={setIsCartDrawerOpen}\n        className=\"max-sm:max-w-100 max-w-139 max-sm:px-6 px-12 pt-10\"\n      >\n        {({ close }) => (\n          <>\n            <div className=\"flex justify-between mb-2\">\n              <div>\n                <p className=\"text-md\">Cart</p>\n              </div>\n              <button onClick={close} aria-label=\"Close cart\">\n                <Icon name=\"close\" className=\"w-6\" />\n              </button>\n            </div>\n            {cart?.items?.length ? (\n              <>\n                <div className=\"pb-8 pr-3 sm:pr-4 overflow-y-scroll\">\n                  {cart?.items\n                    .sort((a, b) => {\n                      return (a.created_at ?? \"\") > (b.created_at ?? \"\")\n                        ? -1\n                        : 1\n                    })\n                    .map((item) => {\n                      return (\n                        <Item\n                          key={item.id}\n                          item={item}\n                          className=\"py-8 last:pb-0 last:border-b-0\"\n                        />\n                      )\n                    })}\n                </div>\n                <div className=\"sticky left-0 bg-white bottom-0 pt-4 border-t border-grayscale-200 mt-auto\">\n                  <CartTotals isPartOfCartDrawer cart={cart} />\n                  <DiscountCode cart={cart} className=\"mt-6\" />\n                  <LocalizedButtonLink\n                    href={`/checkout/?step=${step}`}\n                    isFullWidth\n                    className=\"mt-4\"\n                  >\n                    Proceed to checkout\n                  </LocalizedButtonLink>\n                </div>\n              </>\n            ) : isPending ? (\n              <div className=\"flex align-middle justify-around items-center h-screen \">\n                <Icon name=\"loader\" className=\"w-10 md:w-15 animate-spin\" />\n              </div>\n            ) : (\n              <>\n                <p className=\"md:text-sm max-sm:mr-10 mb-6 mt-2\">\n                  You don&apos;t have anything in your cart. Let&apos;s change\n                  that, use the link below to start browsing our products.\n                </p>\n                <div>\n                  <LocalizedLink\n                    href=\"/store\"\n                    onClick={() => {\n                      setIsCartDrawerOpen(false)\n                    }}\n                  >\n                    Explore products\n                  </LocalizedLink>\n                </div>\n              </>\n            )}\n          </>\n        )}\n      </Drawer>\n    </>\n  )\n})\n"
  },
  {
    "path": "storefront/src/components/CartIcon.tsx",
    "content": "import { Suspense } from \"react\"\nimport { getCartQuantity } from \"@lib/data/cart\"\nimport { Icon, IconProps } from \"@/components/Icon\"\n\nconst CartIconWithQuantity: React.FC<\n  Omit<IconProps, \"status\" | \"name\">\n> = async (props) => {\n  const quantity = await getCartQuantity()\n\n  return (\n    <Icon name=\"case\" status={quantity > 0 ? quantity : undefined} {...props} />\n  )\n}\n\nexport const CartIcon: React.FC<Omit<IconProps, \"status\" | \"name\">> = (\n  props\n) => {\n  return (\n    <Suspense fallback={<Icon name=\"case\" {...props} />}>\n      <CartIconWithQuantity {...props} />\n    </Suspense>\n  )\n}"
  },
  {
    "path": "storefront/src/components/CollectionsSection.tsx",
    "content": "import Image from \"next/image\"\nimport { getCollectionsList } from \"@lib/data/collections\"\nimport { Carousel } from \"@/components/Carousel\"\nimport { LocalizedButtonLink, LocalizedLink } from \"@/components/LocalizedLink\"\n\nexport const CollectionsSection: React.FC<{ className?: string }> = async ({\n  className,\n}) => {\n  const collections = await getCollectionsList(0, 20, [\n    \"id\",\n    \"title\",\n    \"handle\",\n    \"metadata\",\n  ])\n\n  if (!collections) {\n    return null\n  }\n\n  return (\n    <Carousel\n      heading={<h3 className=\"text-md md:text-2xl\">Collections</h3>}\n      button={\n        <>\n          <LocalizedButtonLink\n            href=\"/store\"\n            size=\"md\"\n            className=\"h-full flex-1 max-md:hidden md:h-auto\"\n          >\n            View All\n          </LocalizedButtonLink>\n          <LocalizedButtonLink href=\"/store\" size=\"sm\" className=\"md:hidden\">\n            View All\n          </LocalizedButtonLink>\n        </>\n      }\n      className={className}\n    >\n      {collections.collections.map((collection) => (\n        <div\n          className=\"w-[70%] sm:w-[60%] lg:w-full max-w-124 flex-shrink-0\"\n          key={collection.id}\n        >\n          <LocalizedLink href={`/collections/${collection.handle}`}>\n            {typeof collection.metadata?.image === \"object\" &&\n              collection.metadata.image &&\n              \"url\" in collection.metadata.image &&\n              typeof collection.metadata.image.url === \"string\" && (\n                <div className=\"relative mb-4 md:mb-10 w-full aspect-[3/4]\">\n                  <Image\n                    src={collection.metadata.image.url}\n                    alt={collection.title}\n                    fill\n                  />\n                </div>\n              )}\n            <h3 className=\"md:text-lg mb-2 md:mb-4\">{collection.title}</h3>\n            {typeof collection.metadata?.description === \"string\" &&\n              collection.metadata?.description.length > 0 && (\n                <p className=\"text-xs text-grayscale-500 md:text-md\">\n                  {collection.metadata.description}\n                </p>\n              )}\n          </LocalizedLink>\n        </div>\n      ))}\n    </Carousel>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/Dialog.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ReactAria from \"react-aria-components\"\nimport { twMerge } from \"tailwind-merge\"\nimport { Button, ButtonProps } from \"@/components/Button\"\n\nexport const UiDialogTrigger: React.FC<ReactAria.DialogTriggerProps> = ({\n  children,\n  ...rest\n}) => <ReactAria.DialogTrigger {...rest}>{children}</ReactAria.DialogTrigger>\n\nexport const UiDialog: React.FC<ReactAria.DialogProps> = ({\n  children,\n  className,\n  ...rest\n}) => (\n  <ReactAria.Dialog\n    {...rest}\n    className={twMerge(\"focus-visible:outline-none\", className)}\n  >\n    {children}\n  </ReactAria.Dialog>\n)\n\nexport const UiCloseButton: React.FC<ButtonProps> = (props) => {\n  const { close } = React.useContext(ReactAria.OverlayTriggerStateContext)!\n\n  return <Button {...props} onPress={close} />\n}\n\nexport const UiConfirmButton: React.FC<\n  ButtonProps & { onConfirm: () => Promise<void> }\n> = (props) => {\n  const { close } = React.useContext(ReactAria.OverlayTriggerStateContext)!\n  const onPress = React.useCallback(\n    async (e: ReactAria.PressEvent) => {\n      await props.onConfirm()\n      close()\n      props.onPress?.(e)\n    },\n    [props, close]\n  )\n\n  return <Button {...props} onPress={onPress} />\n}\n"
  },
  {
    "path": "storefront/src/components/Drawer.tsx",
    "content": "import * as React from \"react\"\nimport { twMerge } from \"tailwind-merge\"\nimport * as ReactAria from \"react-aria-components\"\nimport { UiModal, UiModalOverlay, UiModalOwnProps } from \"@/components/ui/Modal\"\nimport { UiDialog } from \"@/components/Dialog\"\n\nexport interface DrawerProps\n  extends Omit<ReactAria.ModalOverlayProps, \"children\">,\n    UiModalOwnProps,\n    Pick<ReactAria.DialogProps, \"children\"> {\n  colorScheme?: \"light\" | \"dark\"\n  className?: string\n}\n\nexport const Drawer: React.FC<DrawerProps> = ({\n  colorScheme = \"dark\",\n  animateFrom,\n  className,\n  children,\n  ...rest\n}) => {\n  return (\n    <UiModalOverlay {...rest}>\n      <UiModal\n        animateFrom={animateFrom}\n        className={twMerge(\n          \"flex justify-self-center overflow-y-scroll max-h-screen h-screen max-w-75 rounded-none\",\n          colorScheme === \"light\"\n            ? \"bg-white text-black\"\n            : \"bg-black text-white\",\n          className\n        )}\n      >\n        <UiDialog className=\"flex flex-col flex-1\">{children}</UiDialog>\n      </UiModal>\n    </UiModalOverlay>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/Footer.tsx",
    "content": "\"use client\"\n\nimport { useParams, usePathname } from \"next/navigation\"\nimport { twMerge } from \"tailwind-merge\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { NewsletterForm } from \"@/components/NewsletterForm\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\n\nexport const Footer: React.FC = () => {\n  const pathName = usePathname()\n  const { countryCode } = useParams()\n  const currentPath = pathName.split(`/${countryCode}`)[1]\n\n  const isAuthPage = currentPath === \"/register\" || currentPath === \"/login\"\n\n  return (\n    <div\n      className={twMerge(\n        \"bg-grayscale-50 py-8 md:py-20\",\n        isAuthPage && \"hidden\"\n      )}\n    >\n      <Layout>\n        <LayoutColumn className=\"col-span-13\">\n          <div className=\"flex max-lg:flex-col justify-between md:gap-20 max-md:px-4\">\n            <div className=\"flex flex-1 max-lg:w-full max-lg:order-2 max-sm:flex-col justify-between sm:gap-30 lg:gap-20 md:items-center\">\n              <div className=\"max-w-35 md:flex-1 max-md:mb-9\">\n                <h1 className=\"text-lg md:text-xl mb-2 md:mb-6 leading-none md:leading-[0.9]\">\n                  Sofa Society Co.\n                </h1>\n                <p className=\"text-xs\">\n                  &copy; {new Date().getFullYear()}, Sofa Society\n                </p>\n              </div>\n              <div className=\"flex gap-10 xl:gap-18 max-md:text-xs flex-1 justify-between lg:justify-center\">\n                <ul className=\"flex flex-col gap-6 md:gap-3.5\">\n                  <li>\n                    <LocalizedLink href=\"/\">FAQ</LocalizedLink>\n                  </li>\n                  <li>\n                    <LocalizedLink href=\"/\">Help</LocalizedLink>\n                  </li>\n                  <li>\n                    <LocalizedLink href=\"/\">Delivery</LocalizedLink>\n                  </li>\n                  <li>\n                    <LocalizedLink href=\"/\">Returns</LocalizedLink>\n                  </li>\n                </ul>\n                <ul className=\"flex flex-col gap-6 md:gap-3.5\">\n                  <li>\n                    <a\n                      href=\"https://www.instagram.com/agiloltd/\"\n                      target=\"_blank\"\n                    >\n                      Instagram\n                    </a>\n                  </li>\n                  <li>\n                    <a href=\"https://tiktok.com\" target=\"_blank\">\n                      TikTok\n                    </a>\n                  </li>\n                  <li>\n                    <a href=\"https://pinterest.com\" target=\"_blank\">\n                      Pinterest\n                    </a>\n                  </li>\n                  <li>\n                    <a href=\"https://facebook.com\" target=\"_blank\">\n                      Facebook\n                    </a>\n                  </li>\n                </ul>\n                <ul className=\"flex flex-col gap-6 md:gap-3.5\">\n                  <li>\n                    <LocalizedLink href=\"/privacy-policy\">\n                      Privacy Policy\n                    </LocalizedLink>\n                  </li>\n                  <li>\n                    <LocalizedLink href=\"/cookie-policy\">\n                      Cookie Policy\n                    </LocalizedLink>\n                  </li>\n                  <li>\n                    <LocalizedLink href=\"/terms-of-use\">\n                      Terms of Use\n                    </LocalizedLink>\n                  </li>\n                </ul>\n              </div>\n            </div>\n\n            <NewsletterForm className=\"flex-1 max-lg:w-full lg:max-w-90 xl:max-w-96 max-lg:order-1 max-md:mb-16\" />\n          </div>\n        </LayoutColumn>\n      </Layout>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/Forms.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\nimport * as ReactAria from \"react-aria-components\"\nimport { Icon } from \"@/components/Icon\"\nimport {\n  FormProvider,\n  useForm,\n  UseFormProps,\n  DefaultValues,\n  UseFormReturn,\n  useController,\n  ControllerRenderProps,\n} from \"react-hook-form\"\nimport { z } from \"zod\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport CountrySelect from \"@modules/checkout/components/country-select\"\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type FormProps<T extends z.ZodType<any, any, any>> = UseFormProps<\n  z.infer<T>\n> & {\n  schema: T\n  onSubmit: (\n    values: z.infer<T>,\n    form: UseFormReturn<z.infer<T>>\n  ) => void | Promise<void>\n  defaultValues?: DefaultValues<z.infer<T>>\n  children?:\n    | React.ReactNode\n    | ((form: UseFormReturn<z.infer<T>>) => React.ReactNode)\n\n  formProps?: Omit<React.ComponentProps<\"form\">, \"onSubmit\">\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const Form = <T extends z.ZodType<any, any, any>>({\n  schema,\n  onSubmit,\n  children,\n  formProps,\n  ...props\n}: FormProps<T>) => {\n  const form = useForm({\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    resolver: zodResolver(schema as any),\n    ...props,\n  })\n\n  const submitHandler = React.useCallback(\n    (values: z.infer<T>) => {\n      return onSubmit(values, form)\n    },\n    [onSubmit, form]\n  )\n\n  const onFormSubmit: React.FormEventHandler<HTMLFormElement> =\n    React.useCallback(\n      (event) => {\n        event.preventDefault()\n        event.stopPropagation()\n        form.handleSubmit(submitHandler, (err) => console.error(err))(event)\n      },\n      [form, submitHandler]\n    )\n\n  return (\n    <FormProvider {...form}>\n      <form {...formProps} onSubmit={onFormSubmit}>\n        <fieldset disabled={form.formState.isSubmitting}>\n          {typeof children === \"function\" ? children(form) : children}\n        </fieldset>\n      </form>\n    </FormProvider>\n  )\n}\n\nexport const getInputClassNames = ({\n  uiSize = \"lg\",\n  isVisuallyDisabled,\n  isSuccess,\n}: InputOwnProps): string => {\n  const sizeClasses = {\n    sm: \"h-9 text-xs focus:pt-3.5 [&:not(:placeholder-shown)]:pt-3.5 [&:autofill]:pt-3.5\",\n    md: \"h-12 focus:pt-3 [&:not(:placeholder-shown)]:pt-3 [&:autofill]:pt-3\",\n    lg: \"h-14 focus:pt-4 [&:not(:placeholder-shown)]:pt-4 [&:autofill]:pt-4\",\n  }\n\n  const visuallyDisabledClasses = isVisuallyDisabled\n    ? \"pointer-events-none bg-grayscale-50\"\n    : \"\"\n\n  const successClasses = isSuccess ? \"border-green-500 pr-7\" : \"\"\n\n  return twJoin(\n    \"peer block w-full rounded-xs transition-all outline-none px-4 placeholder:invisible border border-grayscale-200 hover:border-grayscale-500 focus:border-grayscale-500 bg-transparent disabled:pointer-events-none disabled:bg-grayscale-50 [&:autofill]:bg-clip-text aria-[invalid=true]:border-red-primary aria-[invalid=true]:focus:border-red-900 aria-[invalid=true]:hover:border-red-900\",\n    sizeClasses[uiSize],\n    visuallyDisabledClasses,\n    successClasses\n  )\n}\n\nexport const getPlaceholderClassNames = ({\n  uiSize = \"lg\",\n}: Pick<InputOwnProps, \"uiSize\">): string => {\n  const sizeClasses = {\n    lg: \"peer-focus:top-2.5 peer-[:not(:placeholder-shown)]:top-2.5 peer-[:autofill]:top-2.5 peer-focus:text-xs peer-[:not(:placeholder-shown)]:text-xs peer-[:autofill]:text-xs\",\n    md: \"peer-focus:top-1 peer-[:not(:placeholder-shown)]:top-1 peer-[:autofill]:top-1 peer-focus:text-xs peer-[:not(:placeholder-shown)]:text-xs peer-[:autofill]:text-xs\",\n    sm: \"peer-focus:top-1 peer-[:not(:placeholder-shown)]:top-1 peer-[:autofill]:top-1 text-xs peer-focus:text-2xs peer-[:not(:placeholder-shown)]:text-2xs peer-[:autofill]:text-2xs\",\n  }\n\n  return twJoin(\n    \"absolute -translate-y-1/2 peer-placeholder-shown:top-1/2 left-4 peer-[:not(:placeholder-shown)]:translate-y-0 peer-[:autofill]:translate-y-0 peer-focus:translate-y-0 text-grayscale-400 pointer-events-none transition-all\",\n    sizeClasses[uiSize]\n  )\n}\n\n/**\n * Label\n */\ntype InputLabelOwnProps = {\n  isRequired?: boolean\n}\n\nexport const InputLabel: React.FC<\n  React.ComponentPropsWithRef<\"label\"> & InputLabelOwnProps\n> = ({ isRequired, children, className, ...rest }) => (\n  <ReactAria.Label\n    {...rest}\n    className={twMerge(\"mb-1 block font-semibold\", className)}\n  >\n    {children}\n    {isRequired && <span className=\"ml-0.5 text-orange-700\">*</span>}\n  </ReactAria.Label>\n)\n\n/**\n * SubLabel\n */\ntype InputSubLabelOwnProps = {\n  type: \"success\" | \"error\"\n}\n\nexport const InputSubLabel: React.FC<\n  React.ComponentPropsWithRef<\"p\"> & InputSubLabelOwnProps\n> = ({ type, children, className, ...rest }) => (\n  <ReactAria.Text\n    {...rest}\n    className={twMerge(\n      \"mt-2 text-xs\",\n      type === \"success\" && \"text-green-700\",\n      type === \"error\" && \"text-red-primary\",\n      className\n    )}\n  >\n    {children}\n  </ReactAria.Text>\n)\n\n/**\n * Input\n */\nexport type InputOwnProps = {\n  uiSize?: \"sm\" | \"md\" | \"lg\"\n  isVisuallyDisabled?: boolean\n  isSuccess?: boolean\n  errorMessage?: string\n  wrapperClassName?: string\n}\n\nexport const Input = React.forwardRef<\n  HTMLInputElement,\n  React.ComponentProps<\"input\"> & InputOwnProps\n>(\n  (\n    {\n      uiSize = \"lg\",\n      isVisuallyDisabled,\n      isSuccess,\n      errorMessage,\n      wrapperClassName,\n      placeholder,\n      className,\n      ...rest\n    },\n    ref\n  ) => (\n    <div className={twMerge(\"relative\", wrapperClassName)}>\n      <ReactAria.Input\n        {...rest}\n        ref={ref}\n        className={twMerge(\n          getInputClassNames({\n            uiSize,\n            isVisuallyDisabled,\n            isSuccess,\n          }),\n          className\n        )}\n        placeholder={placeholder}\n      />\n      {placeholder && (\n        <span className={getPlaceholderClassNames({ uiSize })}>\n          {placeholder}\n        </span>\n      )}\n      {isSuccess && (\n        <Icon\n          name=\"check\"\n          className=\"absolute right-0 top-1/2 mr-4 -translate-y-1/2 text-green-500 w-6 h-auto\"\n        />\n      )}\n      {errorMessage && (\n        <InputSubLabel\n          type=\"error\"\n          className=\"hidden aria-[invalid=true]:block\"\n        >\n          {errorMessage}\n        </InputSubLabel>\n      )}\n    </div>\n  )\n)\n\nInput.displayName = \"Input\"\n\nexport interface InputFieldProps {\n  className?: string\n  name: string\n  placeholder?: string\n  type?: React.ComponentProps<typeof Input>[\"type\"]\n  inputProps?: Omit<\n    React.ComponentProps<typeof Input>,\n    \"name\" | \"id\" | \"type\" | keyof ControllerRenderProps\n  >\n}\n\nexport const InputField: React.FC<InputFieldProps> = ({\n  className,\n  name,\n  type,\n  inputProps,\n  placeholder,\n}) => {\n  const { field, fieldState } = useController<{ __name__: string }, \"__name__\">(\n    { name: name as \"__name__\" }\n  )\n\n  return (\n    <div className={className}>\n      <Input\n        placeholder={placeholder}\n        {...inputProps}\n        {...field}\n        value={field.value ?? \"\"}\n        id={name}\n        type={type}\n        aria-invalid={Boolean(fieldState.error)}\n      />\n      {fieldState.error && (\n        <div className=\"pt-2 text-red-900 text-small-regular\">\n          <span>{fieldState.error.message}</span>\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport interface CountrySelectFieldProps {\n  className?: string\n  name: string\n  label?: string\n  selectProps?: Omit<\n    React.ComponentProps<typeof CountrySelect>,\n    \"name\" | \"id\" | keyof ControllerRenderProps\n  >\n  isRequired?: boolean\n  children?: React.ReactNode\n}\n\nexport const CountrySelectField: React.FC<CountrySelectFieldProps> = ({\n  className,\n  name,\n  selectProps,\n  children,\n}) => {\n  const { field, fieldState } = useController<{ __name__: string }, \"__name__\">(\n    { name: name as \"__name__\" }\n  )\n\n  return (\n    <div className={className}>\n      <CountrySelect\n        {...selectProps}\n        {...field}\n        selectedKey={field.value ?? \"\"}\n        name={name}\n      >\n        {children}\n      </CountrySelect>\n      {fieldState.error && (\n        <div className=\"pt-2 text-red-900 text-small-regular\">\n          <span>{fieldState.error.message}</span>\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/Header.tsx",
    "content": "import * as React from \"react\"\nimport { listRegions } from \"@lib/data/regions\"\nimport { SearchField } from \"@/components/SearchField\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { HeaderDrawer } from \"@/components/HeaderDrawer\"\nimport { RegionSwitcher } from \"@/components/RegionSwitcher\"\nimport { HeaderWrapper } from \"@/components/HeaderWrapper\"\n\nimport dynamic from \"next/dynamic\"\n\nconst LoginLink = dynamic(\n  () => import(\"@modules/header/components/LoginLink\"),\n  { loading: () => <></> }\n)\n\nconst CartDrawer = dynamic(\n  () => import(\"@/components/CartDrawer\").then((mod) => mod.CartDrawer),\n  { loading: () => <></> }\n)\n\nexport const Header: React.FC = async () => {\n  const regions = await listRegions()\n\n  const countryOptions = regions\n    .map((r) => {\n      return (r.countries ?? []).map((c) => ({\n        country: c.iso_2,\n        region: r.id,\n        label: c.display_name,\n      }))\n    })\n    .flat()\n    .sort((a, b) => (a?.label ?? \"\").localeCompare(b?.label ?? \"\"))\n\n  return (\n    <>\n      <HeaderWrapper>\n        <Layout>\n          <LayoutColumn>\n            <div className=\"flex justify-between items-center h-18 md:h-21\">\n              <h1 className=\"font-medium text-md\">\n                <LocalizedLink href=\"/\">SofaSocietyCo.</LocalizedLink>\n              </h1>\n              <div className=\"flex items-center gap-8 max-md:hidden\">\n                <LocalizedLink href=\"/about\">About</LocalizedLink>\n                <LocalizedLink href=\"/inspiration\">Inspiration</LocalizedLink>\n                <LocalizedLink href=\"/store\">Shop</LocalizedLink>\n              </div>\n              <div className=\"flex items-center gap-3 lg:gap-6 max-md:hidden\">\n                <RegionSwitcher\n                  countryOptions={countryOptions}\n                  className=\"w-16\"\n                  selectButtonClassName=\"h-auto !gap-0 !p-1 transition-none\"\n                  selectIconClassName=\"text-current\"\n                />\n                <React.Suspense>\n                  <SearchField countryOptions={countryOptions} />\n                </React.Suspense>\n                <LoginLink className=\"p-1 group-data-[light=true]:md:text-white group-data-[sticky=true]:md:text-black\" />\n                <CartDrawer />\n              </div>\n              <div className=\"flex items-center gap-4 md:hidden\">\n                <LoginLink className=\"p-1 group-data-[light=true]:md:text-white\" />\n                <CartDrawer />\n                <React.Suspense>\n                  <HeaderDrawer countryOptions={countryOptions} />\n                </React.Suspense>\n              </div>\n            </div>\n          </LayoutColumn>\n        </Layout>\n      </HeaderWrapper>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/HeaderDrawer.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Button } from \"@/components/Button\"\nimport { Icon } from \"@/components/Icon\"\nimport { Drawer } from \"@/components/Drawer\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { RegionSwitcher } from \"@/components/RegionSwitcher\"\nimport { SearchField } from \"@/components/SearchField\"\nimport { useSearchParams } from \"next/navigation\"\n\nexport const HeaderDrawer: React.FC<{\n  countryOptions: {\n    country: string | undefined\n    region: string\n    label: string | undefined\n  }[]\n}> = ({ countryOptions }) => {\n  const [isMenuOpen, setIsMenuOpen] = React.useState(false)\n\n  const searchParams = useSearchParams()\n  const searchQuery = searchParams.get(\"query\")\n\n  React.useEffect(() => {\n    if (searchQuery) setIsMenuOpen(false)\n  }, [searchQuery])\n\n  return (\n    <>\n      <Button\n        variant=\"ghost\"\n        className=\"p-1 group-data-[light=true]:md:text-white\"\n        onPress={() => setIsMenuOpen(true)}\n        aria-label=\"Open menu\"\n      >\n        <Icon name=\"menu\" className=\"w-6 h-6\" wrapperClassName=\"w-6 h-6\" />\n      </Button>\n      <Drawer\n        animateFrom=\"left\"\n        isOpen={isMenuOpen}\n        onOpenChange={setIsMenuOpen}\n        className=\"rounded-none !p-0\"\n      >\n        {({ close }) => (\n          <>\n            <div className=\"flex flex-col text-white h-full\">\n              <div className=\"flex items-center justify-between pb-6 mb-8 pt-5 w-full border-b border-white px-8\">\n                <SearchField\n                  countryOptions={countryOptions}\n                  isInputAlwaysShown\n                />\n                <button onClick={close} aria-label=\"Close menu\">\n                  <Icon name=\"close\" className=\"w-6\" />\n                </button>\n              </div>\n              <div className=\"text-lg flex flex-col gap-8 font-medium px-8\">\n                <LocalizedLink\n                  href=\"/about\"\n                  onClick={() => setIsMenuOpen(false)}\n                >\n                  About\n                </LocalizedLink>\n                <LocalizedLink\n                  href=\"/inspiration\"\n                  onClick={() => setIsMenuOpen(false)}\n                >\n                  Inspiration\n                </LocalizedLink>\n                <LocalizedLink\n                  href=\"/store\"\n                  onClick={() => setIsMenuOpen(false)}\n                >\n                  Shop\n                </LocalizedLink>\n              </div>\n              <RegionSwitcher\n                countryOptions={countryOptions}\n                className=\"mt-auto ml-8 mb-8\"\n                selectButtonClassName=\"max-md:text-base gap-2 p-1 w-auto\"\n                selectIconClassName=\"text-current w-6 h-6\"\n              />\n            </div>\n          </>\n        )}\n      </Drawer>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/HeaderWrapper.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { usePathname } from \"next/navigation\"\nimport { useCountryCode } from \"hooks/country-code\"\n\nexport const HeaderWrapper: React.FC<{ children?: React.ReactNode }> = ({\n  children,\n}) => {\n  const pathName = usePathname()\n  const countryCode = useCountryCode()\n  const currentPath = countryCode\n    ? pathName.split(`/${countryCode}`)[1]\n    : pathName\n  const isPageWithHeroImage =\n    !currentPath ||\n    currentPath === \"/\" ||\n    currentPath === \"/about\" ||\n    currentPath === \"/inspiration\" ||\n    currentPath.startsWith(\"/collections\")\n  const isAlwaysSticky =\n    currentPath.startsWith(\"/auth\") || currentPath.startsWith(\"/account\")\n\n  React.useEffect(() => {\n    if (isAlwaysSticky) {\n      return\n    }\n\n    const headerElement = document.querySelector(\"#site-header\")\n\n    if (!headerElement) {\n      return\n    }\n\n    const nextElement = headerElement.nextElementSibling\n    let triggerPosition = 0\n\n    const updateTriggerPosition = () => {\n      if (isPageWithHeroImage) {\n        triggerPosition = nextElement\n          ? Math.max(nextElement.clientHeight - headerElement.clientHeight, 1)\n          : 200\n      } else {\n        triggerPosition = nextElement\n          ? Math.max(\n              Number.parseInt(\n                window.getComputedStyle(nextElement).paddingTop,\n                10\n              ) - headerElement.clientHeight,\n              1\n            )\n          : 1\n      }\n    }\n\n    const handleScroll = () => {\n      const position = window.scrollY\n\n      headerElement.setAttribute(\n        \"data-sticky\",\n        position > triggerPosition ? \"true\" : \"false\"\n      )\n    }\n\n    updateTriggerPosition()\n    handleScroll()\n\n    window.addEventListener(\"resize\", updateTriggerPosition, {\n      passive: true,\n    })\n    window.addEventListener(\"orientationchange\", updateTriggerPosition, {\n      passive: true,\n    })\n    window.addEventListener(\"scroll\", handleScroll, {\n      passive: true,\n    })\n\n    return () => {\n      window.removeEventListener(\"resize\", updateTriggerPosition)\n      window.removeEventListener(\"orientationchange\", updateTriggerPosition)\n      window.removeEventListener(\"scroll\", handleScroll)\n    }\n  }, [pathName, isPageWithHeroImage, isAlwaysSticky])\n\n  return (\n    <div\n      id=\"site-header\"\n      className=\"top-0 left-0 w-full max-md:bg-grayscale-50 data-[light=true]:md:text-white data-[sticky=true]:md:bg-white data-[sticky=true]:md:text-black transition-colors fixed z-40 group\"\n      data-light={isPageWithHeroImage}\n      data-sticky={isAlwaysSticky}\n    >\n      {children}\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/Icon.tsx",
    "content": "import * as React from \"react\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\nimport { ArrowLeft } from \"@/components/icons/ArrowLeft\"\nimport { ArrowRight } from \"@/components/icons/ArrowRight\"\nimport { ArrowUpRight } from \"@/components/icons/ArrowUpRight\"\nimport { Calendar } from \"@/components/icons/Calendar\"\nimport { Case } from \"@/components/icons/Case\"\nimport { Check } from \"@/components/icons/Check\"\nimport { ChevronDown } from \"@/components/icons/ChevronDown\"\nimport { ChevronLeft } from \"@/components/icons/ChevronLeft\"\nimport { ChevronRight } from \"@/components/icons/ChevronRight\"\nimport { ChevronUp } from \"@/components/icons/ChevronUp\"\nimport { Close } from \"@/components/icons/Close\"\nimport { CreditCard } from \"@/components/icons/CreditCard\"\nimport { Heart } from \"@/components/icons/Heart\"\nimport { Info } from \"@/components/icons/Info\"\nimport { Loader } from \"@/components/icons/Loader\"\nimport { MapPin } from \"@/components/icons/MapPin\"\nimport { Menu } from \"@/components/icons/Menu\"\nimport { Minus } from \"@/components/icons/Minus\"\nimport { Package } from \"@/components/icons/Package\"\nimport { Plus } from \"@/components/icons/Plus\"\nimport { Receipt } from \"@/components/icons/Receipt\"\nimport { Search } from \"@/components/icons/Search\"\nimport { Sliders } from \"@/components/icons/Sliders\"\nimport { Trash } from \"@/components/icons/Trash\"\nimport { Truck } from \"@/components/icons/Truck\"\nimport { Undo } from \"@/components/icons/Undo\"\nimport { User } from \"@/components/icons/User\"\n\nexport type IconNames =\n  | \"arrow-left\"\n  | \"arrow-right\"\n  | \"arrow-up-right\"\n  | \"calendar\"\n  | \"case\"\n  | \"check\"\n  | \"chevron-down\"\n  | \"chevron-left\"\n  | \"chevron-right\"\n  | \"chevron-up\"\n  | \"close\"\n  | \"credit-card\"\n  | \"heart\"\n  | \"info\"\n  | \"loader\"\n  | \"map-pin\"\n  | \"menu\"\n  | \"minus\"\n  | \"package\"\n  | \"plus\"\n  | \"receipt\"\n  | \"search\"\n  | \"sliders\"\n  | \"trash\"\n  | \"truck\"\n  | \"undo\"\n  | \"user\"\n\nconst baseClasses = \"w-4 h-auto shrink-0\"\n\nexport type IconProps = React.ComponentPropsWithoutRef<\"svg\"> & {\n  name: IconNames\n  status?: number\n  wrapperClassName?: string\n}\n\nexport const Icon: React.FC<IconProps> = ({\n  name,\n  status = 0,\n  wrapperClassName,\n  className,\n  ...rest\n}) => (\n  <div className={twMerge(\"relative shrink-0\", wrapperClassName)}>\n    {Boolean(status) && (\n      <div\n        className={twJoin(\n          \"absolute -right-1 -top-0.5 leading-none rounded-full flex items-center justify-center w-4 h-4 bg-black text-white text-[0.625rem]\",\n          status > 99 && \"!text-[0.5rem]\"\n        )}\n      >\n        <span>{status > 99 ? \"+99\" : status}</span>\n      </div>\n    )}\n    {name === \"arrow-left\" && (\n      <ArrowLeft {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"arrow-right\" && (\n      <ArrowRight {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"arrow-up-right\" && (\n      <ArrowUpRight {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"calendar\" && (\n      <Calendar {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"case\" && (\n      <Case {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"check\" && (\n      <Check {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"chevron-down\" && (\n      <ChevronDown {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"chevron-left\" && (\n      <ChevronLeft {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"chevron-right\" && (\n      <ChevronRight {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"chevron-up\" && (\n      <ChevronUp {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"close\" && (\n      <Close {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"credit-card\" && (\n      <CreditCard {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"heart\" && (\n      <Heart {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"info\" && (\n      <Info {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"loader\" && (\n      <Loader {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"map-pin\" && (\n      <MapPin {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"menu\" && (\n      <Menu {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"minus\" && (\n      <Minus {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"package\" && (\n      <Package {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"plus\" && (\n      <Plus {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"receipt\" && (\n      <Receipt {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"search\" && (\n      <Search {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"sliders\" && (\n      <Sliders {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"trash\" && (\n      <Trash {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"truck\" && (\n      <Truck {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"undo\" && (\n      <Undo {...rest} className={twMerge(baseClasses, className)} />\n    )}\n    {name === \"user\" && (\n      <User {...rest} className={twMerge(baseClasses, className)} />\n    )}\n  </div>\n)\n"
  },
  {
    "path": "storefront/src/components/IconCircle.tsx",
    "content": "import * as React from \"react\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport const IconCircle: React.FC<React.ComponentPropsWithRef<\"div\">> = ({\n  className,\n  ...rest\n}) => (\n  <div\n    {...rest}\n    className={twMerge(\n      \"inline-flex h-10 w-10 items-center justify-center rounded-full border border-black\",\n      className\n    )}\n  />\n)\n"
  },
  {
    "path": "storefront/src/components/InputNumberField.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ReactAria from \"react-aria-components\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\nimport { Icon } from \"@/components/Icon\"\n\ntype InputNumberFieldProps = Omit<\n  ReactAria.InputProps,\n  \"type\" | \"size\" | \"value\" | \"defaultValue\" | \"onChange\"\n> & {\n  size?: \"sm\" | \"base\"\n  value?: number\n  onChange?: (value: number) => void\n  onCommit?: (value: number) => void\n  minValue?: number\n  maxValue?: number\n  step?: number\n  isDisabled?: boolean\n}\n\nconst clampValue = (value: number, min?: number, max?: number) => {\n  if (typeof min === \"number\" && value < min) return min\n  if (typeof max === \"number\" && value > max) return max\n  return value\n}\n\nconst coerceInputValue = (value: string) => {\n  if (value.trim() === \"\") return null\n  const parsed = Number(value)\n  return Number.isNaN(parsed) ? null : parsed\n}\n\nexport const InputNumberField: React.FC<InputNumberFieldProps> = ({\n  size = \"base\",\n  className,\n  value,\n  onChange,\n  onCommit,\n  minValue,\n  maxValue,\n  step = 1,\n  isDisabled,\n  onBlur,\n  onFocus,\n  ...rest\n}) => {\n  const resolvedValue = typeof value === \"number\" ? value : undefined\n  const [inputValue, setInputValue] = React.useState(\n    resolvedValue === undefined ? \"\" : `${resolvedValue}`\n  )\n  const isFocusedRef = React.useRef(false)\n\n  React.useEffect(() => {\n    if (!isFocusedRef.current) {\n      setInputValue(resolvedValue === undefined ? \"\" : `${resolvedValue}`)\n    }\n  }, [resolvedValue])\n\n  const getBaseValue = React.useCallback(() => {\n    const parsed = coerceInputValue(inputValue)\n    if (parsed !== null) return parsed\n    if (typeof resolvedValue === \"number\") return resolvedValue\n    if (typeof minValue === \"number\") return minValue\n    return 0\n  }, [inputValue, minValue, resolvedValue])\n\n  const baseValue = getBaseValue()\n  const canDecrement =\n    typeof minValue === \"number\" ? baseValue > minValue : true\n  const canIncrement =\n    typeof maxValue === \"number\" ? baseValue < maxValue : true\n\n  const handleInputChange = React.useCallback(\n    (event: React.ChangeEvent<HTMLInputElement>) => {\n      const nextValue = event.target.value\n      setInputValue(nextValue)\n\n      if (!onChange) return\n      const parsed = coerceInputValue(nextValue)\n      if (parsed === null) return\n      onChange(clampValue(parsed, minValue, maxValue))\n    },\n    [maxValue, minValue, onChange]\n  )\n\n  const normalizeOnBlur = React.useCallback(() => {\n    if (!onChange) {\n      setInputValue(resolvedValue === undefined ? \"\" : `${resolvedValue}`)\n      return null\n    }\n\n    const parsed = coerceInputValue(inputValue)\n    if (parsed === null) {\n      const fallback = clampValue(baseValue, minValue, maxValue)\n      onChange(fallback)\n      setInputValue(`${fallback}`)\n      return fallback\n    }\n\n    const clamped = clampValue(parsed, minValue, maxValue)\n    if (clamped !== parsed) {\n      onChange(clamped)\n      setInputValue(`${clamped}`)\n    }\n    return clamped\n  }, [baseValue, inputValue, maxValue, minValue, onChange, resolvedValue])\n\n  const handleDecrement = React.useCallback(() => {\n    if (!onChange || isDisabled || !canDecrement) return\n    const nextValue = clampValue(baseValue - step, minValue, maxValue)\n    onChange(nextValue)\n    setInputValue(`${nextValue}`)\n    onCommit?.(nextValue)\n  }, [\n    canDecrement,\n    baseValue,\n    isDisabled,\n    maxValue,\n    minValue,\n    onChange,\n    onCommit,\n    step,\n  ])\n\n  const handleIncrement = React.useCallback(() => {\n    if (!onChange || isDisabled || !canIncrement) return\n    const nextValue = clampValue(baseValue + step, minValue, maxValue)\n    onChange(nextValue)\n    setInputValue(`${nextValue}`)\n    onCommit?.(nextValue)\n  }, [\n    canIncrement,\n    baseValue,\n    isDisabled,\n    maxValue,\n    minValue,\n    onChange,\n    onCommit,\n    step,\n  ])\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLInputElement>) => {\n      if (event.key === \"ArrowDown\") {\n        event.preventDefault()\n        handleDecrement()\n        return\n      }\n      if (event.key === \"ArrowUp\") {\n        event.preventDefault()\n        handleIncrement()\n        return\n      }\n      if (event.key === \"Enter\") {\n        event.preventDefault()\n        const committedValue = normalizeOnBlur()\n        if (committedValue !== null) {\n          onCommit?.(committedValue)\n        }\n      }\n    },\n    [handleDecrement, handleIncrement, normalizeOnBlur, onCommit]\n  )\n\n  return (\n    <div\n      className={twMerge(\n        \"flex justify-between border border-grayscale-200 rounded-xs\",\n        size === \"sm\" ? \"h-8 px-4\" : \"h-12 px-6\",\n        className as string\n      )}\n    >\n      <ReactAria.Button\n        onPress={handleDecrement}\n        isDisabled={isDisabled || !canDecrement}\n        className=\"disabled:text-grayscale-200 transition-colors shrink-0\"\n      >\n        <Icon\n          name=\"minus\"\n          className={twJoin(size === \"sm\" ? \"w-4 h-4\" : \"w-6 h-6\")}\n        />\n      </ReactAria.Button>\n      <ReactAria.Input\n        {...rest}\n        type=\"number\"\n        value={inputValue}\n        min={minValue}\n        max={maxValue}\n        step={step}\n        onChange={handleInputChange}\n        onKeyDown={handleKeyDown}\n        onFocus={(event) => {\n          isFocusedRef.current = true\n          onFocus?.(event)\n        }}\n        onBlur={(event) => {\n          isFocusedRef.current = false\n          const committedValue = normalizeOnBlur()\n          if (committedValue !== null) {\n            onCommit?.(committedValue)\n          }\n          onBlur?.(event)\n        }}\n        disabled={isDisabled}\n        className={twJoin(\n          \"disabled:text-grayscale-200 disabled:bg-transparent text-center focus-within:outline-none w-7 leading-none appearance-none [-moz-appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none\",\n          size === \"sm\" ? \"text-xs\" : \"text-sm\"\n        )}\n      />\n      <ReactAria.Button\n        onPress={handleIncrement}\n        isDisabled={isDisabled || !canIncrement}\n        className=\"disabled:text-grayscale-200 transition-colors shrink-0\"\n      >\n        <Icon\n          name=\"plus\"\n          className={twJoin(size === \"sm\" ? \"w-4 h-4\" : \"w-6 h-6\")}\n        />\n      </ReactAria.Button>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/Layout.tsx",
    "content": "import * as React from \"react\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\n\nexport const Layout = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentPropsWithRef<\"div\">\n>(({ className, ...rest }, ref) => (\n  <div\n    {...rest}\n    ref={ref}\n    className={twMerge(\n      \"mx-auto grid grid-cols-12 gap-x-4 md:gap-x-12 px-4 sm:container\",\n      className\n    )}\n  />\n))\n\nLayout.displayName = \"Layout\"\n\n// const fullConfig = resolveConfig(tailwindConfig);\n// const breakpointsNamesArray = Object.keys(fullConfig.theme.screens);\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst breakpointsNamesArray = [\"base\", \"xs\", \"sm\", \"md\", \"lg\", \"xl\"] as const\n\ntype BreakpointsNames = (typeof breakpointsNamesArray)[number]\ntype ColumnsNumbers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13\ntype LayoutOwnProps = {\n  start?: { [key in BreakpointsNames]?: ColumnsNumbers } | ColumnsNumbers\n  end?: { [key in BreakpointsNames]?: ColumnsNumbers } | ColumnsNumbers\n}\n\nexport const getLayoutColumnClasses = ({\n  start = 1,\n  end = 13,\n}: Pick<LayoutOwnProps, \"start\" | \"end\">): string => {\n  const startClasses =\n    typeof start === \"number\"\n      ? [`col-start-${start}`]\n      : Object.entries(start).map(([breakpoint, columns]) => {\n          if (breakpoint === \"base\") {\n            return `col-start-${columns}`\n          }\n          return `${breakpoint}:col-start-${columns}`\n        })\n\n  const endClasses =\n    typeof end === \"number\"\n      ? [`col-end-${end}`]\n      : Object.entries(end).map(([breakpoint, columns]) => {\n          if (breakpoint === \"base\") {\n            return `col-end-${columns}`\n          }\n          return `${breakpoint}:col-end-${columns}`\n        })\n\n  return twJoin(...startClasses, ...endClasses)\n}\n\nexport const LayoutColumn: React.FC<\n  React.ComponentPropsWithRef<\"div\"> & LayoutOwnProps\n> = ({ start = 1, end = 13, className, ...rest }) => {\n  return (\n    <div\n      {...rest}\n      className={twMerge(getLayoutColumnClasses({ start, end }), className)}\n    />\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/Link.tsx",
    "content": "import * as React from \"react\"\nimport NextLink, { LinkProps as NextLinkProps } from \"next/link\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\n\nexport type LinkOwnProps = {\n  variant?: \"underline\" | \"hover:underline\" | \"unstyled\"\n}\n\nexport const getLinkClassNames = ({ variant }: LinkOwnProps): string =>\n  twJoin(\n    variant !== \"unstyled\" && \"transition-colors\",\n    (variant === \"underline\" || variant === \"hover:underline\") &&\n      \"border-b border-current pb-0.5 md:pb-1\",\n    variant === \"hover:underline\" &&\n      \"border-transparent hover:border-current transition-colors\",\n    variant === \"underline\" && \"hover:border-transparent\"\n  )\n\nexport const Link = <RouteInferType,>({\n  variant = \"unstyled\",\n  className,\n  children,\n  ...rest\n}: React.ComponentPropsWithoutRef<\"a\"> &\n  NextLinkProps<RouteInferType> &\n  LinkOwnProps) => (\n  <NextLink\n    {...rest}\n    className={twMerge(getLinkClassNames({ variant }), className)}\n  >\n    {children}\n  </NextLink>\n)\n\nexport const Anchor: React.FC<\n  React.ComponentPropsWithoutRef<\"a\"> & LinkOwnProps\n> = ({ variant = \"unstyled\", className, children, ...rest }) => (\n  <a {...rest} className={twMerge(getLinkClassNames({ variant }), className)}>\n    {children}\n  </a>\n)\n"
  },
  {
    "path": "storefront/src/components/LocalizedLink.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { LinkProps } from \"next/link\"\nimport { useCountryCode } from \"hooks/country-code\"\nimport { Link, LinkOwnProps } from \"@/components/Link\"\nimport { ButtonLink, ButtonOwnProps } from \"@/components/Button\"\n\nexport const LocalizedLink = <RouteInferType,>({\n  children,\n  href,\n  ...props\n}: React.ComponentPropsWithoutRef<\"a\"> &\n  LinkProps<RouteInferType> &\n  LinkOwnProps) => {\n  const countryCode = useCountryCode()\n\n  return (\n    <Link {...props} href={countryCode ? `/${countryCode}${href}` : href}>\n      {children}\n    </Link>\n  )\n}\n\nexport const LocalizedButtonLink = <RouteInferType,>({\n  children,\n  href,\n  ...props\n}: ButtonOwnProps &\n  Omit<LinkProps<RouteInferType>, \"passHref\"> & {\n    className?: string\n    children?: React.ReactNode\n  }) => {\n  const countryCode = useCountryCode()\n\n  return (\n    <ButtonLink {...props} href={countryCode ? `/${countryCode}${href}` : href}>\n      {children}\n    </ButtonLink>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/NewsletterForm.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Button } from \"@/components/Button\"\nimport { Form, InputField } from \"@/components/Forms\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { z } from \"zod\"\n\nconst newsletterFormSchema = z.object({\n  email: z.string().min(3).email(),\n})\n\nexport const NewsletterForm: React.FC<{ className?: string }> = ({\n  className,\n}) => {\n  const [isSubmitted, setIsSubmitted] = React.useState(false)\n\n  return (\n    <div className={className}>\n      <h2 className=\"text-md md:text-lg mb-2 md:mb-1\">Join our newsletter</h2>\n      {isSubmitted ? (\n        <p className=\"max-md:text-xs\">\n          Thank you for subscribing to our newsletter!\n        </p>\n      ) : (\n        <>\n          <p className=\"max-md:text-xs mb-4\">\n            We will also send you our discount coupons!\n          </p>\n          <Form\n            onSubmit={() => {\n              setIsSubmitted(true)\n            }}\n            schema={newsletterFormSchema}\n          >\n            <div className=\"flex gap-2\">\n              <InputField\n                inputProps={{\n                  uiSize: \"sm\",\n                  className: \"rounded-xs\",\n                  autoComplete: \"email\",\n                }}\n                name=\"email\"\n                type=\"email\"\n                placeholder=\"Your email\"\n                className=\"mb-4 flex-1\"\n              />\n              <Button type=\"submit\" size=\"sm\" className=\"h-9 text-xs\">\n                Subscribe\n              </Button>\n            </div>\n          </Form>\n          <p className=\"text-xs text-grayscale-500\">\n            By subscribing you agree to with our{\" \"}\n            <LocalizedLink\n              href=\"/privacy-policy\"\n              variant=\"underline\"\n              className=\"!pb-0\"\n            >\n              Privacy Policy\n            </LocalizedLink>{\" \"}\n            and provide consent to receive updates from our company.\n          </p>\n        </>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/NumberField.tsx",
    "content": "\"use client\"\n\nimport * as ReactAria from \"react-aria-components\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\nimport { Icon } from \"@/components/Icon\"\n\nexport const NumberField: React.FC<\n  ReactAria.NumberFieldProps & {\n    size?: \"sm\" | \"base\"\n  }\n> = ({ size = \"base\", className, ...rest }) => (\n  <ReactAria.NumberField\n    {...rest}\n    className={twMerge(\n      \"flex justify-between border border-grayscale-200 rounded-xs\",\n      size === \"sm\" ? \"h-8 px-4\" : \"h-12 px-6\",\n      className as string\n    )}\n  >\n    <ReactAria.Button\n      slot=\"decrement\"\n      className=\"disabled:text-grayscale-200 transition-colors shrink-0\"\n    >\n      <Icon\n        name=\"minus\"\n        className={twJoin(size === \"sm\" ? \"w-4 h-4\" : \"w-6 h-6\")}\n      />\n    </ReactAria.Button>\n    <ReactAria.Input\n      className={twJoin(\n        \"disabled:text-grayscale-200 disabled:bg-transparent text-center focus-within:outline-none w-7 leading-none\",\n        size === \"sm\" ? \"text-xs\" : \"text-sm\"\n      )}\n    />\n    <ReactAria.Button\n      slot=\"increment\"\n      className=\"disabled:text-grayscale-200 transition-colors shrink-0\"\n    >\n      <Icon\n        name=\"plus\"\n        className={twJoin(size === \"sm\" ? \"w-4 h-4\" : \"w-6 h-6\")}\n      />\n    </ReactAria.Button>\n  </ReactAria.NumberField>\n)\n"
  },
  {
    "path": "storefront/src/components/ProductPageGallery.tsx",
    "content": "// TODO: Review this component.\n\n\"use client\"\n\nimport * as React from \"react\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\nimport { EmblaCarouselType } from \"embla-carousel\"\nimport useEmblaCarousel from \"embla-carousel-react\"\nimport { Icon } from \"@/components/Icon\"\nimport { IconCircle } from \"@/components/IconCircle\"\n\nexport const ProductPageGallery: React.FC<\n  React.ComponentPropsWithRef<\"div\">\n> = ({ children, className }) => {\n  const [emblaRef, emblaApi] = useEmblaCarousel({\n    containScroll: \"trimSnaps\",\n    skipSnaps: true,\n  })\n  const [prevBtnDisabled, setPrevBtnDisabled] = React.useState(true)\n  const [nextBtnDisabled, setNextBtnDisabled] = React.useState(true)\n\n  const [selectedIndex, setSelectedIndex] = React.useState(0)\n  const [scrollSnaps, setScrollSnaps] = React.useState<number[]>([])\n\n  const scrollPrev = React.useCallback(\n    () => emblaApi && emblaApi.scrollPrev(),\n    [emblaApi]\n  )\n  const scrollNext = React.useCallback(\n    () => emblaApi && emblaApi.scrollNext(),\n    [emblaApi]\n  )\n  const onSelect = React.useCallback((emblaApi: EmblaCarouselType) => {\n    setPrevBtnDisabled(!emblaApi.canScrollPrev())\n    setNextBtnDisabled(!emblaApi.canScrollNext())\n    setSelectedIndex(emblaApi.selectedScrollSnap())\n  }, [])\n  const onInit = React.useCallback((emblaApi: EmblaCarouselType) => {\n    setScrollSnaps(emblaApi.scrollSnapList())\n  }, [])\n\n  const onDotButtonClick = React.useCallback(\n    (index: number) => {\n      if (!emblaApi) return\n      emblaApi.scrollTo(index)\n    },\n    [emblaApi]\n  )\n\n  React.useEffect(() => {\n    if (!emblaApi) return\n\n    onInit(emblaApi)\n    onSelect(emblaApi)\n    emblaApi.on(\"reInit\", onInit).on(\"reInit\", onSelect).on(\"select\", onSelect)\n  }, [emblaApi, onInit, onSelect])\n\n  return (\n    <div className={twMerge(\"overflow-hidden relative\", className)}>\n      <div className=\"relative flex items-center p-0 lg:mb-6\">\n        <button\n          type=\"button\"\n          onClick={scrollPrev}\n          disabled={prevBtnDisabled}\n          className=\"transition-opacity absolute left-4 z-10 max-lg:hidden\"\n          aria-label=\"Previous\"\n        >\n          <IconCircle\n            className={twJoin(\n              \"bg-black text-white transition-colors\",\n              prevBtnDisabled && \"bg-transparent text-black\"\n            )}\n          >\n            <Icon name=\"arrow-left\" className=\"w-6 h-6\" />\n          </IconCircle>\n        </button>\n        <div ref={emblaRef} className=\"w-full\">\n          <div className=\"flex touch-pan-y gap-4\">\n            {React.Children.map(children, (child) => {\n              return (\n                <div className=\"w-full md:max-w-[80%] flex-shrink-0\">\n                  {child}\n                </div>\n              )\n            })}\n          </div>\n        </div>\n        <button\n          type=\"button\"\n          onClick={scrollNext}\n          disabled={nextBtnDisabled}\n          className=\"transition-opacity absolute right-4 z-10 max-lg:hidden\"\n          aria-label=\"Next\"\n        >\n          <IconCircle\n            className={twJoin(\n              \"bg-black text-white transition-colors\",\n              nextBtnDisabled && \"bg-transparent text-black\"\n            )}\n          >\n            <Icon name=\"arrow-right\" className=\"w-6 h-6\" />\n          </IconCircle>\n        </button>\n      </div>\n      <div className=\"flex justify-center max-lg:w-full max-lg:absolute max-lg:bottom-4\">\n        {scrollSnaps.map((_, index) => (\n          <button\n            // eslint-disable-next-line react/no-array-index-key\n            key={index}\n            onClick={() => onDotButtonClick(index)}\n            className=\"px-1.5\"\n          >\n            <span\n              className={twMerge(\n                \"border-b border-transparent transition-colors px-0.5\",\n                index === selectedIndex && \"border-black\"\n              )}\n            >\n              {index + 1}\n            </span>\n          </button>\n        ))}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/RegionSwitcher.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { usePathname } from \"next/navigation\"\nimport * as ReactAria from \"react-aria-components\"\nimport {\n  UiSelectButton,\n  UiSelectIcon,\n  UiSelectListBox,\n  UiSelectListBoxItem,\n  UiSelectValue,\n} from \"@/components/ui/Select\"\nimport { useCountryCode } from \"hooks/country-code\"\nimport { useUpdateRegion } from \"hooks/cart\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\nexport const RegionSwitcher = withReactQueryProvider<{\n  countryOptions: {\n    country: string | undefined\n    region: string\n    label: string | undefined\n  }[]\n  className?: string\n  selectButtonClassName?: string\n  selectIconClassName?: string\n}>(\n  ({\n    countryOptions,\n    className,\n    selectButtonClassName,\n    selectIconClassName,\n  }) => {\n    const pathName = usePathname()\n    const countryCode = useCountryCode(countryOptions)\n    let currentPath = pathName\n\n    const updateRegion = useUpdateRegion()\n\n    if (countryCode) {\n      currentPath = pathName.split(`/${countryCode}`)[1]\n    }\n\n    return (\n      <ReactAria.Select\n        selectedKey={`${countryCode}`}\n        onSelectionChange={(key) => {\n          updateRegion.mutate({ countryCode: `${key}`, currentPath })\n        }}\n        className={className}\n        aria-label=\"Select country\"\n      >\n        <UiSelectButton variant=\"ghost\" className={selectButtonClassName}>\n          <UiSelectValue>\n            {(item) =>\n              typeof item.selectedItem === \"object\" &&\n              item.selectedItem !== null &&\n              \"country\" in item.selectedItem &&\n              typeof item.selectedItem.country === \"string\"\n                ? item.selectedItem.country.toUpperCase()\n                : item.defaultChildren\n            }\n          </UiSelectValue>\n          <UiSelectIcon className={selectIconClassName} />\n        </UiSelectButton>\n        <ReactAria.Popover placement=\"bottom right\" className=\"max-w-61 w-full\">\n          <UiSelectListBox>\n            {countryOptions.map((country) => (\n              <UiSelectListBoxItem\n                key={country.country}\n                id={country.country}\n                value={country}\n              >\n                {country.label}\n              </UiSelectListBoxItem>\n            ))}\n          </UiSelectListBox>\n        </ReactAria.Popover>\n      </ReactAria.Select>\n    )\n  }\n)\n"
  },
  {
    "path": "storefront/src/components/SearchField.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ReactAria from \"react-aria-components\"\nimport { twJoin } from \"tailwind-merge\"\nimport { useAsyncList } from \"react-stately\"\nimport { Hit } from \"meilisearch\"\nimport { useRouter, useSearchParams } from \"next/navigation\"\nimport { useCountryCode } from \"hooks/country-code\"\nimport { MeiliSearchProductHit, searchClient } from \"@lib/search-client\"\nimport { getProductPrice } from \"@lib/util/get-product-price\"\nimport { getProductsById } from \"@lib/data/products\"\nimport Thumbnail from \"@modules/products/components/thumbnail\"\nimport { Button } from \"@/components/Button\"\nimport { Input } from \"@/components/Forms\"\nimport { Icon } from \"@/components/Icon\"\n\ninterface ListItem extends Hit<MeiliSearchProductHit> {\n  price: {\n    calculated_price_number: number\n    calculated_price: string\n    original_price_number: number | null\n    original_price: string\n    currency_code: string | null\n    price_type: string | null | undefined\n    percentage_diff: string\n  } | null\n}\n\nexport const SearchField: React.FC<{\n  countryOptions: {\n    country: string | undefined\n    region: string\n    label: string | undefined\n  }[]\n  isInputAlwaysShown?: boolean\n}> = ({ countryOptions, isInputAlwaysShown }) => {\n  const router = useRouter()\n  const [isInputShown, setIsInputShown] = React.useState(false)\n  const countryCode = useCountryCode()\n  const region = countryOptions.find((co) => co.country === countryCode)?.region\n  const searchParams = useSearchParams()\n  const searchQuery = searchParams.get(\"query\")\n\n  const list = useAsyncList<ListItem>({\n    getKey(item) {\n      return item.handle\n    },\n    load: async ({ filterText, signal }) => {\n      const results = await searchClient\n        .index(\"products\")\n        .search<MeiliSearchProductHit>(filterText, undefined, {\n          signal,\n        })\n      const medusaProducts = await getProductsById({\n        ids: results.hits.map((h) => h.id),\n        regionId: region!,\n      })\n\n      return {\n        items: results.hits.map((hit) => {\n          const product = medusaProducts.find((p) => p.id === hit.id)\n          return {\n            ...hit,\n            price: getProductPrice({\n              product: product!,\n            }).cheapestPrice,\n          }\n        }),\n        filterText,\n      }\n    },\n    initialFilterText: searchQuery ?? \"\",\n  })\n\n  const buttonPressHandle = React.useCallback(() => {\n    if (!isInputShown) {\n      setIsInputShown(true)\n    } else if (list.filterText) {\n      router.push(`/${countryCode}/search?query=${list.filterText}`)\n      if (!isInputAlwaysShown) setIsInputShown(false)\n    } else {\n      if (!isInputAlwaysShown) setIsInputShown(false)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isInputShown, list.filterText, router, countryCode])\n\n  const handleKeyDown = React.useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        if (!isInputAlwaysShown) setIsInputShown(false)\n      } else if (e.key === \"Enter\" && list.filterText) {\n        router.push(`/${countryCode}/search?query=${list.filterText}`)\n        if (!isInputAlwaysShown) setIsInputShown(false)\n      }\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [list.filterText, router, countryCode]\n  )\n\n  React.useEffect(() => {\n    if (searchQuery && !list.filterText) {\n      list.setFilterText(searchQuery)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [searchQuery])\n\n  React.useEffect(() => {\n    if (isInputAlwaysShown) setIsInputShown(true)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <div className=\"flex\">\n      <Button\n        onPress={buttonPressHandle}\n        variant=\"ghost\"\n        className=\"p-1 max-md:text-white group-data-[light=true]:md:text-white group-data-[sticky=true]:md:text-black\"\n        aria-label=\"Open search\"\n      >\n        <Icon name=\"search\" className=\"w-5 h-5\" />\n      </Button>\n      <ReactAria.ComboBox\n        allowsCustomValue\n        className=\"overflow-hidden\"\n        aria-label=\"Search\"\n        items={list.items}\n        inputValue={list.filterText}\n        onInputChange={list.setFilterText}\n        onKeyDown={handleKeyDown}\n        isDisabled={!isInputAlwaysShown && !isInputShown}\n      >\n        <div\n          className={twJoin(\n            \"overflow-hidden transition-width duration-500 h-full max-w-40 md:max-w-30\",\n            isInputShown ? \"w-full md:w-30\" : \"md:w-0\"\n          )}\n        >\n          <Input className=\"px-0 disabled:bg-transparent !py-0 h-7 md:h-6 max-md:border-0 border-black rounded-none border-t-0 border-x-0 group-data-[light=true]:md:border-white group-data-[sticky=true]:md:border-black ml-2 md:ml-1\" />\n        </div>\n        <ReactAria.Popover\n          placement=\"bottom end\"\n          containerPadding={10}\n          maxHeight={243}\n          offset={25}\n          className=\"max-w-90 md:max-w-95 lg:max-w-98 w-full bg-white rounded-xs border border-grayscale-200 overflow-y-scroll\"\n        >\n          <ReactAria.ListBox className=\"outline-none\">\n            {(item: ListItem) => (\n              <ReactAria.ListBoxItem\n                className=\"relative after:absolute after:content-[''] after:h-px after:bg-grayscale-100 after:-bottom-px after:left-6 after:right-6 last:after:hidden mb-px flex gap-6 p-6 transition-colors hover:bg-grayscale-50\"\n                key={item.handle}\n                id={item.handle}\n                href={`/${countryCode}/products/${item.handle}`}\n              >\n                <Thumbnail\n                  thumbnail={item.thumbnail}\n                  size=\"3/4\"\n                  className=\"w-20\"\n                />\n                <div>\n                  <p className=\"text-base font-normal\">{item.title}</p>\n                  <p className=\"text-grayscale-500 text-xs\">\n                    {item.variants[0]}\n                  </p>\n                </div>\n                <p className=\"text-base font-semibold ml-auto\">\n                  {item.price?.calculated_price}\n                </p>\n              </ReactAria.ListBoxItem>\n            )}\n          </ReactAria.ListBox>\n        </ReactAria.Popover>\n      </ReactAria.ComboBox>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/components/icons/ArrowLeft.tsx",
    "content": "import * as React from \"react\"\n\nexport const ArrowLeft: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    strokeWidth=\"2\"\n  >\n    <path d=\"M19 12H5M12 19l-7-7 7-7\" />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/ArrowRight.tsx",
    "content": "import * as React from \"react\"\n\nexport const ArrowRight: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    strokeWidth=\"2\"\n  >\n    <path d=\"M5 12h14M12 5l7 7-7 7\" />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/ArrowUpRight.tsx",
    "content": "import * as React from \"react\"\n\nexport const ArrowUpRight: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <mask id=\"a\" fill=\"#fff\">\n      <path\n        fillRule=\"evenodd\"\n        d=\"M5.785 17.12 16.038 6.867l1.06 1.06L6.847 18.182l-1.06-1.06Z\"\n        clipRule=\"evenodd\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        d=\"M6.096 6.47h11.4v11.4h-1.5v-9.9h-9.9v-1.5Z\"\n        clipRule=\"evenodd\"\n      />\n    </mask>\n    <path\n      fill=\"currentColor\"\n      d=\"m5.785 17.12-.707-.707a1 1 0 0 0 0 1.414l.707-.707ZM16.038 6.867l.707-.707a1 1 0 0 0-1.414 0l.707.707Zm1.06 1.06.708.708a1 1 0 0 0 0-1.414l-.707.707ZM6.847 18.182l-.707.707a1 1 0 0 0 1.414 0l-.707-.707Zm-.75-11.71v-1h-1v1h1Zm11.4 0h1v-1h-1v1Zm0 11.4v1h1v-1h-1Zm-1.5 0h-1v1h1v-1Zm0-9.9h1v-1h-1v1Zm-9.9 0h-1v1h1v-1Zm11.71-.75-1.06-1.06-1.415 1.413 1.06 1.061 1.415-1.414ZM5.078 17.827l1.06 1.061 1.415-1.414-1.06-1.06-1.415 1.413ZM15.331 6.16 5.078 16.413l1.414 1.414L16.745 7.574 15.331 6.16ZM7.553 18.888 17.806 8.635l-1.414-1.414L6.139 17.474l1.414 1.414Zm8.442-.018h1.5v-2h-1.5v2Zm-10.9-12.4v1.5h2v-1.5h-2Zm12.4-1h-11.4v2h11.4v-2Zm1 12.4V6.47h-2v11.4h2Zm-3.5-9.9v9.9h2v-9.9h-2Zm-8.9 1h9.9v-2h-9.9v2Z\"\n      mask=\"url(#a)\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Calendar.tsx",
    "content": "import * as React from \"react\"\n\nexport const Calendar: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      d=\"M5 4.25A1.75 1.75 0 0 0 3.25 6v2.75H3c-.086 0-.17.009-.25.025V6A2.25 2.25 0 0 1 5 3.75h1.75v.5H5Zm-2 5.5h.25v.5H3a.25.25 0 1 1 0-.5Zm1.25.5v-.5h15.5v.5H4.25Zm-1.25 1h.25V20c0 .966.784 1.75 1.75 1.75h14A1.75 1.75 0 0 0 20.75 20v-8.75H21c.086 0 .17-.009.25-.025V20A2.25 2.25 0 0 1 19 22.25H5A2.25 2.25 0 0 1 2.75 20v-8.775c.08.016.164.025.25.025Zm18-1h-.25v-.5H21a.25.25 0 1 1 0 .5Zm0-1.5h-.25V6A1.75 1.75 0 0 0 19 4.25h-1.75v-.5H19A2.25 2.25 0 0 1 21.25 6v2.775A1.257 1.257 0 0 0 21 8.75Zm-4.75-5v.5h-.5v-.5h.5Zm-.5 1.5h.5V6a.25.25 0 1 1-.5 0v-.75Zm-1-1.5v.5h-5.5v-.5h5.5Zm-6.5 0v.5h-.5v-.5h.5Zm-.5 1.5h.5V6a.25.25 0 0 1-.5 0v-.75Zm.5-2.5h-.5V2a.25.25 0 0 1 .5 0v.75Zm8 0h-.5V2a.25.25 0 1 1 .5 0v.75Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Case.tsx",
    "content": "import * as React from \"react\"\n\nexport const Case: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M18.5 8.875h-13v10h13v-10ZM4 7.375v13h16v-13H4Z\"\n      clipRule=\"evenodd\"\n    />\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M9.75 8.376 9 8.375h-.75v-.021a3.367 3.367 0 0 1 .007-.191c.007-.124.02-.298.046-.506.051-.411.155-.973.37-1.545.213-.57.551-1.19 1.094-1.673.559-.496 1.299-.814 2.233-.814.934 0 1.674.318 2.233.814.543.483.88 1.103 1.094 1.673.215.572.319 1.134.37 1.545a7.23 7.23 0 0 1 .052.654v.043l.001.014v.006l-.75.001h-.75V8.35l-.005-.106a5.717 5.717 0 0 0-.036-.4 5.413 5.413 0 0 0-.286-1.205c-.162-.43-.386-.81-.687-1.077-.285-.254-.67-.436-1.236-.436s-.951.182-1.236.436c-.3.267-.525.647-.687 1.077-.16.428-.244.866-.286 1.205a5.728 5.728 0 0 0-.04.506l-.001.024v.003Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Check.tsx",
    "content": "import * as React from \"react\"\n\nexport const Check: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      stroke=\"currentColor\"\n      strokeLinecap=\"square\"\n      strokeLinejoin=\"round\"\n      strokeWidth=\"2px\"\n      d=\"M9.354 16.948 20 6.302l.354.353L9.177 17.832a.25.25 0 0 1-.354 0l-5.176-5.177.353-.353 4.647 4.646a.5.5 0 0 0 .707 0Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/ChevronDown.tsx",
    "content": "import * as React from \"react\"\n\nexport const ChevronDown: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"m5.47 9.265 1.06-1.06 5.47 5.47 5.47-5.47 1.06 1.06-6.53 6.53-6.53-6.53Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/ChevronLeft.tsx",
    "content": "import * as React from \"react\"\n\nexport const ChevronLeft: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"m14.53 5 1.061 1.06-5.47 5.47 5.47 5.47-1.06 1.06L8 11.53 14.53 5Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/ChevronRight.tsx",
    "content": "import * as React from \"react\"\n\nexport const ChevronRight: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M9.06 18.06 8 17l5.47-5.47L8 6.06 9.06 5l6.531 6.53-6.53 6.53Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/ChevronUp.tsx",
    "content": "import * as React from \"react\"\n\nexport const ChevronUp: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"m18.53 14.735-1.06 1.06-5.47-5.47-5.47 5.47-1.06-1.06L12 8.205l6.53 6.53Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Close.tsx",
    "content": "import * as React from \"react\"\n\nexport const Close: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      stroke=\"currentColor\"\n      strokeLinecap=\"square\"\n      strokeLinejoin=\"round\"\n      d=\"m18.354 6-5.293 5.293-.354-.354L18 5.646l.354.354ZM6 5.646l5.293 5.293-.353.354L5.647 6 6 5.646Zm6 6 .354.354-.354.354-.353-.354.353-.354Zm.707 1.415.354-.354L18.354 18l-.354.354-5.293-5.293Zm-1.768-.354.354.354L6 18.354 5.647 18l5.293-5.293Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/CreditCard.tsx",
    "content": "import * as React from \"react\"\n\nexport const CreditCard: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      d=\"M4 5.25A1.75 1.75 0 0 0 2.25 7v1.75H2c-.086 0-.17.009-.25.025V7A2.25 2.25 0 0 1 4 4.75h16A2.25 2.25 0 0 1 22.25 7v1.775A1.257 1.257 0 0 0 22 8.75h-.25V7A1.75 1.75 0 0 0 20 5.25H4Zm-2 4.5h.25v.5H2a.25.25 0 1 1 0-.5Zm1.25.5v-.5h17.5v.5H3.25Zm-1.25 1h.25V17c0 .966.784 1.75 1.75 1.75h16A1.75 1.75 0 0 0 21.75 17v-5.75H22c.086 0 .17-.009.25-.025V17A2.25 2.25 0 0 1 20 19.25H4A2.25 2.25 0 0 1 1.75 17v-5.775c.08.016.164.025.25.025Zm20-1h-.25v-.5H22a.25.25 0 1 1 0 .5Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Heart.tsx",
    "content": "import * as React from \"react\"\n\nexport const Heart: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M3.08058 3.92525C4.25269 2.75315 5.8424 2.09467 7.5 2.09467C8.45514 2.09467 9.31239 2.23105 10.147 2.59455C10.7904 2.8748 11.3924 3.27787 12 3.81442C12.6076 3.27787 13.2096 2.8748 13.853 2.59455C14.6876 2.23105 15.5449 2.09467 16.5 2.09467C18.1576 2.09467 19.7473 2.75315 20.9194 3.92525C22.0915 5.09735 22.75 6.68706 22.75 8.34467C22.75 10.9736 21.0154 12.9196 19.5276 14.3777L12 21.9053L4.47414 14.3795C2.97096 12.9258 1.25 10.9818 1.25 8.34467C1.25 6.68706 1.90848 5.09735 3.08058 3.92525ZM7.5 3.59467C6.24022 3.59467 5.03204 4.09511 4.14124 4.98591C3.25045 5.87671 2.75 7.08489 2.75 8.34467C2.75 10.3048 4.02553 11.8595 5.52127 13.3054L5.53041 13.3143L12 19.784L18.4751 13.3089C19.9667 11.8473 21.25 10.2941 21.25 8.34467C21.25 7.08489 20.7496 5.87671 19.8588 4.98591C18.968 4.09511 17.7598 3.59467 16.5 3.59467C15.6951 3.59467 15.0524 3.70828 14.452 3.96978C13.8467 4.23339 13.2376 4.66774 12.5303 5.375L12 5.90533L11.4697 5.375C10.7624 4.66774 10.1533 4.23339 9.54802 3.96978C8.94761 3.70828 8.30486 3.59467 7.5 3.59467Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Info.tsx",
    "content": "import * as React from \"react\"\n\nexport const Info: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n  >\n    <path\n      stroke=\"currentColor\"\n      strokeLinecap=\"square\"\n      strokeLinejoin=\"round\"\n      d=\"M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25ZM1.75 12C1.75 6.34 6.34 1.75 12 1.75S22.25 6.34 22.25 12 17.66 22.25 12 22.25 1.75 17.66 1.75 12Zm10.5-.25v4.5h-.5v-4.5h.5Zm-.5-4h.51v.5h-.51v-.5Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Loader.tsx",
    "content": "import * as React from \"react\"\n\nexport const Loader: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 25 25\"\n    fill=\"none\"\n  >\n    <path\n      d=\"M25 12.5C25 19.4036 19.4036 25 12.5 25C5.59644 25 0 19.4036 0 12.5C0 5.59644 5.59644 0 12.5 0C19.4036 0 25 5.59644 25 12.5ZM3.125 12.5C3.125 17.6777 7.32233 21.875 12.5 21.875C17.6777 21.875 21.875 17.6777 21.875 12.5C21.875 7.32233 17.6777 3.125 12.5 3.125C7.32233 3.125 3.125 7.32233 3.125 12.5Z\"\n      opacity=\"0.3\"\n      fill=\"currentColor\"\n    />\n    <path\n      d=\"M1.5625 12.5C0.699555 12.5 -0.0100737 11.7977 0.0975115 10.9415C0.400499 8.53018 1.40194 6.24704 2.99493 4.3819C4.92787 2.11871 7.60492 0.61949 10.5446 0.153896C13.4842 -0.311698 16.4935 0.286891 19.0312 1.842C21.1226 3.1236 22.7806 4.98552 23.8139 7.18519C24.1808 7.96625 23.7229 8.85346 22.9022 9.12013V9.12013C22.0815 9.38679 21.2111 8.92899 20.812 8.16389C20.0309 6.66663 18.8548 5.39896 17.3984 4.5065C15.4951 3.34017 13.2382 2.89123 11.0334 3.24042C8.82869 3.58962 6.8209 4.71403 5.37119 6.41142C4.2619 7.71024 3.53507 9.27931 3.2549 10.9447C3.11173 11.7957 2.42544 12.5 1.5625 12.5V12.5Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/MapPin.tsx",
    "content": "import * as React from \"react\"\n\nexport const MapPin: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n  >\n    <path\n      d=\"M11.6841 21.4314C11.7355 21.4733 11.7931 21.5035 11.8535 21.5219C11.7535 21.5526 11.6643 21.6143 11.6 21.7L11.5974 21.7035L11.5947 21.707L11.5921 21.7105L11.5895 21.714L11.5869 21.7174L11.5844 21.7208L11.5818 21.7243L11.5792 21.7277L11.5767 21.7311L11.5741 21.7345L11.5716 21.7378L11.5691 21.7412L11.5666 21.7446L11.5641 21.7479L11.5616 21.7512L11.5591 21.7545L11.5566 21.7578L11.5542 21.7611L11.5517 21.7643L11.5493 21.7676L11.5469 21.7708L11.5444 21.7741L11.542 21.7773L11.5396 21.7805L11.5372 21.7837L11.5349 21.7868L11.5325 21.79L11.5301 21.7932L11.5278 21.7963L11.5254 21.7994L11.5231 21.8025L11.5208 21.8056L11.5185 21.8087L11.5162 21.8118L11.5139 21.8148L11.5116 21.8179L11.5093 21.8209L11.5071 21.8239L11.5048 21.8269L11.5026 21.8299L11.5003 21.8329L11.4981 21.8359L11.4959 21.8388L11.4937 21.8418L11.4915 21.8447L11.4893 21.8476L11.4871 21.8505L11.4849 21.8534L11.4828 21.8563L11.4806 21.8592L11.4785 21.862L11.4763 21.8649L11.4742 21.8677L11.4721 21.8705L11.47 21.8733L11.4679 21.8761L11.4658 21.8789L11.4637 21.8817L11.4617 21.8844L11.4596 21.8872L11.4576 21.8899L11.4569 21.8909C11.2386 21.7147 10.9396 21.4668 10.5854 21.1569C9.82987 20.4958 8.8219 19.5509 7.81315 18.4161C5.80744 16.1597 3.75 13.0988 3.75 10C3.75 7.81196 4.61919 5.71354 6.16637 4.16637C7.71354 2.61919 9.81196 1.75 12 1.75C14.188 1.75 16.2865 2.61919 17.8336 4.16637C19.3808 5.71354 20.25 7.81196 20.25 10C20.25 13.0988 18.1926 16.1597 16.1869 18.4161C15.1781 19.5509 14.1701 20.4958 13.4146 21.1569C13.0605 21.4668 12.7614 21.7146 12.5432 21.8908C12.5014 21.8352 12.4539 21.7719 12.4 21.7C12.3357 21.6143 12.2465 21.5526 12.1465 21.5219C12.2069 21.5035 12.2645 21.4733 12.3159 21.4314C12.5196 21.2653 12.7818 21.0463 13.0854 20.7806C13.8299 20.1292 14.8219 19.1991 15.8131 18.0839C17.8073 15.8405 19.75 12.9013 19.75 10C19.75 7.94457 18.9335 5.97333 17.4801 4.51992C16.0267 3.06652 14.0554 2.25 12 2.25C9.94457 2.25 7.97333 3.06652 6.51992 4.51992C5.06652 5.97333 4.25 7.94457 4.25 10C4.25 12.9013 6.19269 15.8405 8.18685 18.0839C9.17811 19.1991 10.1701 20.1292 10.9146 20.7806C11.2182 21.0463 11.4804 21.2653 11.6841 21.4314ZM12 7.25C10.4812 7.25 9.25 8.48122 9.25 10C9.25 11.5188 10.4812 12.75 12 12.75C13.5188 12.75 14.75 11.5188 14.75 10C14.75 8.48122 13.5188 7.25 12 7.25ZM8.75 10C8.75 8.20507 10.2051 6.75 12 6.75C13.7949 6.75 15.25 8.20507 15.25 10C15.25 11.7949 13.7949 13.25 12 13.25C10.2051 13.25 8.75 11.7949 8.75 10Z\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Menu.tsx",
    "content": "import * as React from \"react\"\n\nexport const Menu: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M2.25 5.095h19.5v1.5H2.25v-1.5Zm0 6h19.5v1.5H2.25v-1.5Zm0 6h19.5v1.5H2.25v-1.5Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Minus.tsx",
    "content": "import * as React from \"react\"\n\nexport const Minus: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M19 12.75H5v-1.5h14v1.5Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Package.tsx",
    "content": "import * as React from \"react\"\n\nexport const Package: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      d=\"m3.346 7.75 1.88-3.762.001-.001A2.25 2.25 0 0 1 7.24 2.75h3.535c-.016.08-.025.164-.025.25v.25H7.24s0 0 0 0h-.001v.5m-3.893 4 .059 1m-.06-1h.56m-.56 0h.56m3.334-4a1.25 1.25 0 0 0-1.119.687h0L4.464 7.75h-.56m3.335-4h3.511-3.511Zm-3.334 4-.5 1m0 0H3a.25.25 0 0 0-.22.13l-.003.008a.25.25 0 1 0 .447.224l.18-.362Zm-.655 1.475c.167.034.337.033.5 0V19A1.75 1.75 0 0 0 5 20.75h14A1.75 1.75 0 0 0 20.75 19v-8.775c.163.033.333.034.5 0V19A2.25 2.25 0 0 1 19 21.25H5A2.25 2.25 0 0 1 2.75 19v-8.775ZM19.48 8.75l.248.5h-6.503c.016-.08.025-.164.025-.25v-.25h6.23Zm-8.705.5H4.273l.25-.5h6.227V9c0 .086.009.17.025.25ZM11.75 9v-.25h.5V9a.25.25 0 1 1-.5 0Zm.5-1.25h-.5v-3.5h.5v3.5Zm8.526 1.361-.18-.361H21a.25.25 0 0 1 .25.25v.01a.25.25 0 0 1-.474.101Zm-.119-1.361h-.559l-1.76-3.535-.001-.003v-.001h-.001a1.75 1.75 0 0 0-1.171-.917A.498.498 0 0 0 17.26 3v-.197a2.25 2.25 0 0 1 1.523 1.185l.447-.225-.447.225h.001l.001.003 1.872 3.759Zm-8.407-4.5h-.5V3a.25.25 0 1 1 .5 0v.25Zm1 0V3c0-.086-.009-.17-.025-.25h3.033l.002.25v.003c0 .09.025.174.067.247H13.25Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Plus.tsx",
    "content": "import * as React from \"react\"\n\nexport const Plus: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M11.25 18.813v-14h1.5v14h-1.5Z\"\n      clipRule=\"evenodd\"\n    />\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M19 12.563H5v-1.5h14v1.5Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Receipt.tsx",
    "content": "import * as React from \"react\"\n\nexport const Receipt: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n  >\n    <path\n      d=\"M5.77639 2.60869C5.91716 2.67907 6.08284 2.67907 6.22361 2.60869L7.8882 1.77639C7.95858 1.7412 8.04142 1.7412 8.1118 1.77639L9.77639 2.60869C9.91716 2.67907 10.0828 2.67907 10.2236 2.60869L11.8882 1.77639C11.9586 1.7412 12.0414 1.7412 12.1118 1.77639L13.7764 2.60869C13.9172 2.67907 14.0828 2.67907 14.2236 2.60869L15.8882 1.77639C15.9586 1.7412 16.0414 1.7412 16.1118 1.77639L17.7764 2.60869C17.9172 2.67907 18.0828 2.67907 18.2236 2.60869L19.8882 1.77639L19.6662 1.33249L19.8882 1.77639C19.9657 1.73765 20.0577 1.74179 20.1314 1.78734C20.2051 1.83289 20.25 1.91336 20.25 2V22C20.25 22.0866 20.2051 22.1671 20.1314 22.2127C20.0577 22.2582 19.9657 22.2624 19.8882 22.2236L18.2236 21.3913C18.0828 21.3209 17.9172 21.3209 17.7764 21.3913L16.1118 22.2236C16.0414 22.2588 15.9586 22.2588 15.8882 22.2236L14.2236 21.3913C14.0828 21.3209 13.9172 21.3209 13.7764 21.3913L12.1118 22.2236C12.0414 22.2588 11.9586 22.2588 11.8882 22.2236L10.2236 21.3913C10.0828 21.3209 9.91716 21.3209 9.77639 21.3913L8.1118 22.2236L8.33541 22.6708L8.1118 22.2236C8.04142 22.2588 7.95858 22.2588 7.8882 22.2236L7.66459 22.6708L7.8882 22.2236L6.22361 21.3913C6.08284 21.3209 5.91716 21.3209 5.77639 21.3913L4.1118 22.2236C4.03431 22.2624 3.94227 22.2582 3.86857 22.2127C3.79486 22.1671 3.75 22.0866 3.75 22V2C3.75 1.91336 3.79486 1.83289 3.86857 1.78734C3.94227 1.74179 4.03431 1.73765 4.1118 1.77639L5.77639 2.60869ZM4.97361 2.76631C4.81861 2.68882 4.63454 2.6971 4.48713 2.7882C4.33973 2.8793 4.25 3.04024 4.25 3.21353V20.7865C4.25 20.9598 4.33973 21.1207 4.48713 21.2118C4.63454 21.3029 4.81861 21.3112 4.97361 21.2337L5.8882 20.7764L5.66459 20.3292L5.8882 20.7764C5.95858 20.7412 6.04142 20.7412 6.1118 20.7764L6.33541 20.3292L6.1118 20.7764L7.77639 21.6087C7.91716 21.6791 8.08284 21.6791 8.22361 21.6087L9.8882 20.7764L9.66459 20.3292L9.8882 20.7764C9.95858 20.7412 10.0414 20.7412 10.1118 20.7764L10.3354 20.3292L10.1118 20.7764L11.7764 21.6087C11.9172 21.6791 12.0828 21.6791 12.2236 21.6087L13.8882 20.7764C13.9586 20.7412 14.0414 20.7412 14.1118 20.7764L15.7764 21.6087C15.9172 21.6791 16.0828 21.6791 16.2236 21.6087L17.8882 20.7764C17.9586 20.7412 18.0414 20.7412 18.1118 20.7764L19.0264 21.2337C19.1814 21.3112 19.3655 21.3029 19.5129 21.2118C19.6603 21.1207 19.75 20.9598 19.75 20.7865V3.21353C19.75 3.04024 19.6603 2.8793 19.5129 2.7882C19.3655 2.6971 19.1814 2.68882 19.0264 2.76631L18.1118 3.22361C18.0414 3.2588 17.9586 3.2588 17.8882 3.22361L16.2236 2.39131C16.0828 2.32093 15.9172 2.32093 15.7764 2.39131L14.1118 3.22361C14.0414 3.2588 13.9586 3.2588 13.8882 3.22361L12.2236 2.39131C12.0828 2.32093 11.9172 2.32093 11.7764 2.39131L10.1118 3.22361C10.0414 3.2588 9.95858 3.2588 9.8882 3.22361L8.22361 2.39131C8.08284 2.32093 7.91716 2.32093 7.77639 2.39131L6.1118 3.22361C6.04142 3.2588 5.95858 3.2588 5.8882 3.22361L4.97361 2.76631ZM16.25 8.25H13.25V7.75H16.25V8.25ZM12.25 7.75V8.25H11.75V7.75H12.25ZM11.75 9.25H12.25V10.75H11.75V9.25ZM11.75 11.75H12.25V12.25H11.75V11.75ZM11.75 13.25H12.25V14.75H11.75V13.25ZM13.25 15.75H14C14.4641 15.75 14.9092 15.5656 15.2374 15.2374C15.5656 14.9092 15.75 14.4641 15.75 14C15.75 13.5359 15.5656 13.0908 15.2374 12.7626C14.9092 12.4344 14.4641 12.25 14 12.25H13.25V11.75H14C14.5967 11.75 15.169 11.9871 15.591 12.409C16.0129 12.831 16.25 13.4033 16.25 14C16.25 14.5967 16.0129 15.169 15.591 15.591C15.169 16.0129 14.5967 16.25 14 16.25H13.25V15.75ZM11.75 15.75H12.25V16.25H11.75V15.75ZM10.75 15.75V16.25H7.75V15.75H10.75ZM10.75 12.25H10C9.40326 12.25 8.83097 12.0129 8.40901 11.591C7.98705 11.169 7.75 10.5967 7.75 10C7.75 9.40326 7.98705 8.83097 8.40901 8.40901C8.83097 7.98705 9.40326 7.75 10 7.75H10.75V8.25H10C9.53587 8.25 9.09075 8.43437 8.76256 8.76256C8.43438 9.09075 8.25 9.53587 8.25 10C8.25 10.4641 8.43438 10.9092 8.76256 11.2374C9.09075 11.5656 9.53587 11.75 10 11.75H10.75V12.25Z\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"square\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Search.tsx",
    "content": "import * as React from \"react\"\n\nexport const Search: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 22 22\"\n    fill=\"none\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M9.694 2.445a7.25 7.25 0 1 0 0 14.5 7.25 7.25 0 0 0 0-14.5Zm-8.75 7.25a8.75 8.75 0 1 1 17.5 0 8.75 8.75 0 0 1-17.5 0Z\"\n      clipRule=\"evenodd\"\n    />\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"m15.694 14.634 5.361 5.36-1.06 1.061-5.361-5.36 1.06-1.061Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Sliders.tsx",
    "content": "import * as React from \"react\"\n\nexport const Sliders: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 16 16\"\n    fill=\"none\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M6.889 2a.55.55 0 0 1 .555.545v3.273a.55.55 0 0 1-.555.546.55.55 0 0 1-.556-.546V4.182H3.556A.55.55 0 0 1 3 3.636a.55.55 0 0 1 .556-.545h2.777v-.546A.55.55 0 0 1 6.89 2Zm1.667 1.636a.55.55 0 0 1 .555-.545h3.333a.55.55 0 0 1 .556.545.55.55 0 0 1-.556.546H9.111a.55.55 0 0 1-.555-.546Zm1.11 2.728a.55.55 0 0 1 .556.545v.546h2.222A.55.55 0 0 1 13 8a.55.55 0 0 1-.556.545h-2.222v.546a.55.55 0 0 1-.555.545.55.55 0 0 1-.556-.545V6.909a.55.55 0 0 1 .556-.545ZM3 8a.55.55 0 0 1 .556-.545H8A.55.55 0 0 1 8.556 8 .55.55 0 0 1 8 8.545H3.556A.55.55 0 0 1 3 8Zm3.333 1.636a.55.55 0 0 1 .556.546v3.272a.55.55 0 0 1-.556.546.55.55 0 0 1-.555-.546v-.545H3.556A.55.55 0 0 1 3 12.364a.55.55 0 0 1 .556-.546h2.222v-1.636a.55.55 0 0 1 .555-.546Zm1.111 2.728A.55.55 0 0 1 8 11.818h4.444a.55.55 0 0 1 .556.546.55.55 0 0 1-.556.545H8a.55.55 0 0 1-.556-.545Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Trash.tsx",
    "content": "import * as React from \"react\"\n\nexport const Trash: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      fill=\"currentColor\"\n      d=\"M10.75 17.75v-7.5h-1.5v7.5h1.5ZM14.75 17.75h-1.5v-7.5h1.5v7.5Z\"\n    />\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M7.25 5.25V4c0-.744.364-1.425.845-1.905.48-.48 1.161-.845 1.905-.845h4c.744 0 1.425.364 1.905.845.48.48.845 1.161.845 1.905v1.25h5v1.5h-2V20c0 .744-.364 1.425-.845 1.905-.48.48-1.161.845-1.905.845H7c-.744 0-1.425-.364-1.905-.845-.48-.48-.845-1.161-.845-1.905V6.75h-2v-1.5h5Zm1.5 0V4c0-.256.136-.575.405-.845.27-.27.589-.405.845-.405h4c.256 0 .575.136.845.405.27.27.405.589.405.845v1.25h-6.5Zm-3 1.5V20c0 .256.136.575.405.845.27.27.589.405.845.405h10c.256 0 .575-.136.845-.405.27-.27.405-.589.405-.845V6.75H5.75Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Truck.tsx",
    "content": "import * as React from \"react\"\n\nexport const Truck: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      d=\"M14.327 17.707a3.251 3.251 0 0 0 6.414-.457H22a.75.75 0 0 0 .75-.75v-3.34l-8.423 4.547Zm0 0c.034-.01.067-.02.1-.032l-.17-.47-.257.016-.243.015.001.014h-3.017a3.25 3.25 0 0 1-6.482 0H2a.75.75 0 0 1-.75-.75v-12A.75.75 0 0 1 2 3.75h12a.75.75 0 0 1 .75.75l-.423 13.207Zm7.423-4.547c0-.492-.096-.98-.285-1.436l-.424.176.424-.176a3.75 3.75 0 0 0-.811-1.217h0l-1.611-1.61a.5.5 0 0 0-.354-.147H14.75a.5.5 0 0 0-.5.5v6.017a.5.5 0 0 0 .923.267 2.75 2.75 0 0 1 4.867.408.5.5 0 0 0 .46.308h.75a.5.5 0 0 0 .5-.5v-2.59Zm0 0h-.5m.5 0v0h-.5m0 0v2.09l-.95-4.39a3.25 3.25 0 0 1 .95 2.3ZM14 16a.5.5 0 0 0-.433.75H10.74a.5.5 0 0 0-.499.462 2.75 2.75 0 0 1-5.484 0 .5.5 0 0 0-.499-.462H2a.25.25 0 0 1-.25-.25v-12A.25.25 0 0 1 2 4.25h12a.25.25 0 0 1 .25.25v3.25a.5.5 0 0 0 .5.5H19a.25.25 0 0 1 .177.073l1.83 1.83s0 0 0 0a4.253 4.253 0 0 1 1.243 3.007v3.34a.25.25 0 0 1-.25.25h-1.26a.5.5 0 0 0-.498.462 2.75 2.75 0 0 1-5.487-.038.5.5 0 0 0-.043-.174H15a.498.498 0 0 0 .251-.067L15.25 17a2.25 2.25 0 1 0 .148-.803A.5.5 0 0 0 15 16h-1Zm-9.5.25a.5.5 0 0 0 .46-.308 2.751 2.751 0 0 1 5.08 0 .5.5 0 0 0 .46.308h2.75a.5.5 0 0 0 .5-.5V5.25a.5.5 0 0 0-.5-.5H2.75a.5.5 0 0 0-.5.5v10.5a.5.5 0 0 0 .5.5H4.5Zm3-1.5a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/Undo.tsx",
    "content": "import * as React from \"react\"\n\nexport const Undo: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      d=\"M4 5.25A1.75 1.75 0 0 0 2.25 7v1.75H2c-.086 0-.17.009-.25.025V7A2.25 2.25 0 0 1 4 4.75h16A2.25 2.25 0 0 1 22.25 7v1.775A1.257 1.257 0 0 0 22 8.75h-.25V7A1.75 1.75 0 0 0 20 5.25H4Zm-2 4.5h.25v.5H2a.25.25 0 1 1 0-.5Zm1.25.5v-.5h17.5v.5H3.25Zm-1.25 1h.25V17c0 .966.784 1.75 1.75 1.75h16A1.75 1.75 0 0 0 21.75 17v-5.75H22c.086 0 .17-.009.25-.025V17A2.25 2.25 0 0 1 20 19.25H4A2.25 2.25 0 0 1 1.75 17v-5.775c.08.016.164.025.25.025Zm20-1h-.25v-.5H22a.25.25 0 1 1 0 .5Z\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/icons/User.tsx",
    "content": "import * as React from \"react\"\n\nexport const User: React.FC<React.ComponentPropsWithoutRef<\"svg\">> = (\n  props\n) => (\n  <svg\n    {...props}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n  >\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M12 4.125a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5Zm-5.75 4.25a5.75 5.75 0 1 1 11.5 0 5.75 5.75 0 0 1-11.5 0Z\"\n      clipRule=\"evenodd\"\n    />\n    <path\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      d=\"M5.813 15.188a8.75 8.75 0 0 1 14.937 6.187h-1.5a7.25 7.25 0 1 0-14.5 0h-1.5a8.75 8.75 0 0 1 2.563-6.187Z\"\n      clipRule=\"evenodd\"\n    />\n  </svg>\n)\n"
  },
  {
    "path": "storefront/src/components/ui/Checkbox.tsx",
    "content": "\"use client\"\n\nimport * as ReactAria from \"react-aria-components\"\nimport { twMerge } from \"tailwind-merge\"\nimport { Icon, IconNames, IconProps } from \"@/components/Icon\"\n\nexport const UiCheckbox: React.FC<ReactAria.CheckboxProps> = ({\n  className,\n  ...props\n}) => (\n  <ReactAria.Checkbox\n    {...props}\n    className={twMerge(\n      \"flex gap-2 group cursor-pointer items-center\",\n      className as string\n    )}\n  />\n)\n\nexport const UiCheckboxBox: React.FC<React.ComponentPropsWithoutRef<\"div\">> = ({\n  className,\n  ...props\n}) => (\n  <div\n    {...props}\n    className={twMerge(\n      \"border border-grayscale-200 w-4 h-4 flex items-center group-hover:border-grayscale-600 justify-center group-data-[selected=true]:bg-black group-data-[selected=true]:border-black group-hover:group-data-[selected=true]:border-grayscale-600 group-hover:group-data-[selected=true]:bg-grayscale-600 transition-colors\",\n      className\n    )}\n  />\n)\n\nexport const UiCheckboxIcon: React.FC<\n  Omit<IconProps, \"name\"> & { name?: IconNames }\n> = ({ name = \"check\", className, ...props }) => (\n  <Icon\n    {...props}\n    name={name}\n    className={twMerge(\n      \"w-3 h-3 group-data-[selected=false]:opacity-0 group-data-[selected=true]:opacity-1 text-white\",\n      className\n    )}\n  />\n)\n\nexport const UiCheckboxLabel: React.FC<\n  React.ComponentPropsWithoutRef<\"span\">\n> = ({ className, ...props }) => <span {...props} className={className} />\n"
  },
  {
    "path": "storefront/src/components/ui/Modal.tsx",
    "content": "\"use client\"\n\nimport * as ReactAria from \"react-aria-components\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\n\nexport const UiModalOverlay: React.FC<ReactAria.ModalOverlayProps> = ({\n  isDismissable = true,\n  className,\n  ...props\n}) => (\n  <ReactAria.ModalOverlay\n    {...props}\n    isDismissable={isDismissable}\n    className={twMerge(\n      \"fixed inset-0 flex min-h-full items-center justify-center bg-black-10% z-50 data-[entering]:animate-in data-[entering]:fade-in data-[entering]:duration-200 data-[entering]:ease-out data-[exiting]:animate-out data-[exiting]:fade-out data-[exiting]:duration-100 data-[exiting]:ease-in p-4\",\n      className as string\n    )}\n  />\n)\n\nexport type UiModalOwnProps = {\n  animateFrom?: \"center\" | \"right\" | \"bottom\" | \"left\"\n}\n\nexport const getModalClassNames = ({\n  animateFrom = \"center\",\n}: UiModalOwnProps): string => {\n  const animateFromClasses = {\n    center: \"data-[entering]:zoom-in-95 data-[exiting]:zoom-out-95\",\n    right:\n      \"data-[entering]:slide-in-from-right-10 data-[exiting]:slide-out-to-right-10 right-0 left-auto absolute\",\n    bottom:\n      \"data-[entering]:slide-in-from-bottom-10 data-[exiting]:slide-out-to-bottom-10 bottom-0 absolute\",\n    left: \"data-[entering]:slide-in-from-left-10 data-[exiting]:slide-out-to-left-10 left-0 right-auto absolute\",\n  }\n\n  return twJoin(\n    \"bg-white max-sm:px-4 p-6 rounded-xs max-h-full overflow-y-scroll max-w-154 w-full shadow-modal data-[entering]:animate-in data-[entering]:ease-out data-[entering]:duration-200 data-[exiting]:animate-out data-[exiting]:ease-in data-[exiting]:duration-100\",\n    animateFromClasses[animateFrom]\n  )\n}\n\nexport const UiModal: React.FC<\n  UiModalOwnProps & ReactAria.ModalOverlayProps\n> = ({ animateFrom = \"center\", className, ...props }) => (\n  <ReactAria.Modal\n    {...props}\n    className={twMerge(\n      getModalClassNames({ animateFrom }),\n      className as string\n    )}\n  />\n)\n"
  },
  {
    "path": "storefront/src/components/ui/Radio.tsx",
    "content": "\"use client\"\n\nimport * as ReactAria from \"react-aria-components\"\nimport { twMerge } from \"tailwind-merge\"\n\ntype UiRadioOwnProps = {\n  variant?: \"ghost\" | \"outline\"\n}\n\nexport const UiRadioGroup: React.FC<ReactAria.RadioGroupProps> = ({\n  ...props\n}) => <ReactAria.RadioGroup {...props} />\n\nexport const UiRadio: React.FC<UiRadioOwnProps & ReactAria.RadioProps> = ({\n  variant = \"ghost\",\n  className,\n  ...props\n}) => (\n  <ReactAria.Radio\n    {...props}\n    className={twMerge(\n      \"flex gap-2 group cursor-pointer items-center\",\n      variant === \"outline\" &&\n        \"p-4 gap-4 border border-grayscale-200 hover:border-grayscale-500 transition-colors\",\n      className as string\n    )}\n  />\n)\n\nexport const UiRadioBox: React.FC<React.ComponentPropsWithoutRef<\"span\">> = ({\n  className,\n  ...props\n}) => (\n  <span\n    {...props}\n    className={twMerge(\n      \"border border-grayscale-200 w-4 h-4 block transition-colors rounded-full group-hover:border-grayscale-600 group-data-[selected]:border-black group-data-[selected]:border-6 group-hover:group-data-[selected]:border-grayscale-600\",\n      className\n    )}\n  />\n)\n\nexport const UiRadioLabel: React.FC<React.ComponentPropsWithoutRef<\"span\">> = ({\n  className,\n  ...props\n}) => <span {...props} className={className} />\n"
  },
  {
    "path": "storefront/src/components/ui/Select.tsx",
    "content": "\"use client\"\n\nimport * as ReactAria from \"react-aria-components\"\nimport { twMerge } from \"tailwind-merge\"\nimport { Icon, IconNames, IconProps } from \"@/components/Icon\"\n\ntype UiSelectButtonOwnProps = {\n  variant?: \"outline\" | \"ghost\"\n}\n\nexport const UiSelectButton: React.FC<\n  ReactAria.ButtonProps & UiSelectButtonOwnProps\n> = ({ variant = \"outline\", className, ...props }) => (\n  <ReactAria.Button\n    {...props}\n    className={twMerge(\n      \"w-full gap-1 md:gap-2 flex items-center focus:border-grayscale-500 max-md:text-xs justify-between h-8 md:h-10 px-3 md:pl-4 md:pr-3 focus-visible:outline-none transition-colors\",\n      variant === \"outline\" &&\n        \"border border-grayscale-200 rounded-xs hover:border-grayscale-500 hover:text-grayscale-500\",\n      className as string\n    )}\n  />\n)\n\nexport const UiSelectIcon: React.FC<\n  Omit<IconProps, \"name\"> & { name?: IconNames }\n> = ({ name = \"chevron-down\", className, ...props }) => (\n  <Icon\n    {...props}\n    name={name}\n    aria-hidden=\"true\"\n    className={twMerge(\"h-4 w-4 md:w-6 md:h-6\", className)}\n  />\n)\n\nexport const UiSelectValue = <T extends object>({\n  className,\n  ...props\n}: ReactAria.SelectValueProps<T>) => (\n  <ReactAria.SelectValue\n    {...props}\n    className={twMerge(\"truncate\", className as string)}\n  />\n)\n\nexport const UiSelectListBox = <T extends object>({\n  className,\n  ...props\n}: ReactAria.ListBoxProps<T>) => (\n  <ReactAria.ListBox\n    {...props}\n    className={twMerge(\n      \"border border-grayscale-200 bg-white rounded-xs focus-visible:outline-none max-h-50 overflow-scroll\",\n      className as string\n    )}\n  />\n)\n\nexport const UiSelectListBoxItem: React.FC<ReactAria.ListBoxItemProps> = ({\n  className,\n  ...props\n}) => (\n  <ReactAria.ListBoxItem\n    {...props}\n    className={twMerge(\n      \"cursor-pointer p-4 focus-visible:outline-none data-[selected]:font-semibold hover:bg-grayscale-50 transition-colors\",\n      className as string\n    )}\n  />\n)\n\nexport const UiSelectDialog: React.FC<ReactAria.DialogProps> = ({\n  className,\n  ...props\n}) => (\n  <ReactAria.Dialog\n    {...props}\n    className={twMerge(\n      \"border border-grayscale-200 bg-white rounded-xs focus-visible:outline-none\",\n      className\n    )}\n  />\n)\n"
  },
  {
    "path": "storefront/src/components/ui/Skeleton.tsx",
    "content": "import { twMerge } from \"tailwind-merge\"\n\ntype SkeletonProps = {\n  colorScheme?: \"white\" | \"grayscale\"\n} & React.ComponentPropsWithoutRef<\"div\">\n\nexport const Skeleton: React.FC<SkeletonProps> = ({\n  colorScheme = \"grayscale\",\n  className,\n  ...rest\n}) => (\n  <div\n    {...rest}\n    className={twMerge(\n      \"relative overflow-hidden shrink-0 rounded-2xs before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:to-transparent\",\n      colorScheme === \"grayscale\" && \"bg-grayscale-50 before:via-white\",\n      colorScheme === \"white\" && \"bg-white before:via-grayscale-50\",\n      className\n    )}\n  />\n)\n"
  },
  {
    "path": "storefront/src/components/ui/Slider.tsx",
    "content": "import * as ReactAria from \"react-aria-components\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport const UiSliderTrack: React.FC<ReactAria.SliderTrackProps> = ({\n  className,\n  ...props\n}) => (\n  <ReactAria.SliderTrack\n    {...props}\n    className={twMerge(\"h-px bg-black\", className as string)}\n  />\n)\n\nexport const UiSliderThumb: React.FC<ReactAria.SliderThumbProps> = ({\n  className,\n  ...props\n}) => (\n  <ReactAria.SliderThumb\n    {...props}\n    className={twMerge(\n      \"w-4 h-4 border border-black bg-white rounded-full cursor-pointer\",\n      className as string\n    )}\n  />\n)\n\nexport const UiSliderOutput: React.FC<ReactAria.SliderOutputProps> = ({\n  className,\n  ...props\n}) => (\n  <ReactAria.SliderOutput\n    {...props}\n    className={twMerge(\"flex justify-between mt-5\", className as string)}\n  />\n)\n\nexport const UiSliderOutputValue: React.FC<\n  React.ComponentPropsWithoutRef<\"span\">\n> = ({ className, ...props }) => (\n  <span {...props} className={twMerge(\"text-xs\", className as string)} />\n)\n"
  },
  {
    "path": "storefront/src/components/ui/Tag.tsx",
    "content": "import * as React from \"react\"\nimport { twMerge } from \"tailwind-merge\"\nimport { Icon, IconNames } from \"@/components/Icon\"\n\ntype UiTagOwnProps = {\n  isActive?: boolean\n  iconName?: IconNames\n  iconPosition?: \"start\" | \"end\"\n}\n\nexport const UiTag: React.FC<\n  React.ComponentPropsWithRef<\"div\"> & UiTagOwnProps\n> = ({\n  isActive = false,\n  iconName,\n  iconPosition,\n  className,\n  children,\n  ...rest\n}) => (\n  <div\n    {...rest}\n    className={twMerge(\n      \"inline-flex justify-center items-center gap-2 rounded-md px-4 py-1.5 max-h-6 text-xs bg-grayscale-50\",\n      isActive && \"bg-black text-white\",\n      iconPosition === \"end\" && \"flex-row-reverse\",\n      className\n    )}\n  >\n    {iconName && <Icon name={iconName} className=\"w-3 h-3\" />}\n    <span className=\"text-grayscale-200\">{children}</span>\n  </div>\n)\n"
  },
  {
    "path": "storefront/src/components/ui/TagList.tsx",
    "content": "import { twMerge } from \"tailwind-merge\"\n\nexport const UiTagList: React.FC<React.ComponentPropsWithoutRef<\"ul\">> = ({\n  className,\n  ...rest\n}) => (\n  <ul\n    {...rest}\n    className={twMerge(\"inline-flex flex-wrap gap-y-2 items-center\", className)}\n  />\n)\n\nexport const UiTagListDivider: React.FC<\n  React.ComponentPropsWithoutRef<\"span\">\n> = ({ className, ...rest }) => (\n  <span {...rest} className={twMerge(\"w-6 h-px bg-black\", className)} />\n)\n"
  },
  {
    "path": "storefront/src/hooks/cart.ts",
    "content": "import {\n  addToCart,\n  applyPromotions,\n  deleteLineItem,\n  getCartQuantity,\n  getPaymentMethod,\n  initiatePaymentSession,\n  placeOrder,\n  retrieveCart,\n  setAddresses,\n  setEmail,\n  setPaymentMethod,\n  setShippingMethod,\n  updateLineItem,\n  updateRegion,\n} from \"@lib/data/cart\"\nimport { listCartShippingMethods } from \"@lib/data/fulfillment\"\nimport { listCartPaymentMethods } from \"@lib/data/payment\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport {\n  useMutation,\n  UseMutationOptions,\n  useQuery,\n  useQueryClient,\n} from \"@tanstack/react-query\"\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport { z } from \"zod\"\n\nexport const useCart = ({ enabled }: { enabled: boolean }) => {\n  return useQuery({\n    queryKey: [\"cart\"],\n    queryFn: async () => {\n      const res = await retrieveCart()\n      return res\n    },\n    enabled,\n  })\n}\n\nexport const useCartQuantity = () => {\n  return useQuery({\n    queryKey: [\"cart\", \"cart-quantity\"],\n    queryFn: async () => {\n      const res = await getCartQuantity()\n      return res\n    },\n  })\n}\n\nexport const useCartShippingMethods = (cartId: string) => {\n  return useQuery({\n    queryKey: [cartId],\n    queryFn: async () => {\n      const res = await listCartShippingMethods(cartId)\n      return res\n    },\n  })\n}\n\nexport const useCartPaymentMethods = (regionId: string) => {\n  return useQuery({\n    queryKey: [regionId],\n    queryFn: async () => {\n      const res = await listCartPaymentMethods(regionId)\n      return res\n    },\n  })\n}\n\ntype UpdateLineItemContext = {\n  previousCart: HttpTypes.StoreCart | null | undefined\n}\n\nconst coerceMutationContext = (\n  context: UpdateLineItemContext | void | unknown\n): UpdateLineItemContext => {\n  if (context && typeof context === \"object\") {\n    return context as UpdateLineItemContext\n  }\n  return { previousCart: undefined }\n}\n\nexport const useUpdateLineItem = (\n  options?: UseMutationOptions<\n    void,\n    Error,\n    { lineId: string; quantity: number },\n    UpdateLineItemContext\n  >\n) => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationKey: [\"cart-update-line-item\"],\n    mutationFn: async (payload: { lineId: string; quantity: number }) => {\n      const response = await updateLineItem({\n        lineId: payload.lineId,\n        quantity: payload.quantity,\n      })\n      return response\n    },\n    ...options,\n    onMutate: async ({ lineId, quantity, ...rest }, ...restArgs) => {\n      await queryClient.cancelQueries({ queryKey: [\"cart\"] })\n\n      const userContext = await options?.onMutate?.(\n        { lineId, quantity, ...rest },\n        ...restArgs\n      )\n\n      const previousCart = queryClient.getQueryData<HttpTypes.StoreCart | null>(\n        [\"cart\"]\n      )\n\n      queryClient.setQueryData(\n        [\"cart\"],\n        (old: HttpTypes.StoreCart | null | undefined) => {\n          if (!old) return old\n          return {\n            ...old,\n            items: (old.items ?? []).map((cartItem) =>\n              cartItem.id === lineId ? { ...cartItem, quantity } : cartItem\n            ),\n          }\n        }\n      )\n\n      const previousItem = previousCart?.items?.find((i) => i.id === lineId)\n      if (previousItem) {\n        const delta = quantity - previousItem.quantity\n        queryClient.setQueryData(\n          [\"cart\", \"cart-quantity\"],\n          (old: number | undefined) => Math.max(0, (old ?? 0) + delta)\n        )\n      }\n\n      return { ...coerceMutationContext(userContext), previousCart }\n    },\n    onError: (error, variables, onMutateResult, context) => {\n      if (onMutateResult?.previousCart) {\n        queryClient.setQueryData([\"cart\"], onMutateResult.previousCart)\n        const total = (onMutateResult.previousCart.items ?? []).reduce(\n          (acc, i) => acc + i.quantity,\n          0\n        )\n        queryClient.setQueryData([\"cart\", \"cart-quantity\"], total)\n      }\n\n      options?.onError?.(error, variables, onMutateResult, context)\n    },\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n\ntype LineItemQuantityUpdater = {\n  quantity: number\n  error: Error | null\n  onQuantityChange: (value: number) => void\n  onQuantityCommit: (value: number) => void\n  onQuantityFocus: () => void\n  onQuantityBlur: () => void\n}\n\nexport const useLineItemQuantityUpdater = ({\n  lineId,\n  initialQuantity,\n}: {\n  lineId: string\n  initialQuantity: number\n}): LineItemQuantityUpdater => {\n  const { mutateAsync, error, reset } = useUpdateLineItem({\n    onSuccess: () => {\n      reset()\n    },\n  })\n  const [quantity, setQuantity] = useState(initialQuantity)\n  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const inFlightRef = useRef(false)\n  const queuedQuantityRef = useRef<number | null>(null)\n  const lastCommittedRef = useRef(initialQuantity)\n  const isEditingRef = useRef(false)\n\n  useEffect(() => {\n    lastCommittedRef.current = initialQuantity\n\n    if (isEditingRef.current) return\n    setQuantity(initialQuantity)\n  }, [initialQuantity])\n\n  useEffect(() => {\n    return () => {\n      if (timerRef.current) {\n        clearTimeout(timerRef.current)\n        timerRef.current = null\n      }\n    }\n  }, [])\n\n  const flushQuantityUpdate = useCallback(async () => {\n    if (inFlightRef.current) return\n    const quantityToCommit = queuedQuantityRef.current\n    if (\n      quantityToCommit === null ||\n      quantityToCommit === lastCommittedRef.current\n    ) {\n      return\n    }\n\n    inFlightRef.current = true\n    queuedQuantityRef.current = null\n    lastCommittedRef.current = quantityToCommit\n\n    try {\n      await mutateAsync({ lineId, quantity: quantityToCommit })\n    } finally {\n      inFlightRef.current = false\n      if (queuedQuantityRef.current !== null) {\n        void flushQuantityUpdate()\n      }\n    }\n  }, [lineId, mutateAsync])\n\n  const scheduleQuantityUpdate = useCallback(\n    (nextQuantity: number) => {\n      queuedQuantityRef.current = nextQuantity\n      if (timerRef.current) clearTimeout(timerRef.current)\n      timerRef.current = setTimeout(() => {\n        void flushQuantityUpdate()\n      }, 350)\n    },\n    [flushQuantityUpdate]\n  )\n\n  const onQuantityChange = useCallback(\n    (newQuantity: number) => {\n      setQuantity(newQuantity)\n      scheduleQuantityUpdate(newQuantity)\n    },\n    [scheduleQuantityUpdate]\n  )\n\n  const onQuantityCommit = useCallback(\n    (newQuantity: number) => {\n      queuedQuantityRef.current = newQuantity\n      if (timerRef.current) {\n        clearTimeout(timerRef.current)\n        timerRef.current = null\n      }\n      void flushQuantityUpdate()\n    },\n    [flushQuantityUpdate]\n  )\n\n  const onQuantityFocus = useCallback(() => {\n    isEditingRef.current = true\n  }, [])\n\n  const onQuantityBlur = useCallback(() => {\n    isEditingRef.current = false\n  }, [])\n\n  return {\n    quantity,\n    error: error ?? null,\n    onQuantityChange,\n    onQuantityCommit,\n    onQuantityFocus,\n    onQuantityBlur,\n  }\n}\n\ntype DeleteLineItemContext = {\n  previousCart: HttpTypes.StoreCart | null | undefined\n}\n\nexport const useDeleteLineItem = (\n  options?: UseMutationOptions<\n    void,\n    Error,\n    { lineId: string },\n    DeleteLineItemContext\n  >\n) => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationKey: [\"cart-delete-line-item\"],\n    mutationFn: async (payload: { lineId: string }) => {\n      const response = await deleteLineItem(payload.lineId)\n\n      return response\n    },\n    ...options,\n    onMutate: async ({ lineId }) => {\n      await queryClient.cancelQueries({ queryKey: [\"cart\"] })\n\n      const previousCart = queryClient.getQueryData<HttpTypes.StoreCart | null>(\n        [\"cart\"]\n      )\n\n      queryClient.setQueryData(\n        [\"cart\"],\n        (old: HttpTypes.StoreCart | null | undefined) => {\n          if (!old) return old\n          return {\n            ...old,\n            items: (old.items ?? []).filter((item) => item.id !== lineId),\n          }\n        }\n      )\n\n      const removedItem = previousCart?.items?.find(\n        (item) => item.id === lineId\n      )\n      if (removedItem) {\n        queryClient.setQueryData(\n          [\"cart\", \"cart-quantity\"],\n          (old: number | undefined) =>\n            Math.max(0, (old ?? 0) - removedItem.quantity)\n        )\n      }\n\n      return { previousCart }\n    },\n    onError: (error, variables, onMutateResult, context) => {\n      if (onMutateResult?.previousCart) {\n        queryClient.setQueryData([\"cart\"], onMutateResult.previousCart)\n        const total = (onMutateResult.previousCart.items ?? []).reduce(\n          (acc, item) => acc + item.quantity,\n          0\n        )\n        queryClient.setQueryData([\"cart\", \"cart-quantity\"], total)\n      }\n\n      options?.onError?.(error, variables, onMutateResult, context)\n    },\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n\nexport const useAddLineItem = (\n  options?: UseMutationOptions<\n    void,\n    Error,\n    { variantId: string; quantity: number; countryCode: string | undefined },\n    unknown\n  >\n) => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationKey: [\"cart-add-line-item\"],\n    mutationFn: async (payload: {\n      variantId: string\n      quantity: number\n      countryCode: string | undefined\n    }) => {\n      const response = await addToCart({ ...payload })\n\n      return response\n    },\n    ...options,\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n\nexport const useSetShippingMethod = (\n  { cartId }: { cartId: string },\n  options?: UseMutationOptions<\n    void,\n    Error,\n    { shippingMethodId: string },\n    unknown\n  >\n) => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationKey: [\"shipping-update\", cartId],\n    mutationFn: async ({ shippingMethodId }) => {\n      const response = await setShippingMethod({\n        cartId,\n        shippingMethodId,\n      })\n\n      return response\n    },\n    ...options,\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n\nexport const addressesFormSchema = z\n  .object({\n    shipping_address: z.object({\n      first_name: z.string().min(1),\n      last_name: z.string().min(1),\n      company: z.string().optional(),\n      address_1: z.string().min(1),\n      address_2: z.string().optional(),\n      city: z.string().min(1),\n      postal_code: z.string().min(1),\n      province: z.string().optional(),\n      country_code: z.string().min(2),\n      phone: z.string().optional(),\n    }),\n  })\n  .and(\n    z.discriminatedUnion(\"same_as_billing\", [\n      z.object({\n        same_as_billing: z.literal(\"on\"),\n      }),\n      z.object({\n        same_as_billing: z.literal(\"off\").optional(),\n        billing_address: z.object({\n          first_name: z.string().min(1),\n          last_name: z.string().min(1),\n          company: z.string().optional(),\n          address_1: z.string().min(1),\n          address_2: z.string().optional(),\n          city: z.string().min(1),\n          postal_code: z.string().min(1),\n          province: z.string().optional(),\n          country_code: z.string().min(2),\n          phone: z.string().optional(),\n        }),\n      }),\n    ])\n  )\n\nexport const useSetShippingAddress = (\n  options?: UseMutationOptions<\n    { success: boolean; error: string | null },\n    Error,\n    z.infer<typeof addressesFormSchema>,\n    unknown\n  >\n) => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationKey: [\"shipping-address-update\"],\n    mutationFn: async (payload) => {\n      const response = await setAddresses(payload)\n      return response\n    },\n    ...options,\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n\nexport const useSetEmail = (\n  options?: UseMutationOptions<\n    { success: boolean; error: string | null },\n    Error,\n    { email: string; country_code: string },\n    unknown\n  >\n) => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationKey: [\"set-email\"],\n    mutationFn: async (payload) => {\n      const response = await setEmail(payload)\n      return response\n    },\n    ...options,\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n\nexport const useInitiatePaymentSession = (\n  options?: UseMutationOptions<\n    HttpTypes.StorePaymentCollectionResponse,\n    Error,\n    {\n      providerId: string\n    },\n    unknown\n  >\n) => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationKey: [\"initiate-payment\"],\n    mutationFn: async (payload: { providerId: string }) => {\n      const response = await initiatePaymentSession(payload.providerId)\n\n      return response\n    },\n    ...options,\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n\nexport const useSetPaymentMethod = (\n  options?: UseMutationOptions<\n    void,\n    Error,\n    { sessionId: string; token: string | null | undefined },\n    unknown\n  >\n) => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationKey: [\"set-payment\"],\n    mutationFn: async (payload) => {\n      const response = await setPaymentMethod(payload.sessionId, payload.token)\n\n      return response\n    },\n    ...options,\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n\nexport const useGetPaymentMethod = (id: string | undefined) => {\n  return useQuery({\n    queryKey: [\"payment\", id],\n    queryFn: async () => {\n      if (!id) {\n        return null\n      }\n      const res = await getPaymentMethod(id)\n      return res\n    },\n  })\n}\n\nexport const usePlaceOrder = (\n  options?: UseMutationOptions<\n    | {\n        type: \"cart\"\n        cart: HttpTypes.StoreCart\n        error: {\n          message: string\n          name: string\n          type: string\n        }\n      }\n    | {\n        type: \"order\"\n        order: HttpTypes.StoreOrder\n      }\n    | null,\n    Error,\n    null,\n    unknown\n  >\n) => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationKey: [\"place-order\"],\n    mutationFn: async () => {\n      const response = await placeOrder()\n      return response\n    },\n    ...options,\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n\nexport const useApplyPromotions = (\n  options?: UseMutationOptions<void, Error, string[], unknown>\n) => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationKey: [\"apply-promotion\"],\n    mutationFn: async (payload) => {\n      const response = await applyPromotions(payload)\n\n      return response\n    },\n    ...options,\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n\nexport const useUpdateRegion = (\n  options?: UseMutationOptions<\n    void,\n    Error,\n    { countryCode: string; currentPath: string },\n    unknown\n  >\n) => {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationKey: [\"update-region\"],\n    mutationFn: async ({ countryCode, currentPath }) => {\n      await updateRegion(countryCode, currentPath)\n    },\n    ...options,\n    async onSuccess(...args) {\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"cart\"],\n      })\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"regions\"],\n      })\n      await queryClient.invalidateQueries({\n        exact: false,\n        queryKey: [\"products\"],\n      })\n\n      await options?.onSuccess?.(...args)\n    },\n  })\n}\n"
  },
  {
    "path": "storefront/src/hooks/country-code.tsx",
    "content": "import { useParams, usePathname } from \"next/navigation\"\n\nexport const useCountryCode = (\n  countryOptions?: {\n    country: string | undefined\n    region: string\n    label: string | undefined\n  }[]\n) => {\n  const pathName = usePathname()\n  const params = useParams()\n\n  if (typeof params.countryCode === \"string\") {\n    return params.countryCode\n  }\n\n  if (countryOptions) {\n    // Check if the path contains a country code and update the current path\n    const pathParts = pathName.replace(/^\\//, \"\").split(\"/\")\n\n    if (pathParts.length > 1) {\n      const firstPathPart = pathParts[0]\n      const country = countryOptions.find(\n        (country) => country.country === firstPathPart\n      )\n\n      if (country) {\n        return country.country\n      }\n    }\n  } else {\n    const pathParts = pathName.replace(/^\\//, \"\").split(\"/\")\n\n    if (pathParts.length > 1 && pathParts[0].length === 2) {\n      return pathParts[0]\n    }\n  }\n}\n"
  },
  {
    "path": "storefront/src/hooks/customer.ts",
    "content": "import {\n  useMutation,\n  UseMutationOptions,\n  useQuery,\n  useQueryClient,\n} from \"@tanstack/react-query\"\nimport {\n  addCustomerAddress,\n  deleteCustomerAddress,\n  getCustomer,\n  login,\n  signout,\n  signup,\n  updateCustomer,\n  updateCustomerAddress,\n} from \"@lib/data/customer\"\nimport { z } from \"zod\"\nimport { StoreCustomer } from \"@medusajs/types\"\n\nexport const useCustomer = () => {\n  return useQuery({\n    queryKey: [\"customer\"],\n    queryFn: async () => {\n      const customer = await getCustomer()\n      return customer\n    },\n    staleTime: 5 * 60 * 1000,\n  })\n}\n\nexport const loginFormSchema = z.object({\n  email: z.string().email(),\n  password: z.string().min(6),\n  redirect_url: z.string().optional().nullable(),\n})\n\nexport const useLogin = (\n  options?: UseMutationOptions<\n    { success: boolean; redirectUrl?: string; message?: string },\n    Error,\n    z.infer<typeof loginFormSchema>\n  >\n) => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationKey: [\"login\"],\n    mutationFn: async (values: z.infer<typeof loginFormSchema>) => {\n      return login({ ...values })\n    },\n    onSuccess: async (...args) => {\n      await queryClient.invalidateQueries({ queryKey: [\"customer\"] })\n      await options?.onSuccess?.(...args)\n    },\n    ...options,\n  })\n}\n\nexport const useSignout = (\n  options?: UseMutationOptions<string, Error, string>\n) => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationKey: [\"signout\"],\n    mutationFn: async (countryCode: string) => {\n      return signout(countryCode)\n    },\n    onSuccess: async (...args) => {\n      await queryClient.invalidateQueries({ queryKey: [\"customer\"] })\n      await options?.onSuccess?.(...args)\n    },\n    ...options,\n  })\n}\n\nexport const updateCustomerFormSchema = z.object({\n  first_name: z.string().min(1),\n  last_name: z.string().min(1),\n  phone: z.string().optional().nullable(),\n})\n\nexport const useUpdateCustomer = (\n  options?: UseMutationOptions<\n    { state: \"error\" | \"success\" | \"initial\"; error?: string },\n    Error,\n    z.infer<typeof updateCustomerFormSchema>\n  >\n) => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationKey: [\"update-customer\"],\n    mutationFn: async (values: z.infer<typeof updateCustomerFormSchema>) => {\n      return updateCustomer(values)\n    },\n    onSuccess: async (...args) => {\n      await queryClient.invalidateQueries({ queryKey: [\"customer\"] })\n      await options?.onSuccess?.(...args)\n    },\n    ...options,\n  })\n}\n\nexport const customerAddressSchema = z.object({\n  first_name: z.string().min(1),\n  last_name: z.string().min(1),\n  company: z.string().optional().nullable(),\n  address_1: z.string().min(1),\n  address_2: z.string().optional().nullable(),\n  city: z.string().min(1),\n  postal_code: z.string().min(1),\n  province: z.string().optional().nullable(),\n  country_code: z.string().min(2),\n  phone: z.string().optional().nullable(),\n})\n\nexport const useAddressMutation = (\n  addressId?: string,\n  options?: UseMutationOptions<\n    { addressId: string; success: boolean; error: string | null },\n    Error,\n    z.infer<typeof customerAddressSchema>\n  >\n) => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationKey: [\"add-address\", \"update-address\"],\n    mutationFn: async (values: z.infer<typeof customerAddressSchema>) => {\n      return addressId\n        ? updateCustomerAddress(addressId, values)\n        : addCustomerAddress(values)\n    },\n    onSuccess: async (...args) => {\n      await queryClient.invalidateQueries({ queryKey: [\"customer\"] })\n      await options?.onSuccess?.(...args)\n    },\n    ...options,\n  })\n}\n\nexport const useDeleteCustomerAddress = (\n  options?: UseMutationOptions<void, Error, string>\n) => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationKey: [\"delete-address\"],\n    mutationFn: async (addressId: string) => {\n      return deleteCustomerAddress(addressId)\n    },\n    onSuccess: async (...args) => {\n      await queryClient.invalidateQueries({ queryKey: [\"customer\"] })\n      await options?.onSuccess?.(...args)\n    },\n    ...options,\n  })\n}\n\nexport const signupFormSchema = z.object({\n  email: z.string().email(),\n  first_name: z.string().min(1),\n  last_name: z.string().min(1),\n  phone: z.string().optional().nullable(),\n  password: z.string().min(6),\n})\n\nexport const useSignup = (\n  options?: UseMutationOptions<\n    { success: boolean; error?: string | null; customer?: StoreCustomer },\n    Error,\n    z.infer<typeof signupFormSchema>\n  >\n) => {\n  const queryClient = useQueryClient()\n\n  return useMutation({\n    mutationKey: [\"signup\"],\n    mutationFn: async (values: z.infer<typeof signupFormSchema>) => {\n      return signup(values)\n    },\n    onSuccess: async (...args) => {\n      await queryClient.invalidateQueries({ queryKey: [\"customer\"] })\n      await options?.onSuccess?.(...args)\n    },\n    ...options,\n  })\n}\n"
  },
  {
    "path": "storefront/src/hooks/store.tsx",
    "content": "import { getProductsListWithSort } from \"@lib/data/products\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport { SortOptions } from \"@modules/store/components/refinement-list/sort-products\"\nimport { useInfiniteQuery } from \"@tanstack/react-query\"\n\nexport const useStoreProducts = ({\n  page,\n  queryParams,\n  sortBy,\n  countryCode,\n}: {\n  page: number\n  queryParams: HttpTypes.StoreProductListParams\n  sortBy: SortOptions | undefined\n  countryCode: string\n}) => {\n  return useInfiniteQuery({\n    initialPageParam: page,\n    queryKey: [\"products\", queryParams, sortBy, countryCode],\n    queryFn: async ({ pageParam }) => {\n      return getProductsListWithSort({\n        page: pageParam,\n        queryParams,\n        sortBy,\n        countryCode,\n      })\n    },\n    getNextPageParam: (lastPage: {\n      response: { products: HttpTypes.StoreProduct[]; count: number }\n      nextPage: number | null\n      queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams\n    }) => {\n      if (!lastPage.nextPage) {\n        return undefined\n      }\n      return (\n        Math.ceil(lastPage.nextPage / (lastPage.queryParams?.limit || 12)) + 1\n      )\n    },\n  })\n}\n"
  },
  {
    "path": "storefront/src/lib/config.ts",
    "content": "import Medusa from \"@medusajs/js-sdk\"\n\n// Defaults to standard port for Medusa server\nlet MEDUSA_BACKEND_URL = \"http://localhost:9000\"\n\nif (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) {\n  MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL\n}\n\nexport const sdk = new Medusa({\n  baseUrl: MEDUSA_BACKEND_URL,\n  debug: process.env.NODE_ENV === \"development\",\n  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,\n})\n"
  },
  {
    "path": "storefront/src/lib/constants.tsx",
    "content": "import React from \"react\"\nimport { CreditCard } from \"@medusajs/icons\"\n\nimport Ideal from \"@modules/common/icons/ideal\"\nimport Bancontact from \"@modules/common/icons/bancontact\"\nimport PayPal from \"@modules/common/icons/paypal\"\n\n/* Map of payment provider_id to their title and icon. Add in any payment providers you want to use. */\nexport const paymentInfoMap: Record<\n  string,\n  { title: string; icon: React.JSX.Element }\n> = {\n  pp_stripe_stripe: {\n    title: \"Credit card\",\n    icon: <CreditCard />,\n  },\n  \"pp_stripe-ideal_stripe\": {\n    title: \"iDeal\",\n    icon: <Ideal />,\n  },\n  \"pp_stripe-bancontact_stripe\": {\n    title: \"Bancontact\",\n    icon: <Bancontact />,\n  },\n  pp_paypal_paypal: {\n    title: \"PayPal\",\n    icon: <PayPal />,\n  },\n  pp_system_default: {\n    title: \"Manual Payment\",\n    icon: <CreditCard />,\n  },\n  // Add more payment providers here\n}\n\n// This only checks if it is native stripe for card payments, it ignores the other stripe-based providers\nexport const isStripe = (providerId?: string) => {\n  return providerId?.startsWith(\"pp_stripe_\")\n}\nexport const isPaypal = (providerId?: string) => {\n  return providerId?.startsWith(\"pp_paypal\")\n}\nexport const isManual = (providerId?: string) => {\n  return providerId?.startsWith(\"pp_system_default\")\n}\n"
  },
  {
    "path": "storefront/src/lib/data/cart.ts",
    "content": "\"use server\"\n\nimport { HttpTypes } from \"@medusajs/types\"\nimport { revalidateTag } from \"next/cache\"\nimport { redirect } from \"next/navigation\"\nimport { z } from \"zod\"\nimport { PaymentMethod } from \"@stripe/stripe-js\"\n\nimport { sdk } from \"@lib/config\"\nimport medusaError from \"@lib/util/medusa-error\"\nimport { enrichLineItems } from \"@lib/util/enrich-line-items\"\nimport {\n  getCartId,\n  getAuthHeaders,\n  setCartId,\n  removeCartId,\n} from \"@lib/data/cookies\"\nimport { getRegion } from \"@lib/data/regions\"\nimport { addressesFormSchema } from \"hooks/cart\"\n\nexport async function retrieveCart() {\n  const cartId = await getCartId()\n\n  if (!cartId) {\n    return null\n  }\n  const cart = await sdk.client\n    .fetch<HttpTypes.StoreCartResponse>(`/store/carts/${cartId}`, {\n      next: { tags: [\"cart\"] },\n      headers: { ...(await getAuthHeaders()) },\n      cache: \"no-store\",\n    })\n    .then(({ cart }) => cart)\n    .catch(() => {\n      return null\n    })\n\n  if (cart?.items && cart.items.length && cart.region_id) {\n    cart.items = await enrichLineItems(cart.items, cart.region_id)\n  }\n\n  return cart\n}\n\nexport async function getCartQuantity() {\n  const cart = await retrieveCart()\n\n  if (!cart || !cart.items || !cart.items.length) {\n    return 0\n  }\n\n  return cart.items.reduce((acc, item) => acc + item.quantity, 0)\n}\n\nexport async function getOrSetCart(input: unknown) {\n  if (typeof input !== \"string\") {\n    throw new Error(\"Invalid input when retrieving cart\")\n  }\n\n  const countryCode = input\n\n  let cart = await retrieveCart()\n  const region = await getRegion(countryCode)\n\n  if (!region) {\n    throw new Error(`Region not found for country code: ${countryCode}`)\n  }\n\n  if (!cart) {\n    const cartResp = await sdk.store.cart.create(\n      { region_id: region.id },\n      {},\n      await getAuthHeaders()\n    )\n    cart = cartResp.cart\n\n    await setCartId(cart.id)\n    revalidateTag(\"cart\")\n  }\n\n  if (cart && cart?.region_id !== region.id) {\n    await sdk.store.cart.update(\n      cart.id,\n      { region_id: region.id },\n      {},\n      await getAuthHeaders()\n    )\n    revalidateTag(\"cart\")\n  }\n\n  return cart\n}\n\nasync function updateCart(data: HttpTypes.StoreUpdateCart) {\n  const cartId = await getCartId()\n  if (!cartId) {\n    throw new Error(\"No existing cart found, please create one before updating\")\n  }\n\n  return sdk.store.cart\n    .update(cartId, data, {}, await getAuthHeaders())\n    .then(({ cart }) => {\n      revalidateTag(\"cart\")\n      return cart\n    })\n    .catch(medusaError)\n}\n\nexport async function addToCart({\n  variantId,\n  quantity,\n  countryCode,\n}: {\n  variantId: unknown\n  quantity: unknown\n  countryCode: unknown\n}) {\n  if (typeof variantId !== \"string\") {\n    throw new Error(\"Missing variant ID when adding to cart\")\n  }\n\n  if (\n    typeof quantity !== \"number\" ||\n    quantity < 1 ||\n    !Number.isSafeInteger(quantity)\n  ) {\n    throw new Error(\"Missing quantity when adding to cart\")\n  }\n\n  if (typeof countryCode !== \"string\") {\n    throw new Error(\"Missing country code when adding to cart\")\n  }\n\n  const cart = await getOrSetCart(countryCode)\n  if (!cart) {\n    throw new Error(\"Error retrieving or creating cart\")\n  }\n\n  await sdk.store.cart\n    .createLineItem(\n      cart.id,\n      {\n        variant_id: variantId,\n        quantity,\n      },\n      {},\n      await getAuthHeaders()\n    )\n    .then(() => {\n      revalidateTag(\"cart\")\n    })\n    .catch(medusaError)\n}\n\nexport async function updateLineItem({\n  lineId,\n  quantity,\n}: {\n  lineId: unknown\n  quantity: unknown\n}) {\n  if (typeof lineId !== \"string\") {\n    throw new Error(\"Missing lineItem ID when updating line item\")\n  }\n\n  if (\n    typeof quantity !== \"number\" ||\n    quantity < 1 ||\n    !Number.isSafeInteger(quantity)\n  ) {\n    throw new Error(\"Missing quantity when updating line item\")\n  }\n\n  const cartId = await getCartId()\n  if (!cartId) {\n    throw new Error(\"Missing cart ID when updating line item\")\n  }\n\n  await sdk.store.cart\n    .updateLineItem(cartId, lineId, { quantity }, {}, await getAuthHeaders())\n    .then(() => {\n      revalidateTag(\"cart\")\n    })\n    .catch(medusaError)\n}\n\nexport async function deleteLineItem(lineId: unknown) {\n  if (typeof lineId !== \"string\") {\n    throw new Error(\"Missing lineItem ID when deleting line item\")\n  }\n\n  const cartId = await getCartId()\n  if (!cartId) {\n    throw new Error(\"Missing cart ID when deleting line item\")\n  }\n\n  await sdk.store.cart\n    .deleteLineItem(cartId, lineId, undefined, await getAuthHeaders())\n    .then(() => {\n      revalidateTag(\"cart\")\n    })\n    .catch(medusaError)\n  revalidateTag(\"cart\")\n}\n\nexport async function setShippingMethod({\n  cartId,\n  shippingMethodId,\n}: {\n  cartId: unknown\n  shippingMethodId: unknown\n}) {\n  if (typeof cartId !== \"string\") {\n    throw new Error(\"Missing cart ID when setting shipping method\")\n  }\n\n  if (typeof shippingMethodId !== \"string\") {\n    throw new Error(\"Missing shipping method ID when setting shipping method\")\n  }\n\n  return sdk.store.cart\n    .addShippingMethod(\n      cartId,\n      { option_id: shippingMethodId },\n      {},\n      await getAuthHeaders()\n    )\n    .then(() => {\n      revalidateTag(\"cart\")\n    })\n    .catch(medusaError)\n}\n\nexport async function setPaymentMethod(\n  session_id: string,\n  token: string | null | undefined\n) {\n  await sdk.client\n    .fetch(\"/store/custom/stripe/set-payment-method\", {\n      method: \"POST\",\n      body: { session_id, token },\n    })\n    .then((resp) => {\n      revalidateTag(\"cart\")\n      return resp\n    })\n    .catch(medusaError)\n}\n\nexport async function getPaymentMethod(id: string) {\n  return await sdk.client\n    .fetch<PaymentMethod>(`/store/custom/stripe/get-payment-method/${id}`)\n    .then((resp: PaymentMethod) => {\n      return resp\n    })\n    .catch(medusaError)\n}\n\nexport async function initiatePaymentSession(provider_id: unknown) {\n  const cart = await retrieveCart()\n\n  if (!cart) {\n    throw new Error(\"Can't initiate payment without cart\")\n  }\n\n  if (typeof provider_id !== \"string\") {\n    throw new Error(\"Invalid payment provider\")\n  }\n\n  return sdk.store.payment\n    .initiatePaymentSession(\n      cart,\n      {\n        provider_id,\n      },\n      {},\n      await getAuthHeaders()\n    )\n    .then((resp) => {\n      revalidateTag(\"cart\")\n      return resp\n    })\n    .catch(medusaError)\n}\n\nexport async function applyPromotions(codes: string[]) {\n  const cartId = await getCartId()\n  if (!cartId) {\n    throw new Error(\"No existing cart found\")\n  }\n\n  await updateCart({ promo_codes: codes })\n    .then(() => {\n      revalidateTag(\"cart\")\n    })\n    .catch(medusaError)\n}\n\nexport async function removePromotions(codes: string[]) {\n  const cartId = await getCartId()\n  if (!cartId) {\n    throw new Error(\"No existing cart found\")\n  }\n\n  if (!Array.isArray(codes) || !codes.length) {\n    throw new Error(\"No promotion codes provided\")\n  }\n\n  if (codes.some((code) => typeof code !== \"string\" || !code.trim())) {\n    throw new Error(\"Invalid promotion codes\")\n  }\n\n  await sdk.client\n    .fetch<HttpTypes.StoreCartResponse>(`/store/carts/${cartId}/promotions`, {\n      method: \"DELETE\",\n      body: { promo_codes: codes },\n      headers: { ...(await getAuthHeaders()) },\n    })\n    .then(() => {\n      revalidateTag(\"cart\")\n    })\n    .catch(medusaError)\n}\n\nexport async function setEmail({\n  email,\n  country_code,\n}: {\n  email: string\n  country_code: string\n}) {\n  try {\n    const cartId = await getCartId()\n    if (!cartId) {\n      throw new Error(\"No existing cart found when setting addresses\")\n    }\n  } catch (e) {\n    return {\n      success: false,\n      error: e instanceof Error ? e.message : \"Could not get your cart\",\n    }\n  }\n\n  const countryCode = z.string().min(2).safeParse(country_code)\n  if (!countryCode.success) {\n    return { success: false, error: \"Invalid country code\" }\n  }\n\n  await updateCart({ email })\n\n  return { success: true, error: null }\n}\n\nexport async function setAddresses(\n  formData: z.infer<typeof addressesFormSchema>\n) {\n  try {\n    if (!formData) {\n      throw new Error(\"No form data found when setting addresses\")\n    }\n    const cartId = await getCartId()\n    if (!cartId) {\n      throw new Error(\"No existing cart found when setting addresses\")\n    }\n\n    await updateCart({\n      shipping_address: formData.shipping_address,\n      billing_address:\n        formData.same_as_billing === \"on\"\n          ? formData.shipping_address\n          : formData.billing_address,\n    })\n    revalidateTag(\"shipping\")\n    return { success: true, error: null }\n  } catch (e) {\n    return {\n      success: false,\n      error: e instanceof Error ? e.message : \"Could not set addresses\",\n    }\n  }\n}\n\nexport async function placeOrder() {\n  const cartId = await getCartId()\n  if (!cartId) {\n    throw new Error(\"No existing cart found when placing an order\")\n  }\n\n  const cartRes = await sdk.store.cart\n    .complete(cartId, {}, await getAuthHeaders())\n    .then((cartRes) => {\n      revalidateTag(\"cart\")\n      revalidateTag(\"orders\")\n      return cartRes\n    })\n    .catch(medusaError)\n\n  if (cartRes?.type === \"order\") {\n    await removeCartId()\n  }\n\n  return cartRes\n}\n\n/**\n * Updates the countryCode param and revalidate the regions cache\n * @param regionId\n * @param countryCode\n */\nexport async function updateRegion(countryCode: string, currentPath: string) {\n  if (typeof countryCode !== \"string\") {\n    throw new Error(\"Invalid country code\")\n  }\n\n  if (typeof currentPath !== \"string\") {\n    throw new Error(\"Invalid current path\")\n  }\n\n  const cartId = await getCartId()\n  const region = await getRegion(countryCode)\n\n  if (!region) {\n    throw new Error(`Region not found for country code: ${countryCode}`)\n  }\n\n  if (cartId) {\n    await updateCart({ region_id: region.id })\n    revalidateTag(\"cart\")\n  }\n\n  revalidateTag(\"regions\")\n  revalidateTag(\"products\")\n\n  redirect(`/${countryCode}${currentPath}`)\n}\n"
  },
  {
    "path": "storefront/src/lib/data/categories.ts",
    "content": "import { sdk } from \"@lib/config\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nexport const listCategories = async function () {\n  return sdk.client\n    .fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>(\n      \"/store/product-categories\",\n      {\n        query: { fields: \"+category_children\" },\n        next: { tags: [\"categories\"] },\n        cache: \"force-cache\",\n      }\n    )\n    .then(({ product_categories }) => product_categories)\n}\n\nexport const getCategoriesList = async function (\n  offset: number = 0,\n  limit: number = 100,\n  fields?: (keyof HttpTypes.StoreProductCategory)[]\n) {\n  return sdk.client.fetch<{\n    product_categories: HttpTypes.StoreProductCategory[]\n  }>(\"/store/product-categories\", {\n    query: {\n      limit,\n      offset,\n      fields: fields ? fields.join(\",\") : undefined,\n    },\n    next: { tags: [\"categories\"] },\n    cache: \"force-cache\",\n  })\n}\n\nexport const getCategoryByHandle = async function (categoryHandle: string[]) {\n  return sdk.client.fetch<HttpTypes.StoreProductCategoryListResponse>(\n    `/store/product-categories`,\n    {\n      query: { handle: categoryHandle },\n      next: { tags: [\"categories\"] },\n      cache: \"force-cache\",\n    }\n  )\n}\n"
  },
  {
    "path": "storefront/src/lib/data/collections.ts",
    "content": "import { sdk } from \"@lib/config\"\nimport { getProductsList } from \"@lib/data/products\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nexport const retrieveCollection = async function (id: string) {\n  return sdk.client\n    .fetch<{ collection: HttpTypes.StoreCollection }>(\n      `/store/collections/${id}`,\n      {\n        next: { tags: [\"collections\"] },\n        cache: \"force-cache\",\n      }\n    )\n    .then(({ collection }) => collection)\n}\n\nexport const getCollectionsList = async function (\n  offset: number = 0,\n  limit: number = 100,\n  fields?: (keyof HttpTypes.StoreCollection)[]\n): Promise<{ collections: HttpTypes.StoreCollection[]; count: number }> {\n  return sdk.client\n    .fetch<{\n      collections: HttpTypes.StoreCollection[]\n      count: number\n    }>(\"/store/collections\", {\n      query: { limit, offset, fields: fields ? fields.join(\",\") : undefined },\n      next: { tags: [\"collections\"] },\n      cache: \"force-cache\",\n    })\n    .then(({ collections }) => ({ collections, count: collections.length }))\n}\n\nexport const getCollectionByHandle = async function (\n  handle: string,\n  fields?: (keyof HttpTypes.StoreCollection)[]\n): Promise<HttpTypes.StoreCollection> {\n  return sdk.client\n    .fetch<HttpTypes.StoreCollectionListResponse>(`/store/collections`, {\n      query: {\n        handle,\n        fields: fields ? fields.join(\",\") : undefined,\n        limit: 1,\n      },\n      next: { tags: [\"collections\"] },\n      cache: \"force-cache\",\n    })\n    .then(({ collections }) => collections[0])\n}\n\nexport const getCollectionsWithProducts = async (\n  countryCode: string\n): Promise<HttpTypes.StoreCollection[] | null> => {\n  const { collections } = await getCollectionsList(0, 3)\n\n  if (!collections) {\n    return null\n  }\n\n  const collectionIds = collections\n    .map((collection) => collection.id)\n    .filter(Boolean) as string[]\n\n  const { response } = await getProductsList({\n    queryParams: { collection_id: collectionIds },\n    countryCode,\n  })\n\n  response.products.forEach((product) => {\n    const collection = collections.find(\n      (collection) => collection.id === product.collection_id\n    )\n\n    if (collection) {\n      if (!collection.products) {\n        collection.products = []\n      }\n\n      collection.products.push(product)\n    }\n  })\n\n  return collections as unknown as HttpTypes.StoreCollection[]\n}\n"
  },
  {
    "path": "storefront/src/lib/data/cookies.ts",
    "content": "import \"server-only\"\nimport { cookies } from \"next/headers\"\n\nexport const getAuthHeaders = async (): Promise<\n  // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n  { authorization: string } | {}\n> => {\n  const token = (await cookies()).get(\"_medusa_jwt\")?.value\n\n  if (token) {\n    return { authorization: `Bearer ${token}` }\n  }\n\n  return {}\n}\n\nexport const setAuthToken = async (token: string) => {\n  return (await cookies()).set(\"_medusa_jwt\", token, {\n    maxAge: 60 * 60 * 24 * 7,\n    httpOnly: true,\n    sameSite: \"strict\",\n    secure: process.env.NODE_ENV === \"production\",\n  })\n}\n\nexport const removeAuthToken = async () => {\n  return (await cookies()).set(\"_medusa_jwt\", \"\", {\n    maxAge: -1,\n  })\n}\n\nexport const getCartId = async () => {\n  return (await cookies()).get(\"_medusa_cart_id\")?.value\n}\n\nexport const setCartId = async (cartId: string) => {\n  return (await cookies()).set(\"_medusa_cart_id\", cartId, {\n    maxAge: 60 * 60 * 24 * 7,\n    httpOnly: true,\n    sameSite: \"strict\",\n    secure: process.env.NODE_ENV === \"production\",\n  })\n}\n\nexport const removeCartId = async () => {\n  return (await cookies()).set(\"_medusa_cart_id\", \"\", { maxAge: -1 })\n}\n"
  },
  {
    "path": "storefront/src/lib/data/customer.ts",
    "content": "\"use server\"\n\nimport { z } from \"zod\"\nimport { redirect } from \"next/navigation\"\nimport { revalidateTag } from \"next/cache\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nimport { sdk } from \"@lib/config\"\nimport {\n  getAuthHeaders,\n  setAuthToken,\n  removeAuthToken,\n  getCartId,\n} from \"@lib/data/cookies\"\nimport {\n  customerAddressSchema,\n  loginFormSchema,\n  signupFormSchema,\n  updateCustomerFormSchema,\n} from \"hooks/customer\"\n\nexport const getCustomer = async function () {\n  return await sdk.client\n    .fetch<{ customer: HttpTypes.StoreCustomer }>(`/store/customers/me`, {\n      next: { tags: [\"customer\"] },\n      headers: { ...(await getAuthHeaders()) },\n      cache: \"no-store\",\n    })\n    .then(({ customer }) => customer)\n    .catch(() => null)\n}\n\nexport const updateCustomer = async function (\n  formData: z.infer<typeof updateCustomerFormSchema>\n): Promise<\n  { state: \"initial\" | \"success\" } | { state: \"error\"; error: string }\n> {\n  return sdk.store.customer\n    .update(\n      {\n        first_name: formData.first_name,\n        last_name: formData.last_name,\n        phone: formData.phone ?? undefined,\n      },\n      {},\n      await getAuthHeaders()\n    )\n    .then(() => {\n      revalidateTag(\"customer\")\n      return {\n        state: \"success\" as const,\n      }\n    })\n    .catch(() => {\n      revalidateTag(\"customer\")\n      return {\n        state: \"error\" as const,\n        error: \"Failed to update customer personal information\",\n      }\n    })\n}\n\nexport async function signup(formData: z.infer<typeof signupFormSchema>) {\n  try {\n    const token = await sdk.auth.register(\"customer\", \"emailpass\", {\n      email: formData.email,\n      password: formData.password,\n    })\n\n    const customHeaders = { authorization: `Bearer ${token}` }\n\n    const { customer: createdCustomer } = await sdk.store.customer.create(\n      {\n        email: formData.email,\n        first_name: formData.first_name,\n        last_name: formData.last_name,\n        phone: formData.phone ?? undefined,\n      },\n      {},\n      customHeaders\n    )\n\n    const loginToken = await sdk.auth.login(\"customer\", \"emailpass\", {\n      email: formData.email,\n      password: formData.password,\n    })\n\n    if (typeof loginToken === \"object\") {\n      redirect(loginToken.location)\n\n      return { success: true, customer: createdCustomer }\n    }\n\n    await setAuthToken(loginToken)\n\n    await sdk.client.fetch(\"/store/custom/customer/send-welcome-email\", {\n      method: \"POST\",\n      headers: await getAuthHeaders(),\n    })\n\n    revalidateTag(\"customer\")\n\n    const cartId = await getCartId()\n    if (cartId) {\n      await sdk.store.cart.transferCart(cartId, {}, await getAuthHeaders())\n      revalidateTag(\"cart\")\n    }\n\n    return { success: true, customer: createdCustomer }\n  } catch (error) {\n    return {\n      success: false,\n      error: error instanceof Error ? error.message : `${error}`,\n    }\n  }\n}\n\nexport async function login(formData: z.infer<typeof loginFormSchema>) {\n  const redirectUrl = formData.redirect_url\n\n  try {\n    const token = await sdk.auth.login(\"customer\", \"emailpass\", {\n      email: formData.email,\n      password: formData.password,\n    })\n\n    if (typeof token === \"object\") {\n      return { success: true, redirectUrl: token.location }\n    }\n\n    await setAuthToken(token)\n    revalidateTag(\"customer\")\n\n    const cartId = await getCartId()\n    if (cartId) {\n      await sdk.store.cart.transferCart(cartId, {}, await getAuthHeaders())\n      revalidateTag(\"cart\")\n    }\n    return { success: true, redirectUrl: redirectUrl || \"/\" }\n  } catch (error) {\n    return {\n      success: false,\n      message: error instanceof Error ? error.message : `${error}`,\n    }\n  }\n}\n\nexport async function signout(countryCode: string) {\n  await sdk.auth.logout()\n  await removeAuthToken()\n  revalidateTag(\"customer\")\n  return countryCode\n}\n\nexport const addCustomerAddress = async (\n  formData: z.infer<typeof customerAddressSchema>\n) => {\n  return sdk.store.customer\n    .createAddress(\n      {\n        first_name: formData.first_name,\n        last_name: formData.last_name,\n        company: formData.company ?? undefined,\n        address_1: formData.address_1,\n        address_2: formData.address_2 ?? undefined,\n        city: formData.city,\n        postal_code: formData.postal_code,\n        province: formData.province ?? undefined,\n        country_code: formData.country_code,\n        phone: formData.phone ?? undefined,\n      },\n      {},\n      await getAuthHeaders()\n    )\n    .then(({ customer }) => {\n      revalidateTag(\"customer\")\n      return {\n        addressId: customer.addresses[customer.addresses.length - 1].id,\n        success: true,\n        error: null,\n      }\n    })\n    .catch((err) => {\n      revalidateTag(\"customer\")\n      return { addressId: \"\", success: false, error: err.toString() }\n    })\n}\n\nexport const deleteCustomerAddress = async (\n  addressId: unknown\n): Promise<void> => {\n  if (typeof addressId !== \"string\") {\n    throw new Error(\"Invalid input data\")\n  }\n\n  await sdk.store.customer\n    .deleteAddress(addressId, await getAuthHeaders())\n    .then(() => {\n      return { success: true, error: null }\n    })\n    .catch((err) => {\n      return { success: false, error: err.toString() }\n    })\n  revalidateTag(\"customer\")\n}\n\nexport const updateCustomerAddress = async (\n  addressId: string,\n  formData: z.infer<typeof customerAddressSchema>\n) => {\n  if (!addressId) {\n    throw new Error(\"Invalid input data\")\n  }\n\n  return sdk.store.customer\n    .updateAddress(\n      addressId,\n      {\n        first_name: formData.first_name,\n        last_name: formData.last_name,\n        company: formData.company ?? undefined,\n        address_1: formData.address_1,\n        address_2: formData.address_2 ?? undefined,\n        city: formData.city,\n        postal_code: formData.postal_code,\n        province: formData.province ?? undefined,\n        country_code: formData.country_code,\n        phone: formData.phone ?? undefined,\n      },\n      {},\n      await getAuthHeaders()\n    )\n    .then(() => {\n      revalidateTag(\"customer\")\n      return { addressId, success: true, error: null }\n    })\n    .catch((err) => {\n      revalidateTag(\"customer\")\n      return { addressId, success: false, error: err.toString() }\n    })\n}\n\nexport async function requestPasswordReset() {\n  const customer = await getCustomer()\n\n  if (!customer) {\n    return {\n      success: false as const,\n      error: \"No customer found\",\n    }\n  }\n  await sdk.auth.resetPassword(\"logged-in-customer\", \"emailpass\", {\n    identifier: customer.email,\n  })\n\n  return {\n    success: true as const,\n  }\n}\n\nconst resetPasswordStateSchema = z.object({\n  email: z.string().email(),\n  token: z.string(),\n})\n\nconst resetPasswordFormSchema = z.object({\n  type: z.literal(\"reset\"),\n  current_password: z.string().min(6),\n  new_password: z.string().min(6),\n  confirm_new_password: z.string().min(6),\n})\n\nconst forgotPasswordSchema = z.object({\n  type: z.literal(\"forgot\"),\n  new_password: z.string().min(6),\n  confirm_new_password: z.string().min(6),\n})\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst baseSchema = z.discriminatedUnion(\"type\", [\n  resetPasswordFormSchema,\n  forgotPasswordSchema,\n])\n\nexport async function resetPassword(\n  currentState: unknown,\n  formData: z.infer<typeof baseSchema>\n): Promise<\n  z.infer<typeof resetPasswordStateSchema> &\n    ({ state: \"initial\" | \"success\" } | { state: \"error\"; error: string })\n> {\n  const validatedState = resetPasswordStateSchema.parse(currentState)\n  if (formData.type === \"reset\") {\n    try {\n      await sdk.auth.login(\"customer\", \"emailpass\", {\n        email: validatedState.email,\n        password: formData.current_password,\n      })\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    } catch (error) {\n      return {\n        ...validatedState,\n        state: \"error\" as const,\n        error: \"Wrong password\",\n      }\n    }\n  }\n  return sdk.auth\n    .updateProvider(\n      formData.type === \"reset\" ? \"logged-in-customer\" : \"customer\",\n      \"emailpass\",\n      {\n        email: validatedState.email,\n        password: formData.new_password,\n      },\n      validatedState.token\n    )\n    .then(() => {\n      return {\n        ...validatedState,\n        state: \"success\" as const,\n      }\n    })\n    .catch(() => {\n      return {\n        ...validatedState,\n        state: \"error\" as const,\n        error: \"Failed to update password\",\n      }\n    })\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst forgotPasswordFormSchema = z.object({\n  email: z.string().email(),\n})\n\nexport async function forgotPassword(\n  _currentState: unknown,\n  formData: z.infer<typeof forgotPasswordFormSchema>\n): Promise<\n  { state: \"initial\" | \"success\" } | { state: \"error\"; error: string }\n> {\n  return sdk.auth\n    .resetPassword(\"customer\", \"emailpass\", {\n      identifier: formData.email,\n    })\n    .then(() => {\n      return {\n        state: \"success\" as const,\n      }\n    })\n    .catch(() => {\n      return {\n        state: \"error\" as const,\n        error: \"Failed to reset password\",\n      }\n    })\n}\n\nexport async function updateDefaultShippingAddress(addressId: string) {\n  if (!addressId) {\n    return { success: false, error: \"No address id provided\" }\n  }\n\n  return sdk.store.customer\n    .updateAddress(\n      addressId,\n      {\n        is_default_shipping: true,\n      },\n      {},\n      await getAuthHeaders()\n    )\n    .then(() => {\n      revalidateTag(\"customer\")\n      return { success: true, error: null }\n    })\n    .catch((err) => {\n      revalidateTag(\"customer\")\n      return { success: false, error: err.toString() }\n    })\n}\n\nexport async function updateDefaultBillingAddress(addressId: string) {\n  if (!addressId) {\n    return { success: false, error: \"No address id provided\" }\n  }\n\n  return sdk.store.customer\n    .updateAddress(\n      addressId,\n      {\n        is_default_billing: true,\n      },\n      {},\n      await getAuthHeaders()\n    )\n    .then(() => {\n      revalidateTag(\"customer\")\n      return { success: true, error: null }\n    })\n    .catch((err) => {\n      revalidateTag(\"customer\")\n      return { success: false, error: err.toString() }\n    })\n}\n"
  },
  {
    "path": "storefront/src/lib/data/fulfillment.ts",
    "content": "import { sdk } from \"@lib/config\"\nimport { HttpTypes } from \"@medusajs/types\"\n\n// Shipping actions\nexport const listCartShippingMethods = async function (cartId: string) {\n  return sdk.client\n    .fetch<HttpTypes.StoreShippingOptionListResponse>(\n      `/store/shipping-options`,\n      {\n        query: { cart_id: cartId },\n        next: { tags: [\"shipping\"] },\n        cache: \"force-cache\",\n      }\n    )\n    .then(({ shipping_options }) => shipping_options)\n    .catch(() => {\n      return null\n    })\n}\n"
  },
  {
    "path": "storefront/src/lib/data/orders.ts",
    "content": "\"use server\"\n\nimport { cache } from \"react\"\nimport { sdk } from \"@lib/config\"\nimport medusaError from \"@lib/util/medusa-error\"\nimport { enrichLineItems } from \"@lib/util/enrich-line-items\"\nimport { getAuthHeaders } from \"@lib/data/cookies\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nexport const retrieveOrder = cache(async (id: unknown) => {\n  if (typeof id !== \"string\") {\n    throw new Error(\"Invalid order id\")\n  }\n\n  const order = await sdk.client\n    .fetch<HttpTypes.StoreOrderResponse>(`/store/orders/${id}`, {\n      query: { fields: \"*payment_collections.payments\" },\n      next: { tags: [\"orders\"] },\n      headers: { ...(await getAuthHeaders()) },\n    })\n    .then(({ order }) => order)\n    .catch((err) => medusaError(err))\n\n  if (order.items?.length && order.region_id) {\n    order.items = await enrichLineItems(order.items, order.region_id)\n  }\n\n  return order\n})\n\nexport const listOrders = async function (\n  limit: number = 10,\n  offset: number = 0\n) {\n  if (\n    typeof limit !== \"number\" ||\n    typeof offset !== \"number\" ||\n    limit < 1 ||\n    offset < 0 ||\n    limit > 100 ||\n    !Number.isSafeInteger(offset)\n  ) {\n    throw new Error(\"Invalid input data\")\n  }\n\n  return sdk.client\n    .fetch<HttpTypes.StoreOrderListResponse>(`/store/orders`, {\n      query: { limit, offset, order: \"-created_at\" },\n      next: { tags: [\"orders\"] },\n      headers: { ...(await getAuthHeaders()) },\n    })\n    .catch((err) => medusaError(err))\n}\n"
  },
  {
    "path": "storefront/src/lib/data/payment.ts",
    "content": "import { sdk } from \"@lib/config\"\nimport { HttpTypes } from \"@medusajs/types\"\n\n// Shipping actions\nexport const listCartPaymentMethods = async function (regionId: string) {\n  return sdk.client\n    .fetch<HttpTypes.StorePaymentProviderListResponse>(\n      `/store/payment-providers`,\n      {\n        query: { region_id: regionId },\n        next: { tags: [\"payment_providers\"] },\n        cache: \"force-cache\",\n      }\n    )\n    .then(({ payment_providers }) => payment_providers)\n    .catch(() => {\n      return null\n    })\n}\n"
  },
  {
    "path": "storefront/src/lib/data/product-types.ts",
    "content": "import { sdk } from \"@lib/config\"\nimport { HttpTypes, PaginatedResponse } from \"@medusajs/types\"\n\nexport const getProductTypesList = async function (\n  offset: number = 0,\n  limit: number = 100,\n  fields?: (keyof HttpTypes.StoreProductType)[]\n): Promise<{ productTypes: HttpTypes.StoreProductType[]; count: number }> {\n  return sdk.client\n    .fetch<\n      PaginatedResponse<{\n        product_types: HttpTypes.StoreProductType[]\n        count: number\n      }>\n    >(\"/store/custom/product-types\", {\n      query: { limit, offset, fields: fields ? fields.join(\",\") : undefined },\n      next: { tags: [\"product-types\"] },\n      cache: \"force-cache\",\n    })\n    .then(({ product_types, count }) => ({\n      productTypes: product_types,\n      count,\n    }))\n}\n\nexport const getProductTypeByHandle = async function (\n  handle: string\n): Promise<HttpTypes.StoreProductType> {\n  return sdk.client\n    .fetch<\n      PaginatedResponse<{\n        product_types: HttpTypes.StoreProductType[]\n        count: number\n      }>\n    >(\"/store/custom/product-types\", {\n      query: { handle, limit: 1 },\n      next: { tags: [\"product-types\"] },\n      cache: \"force-cache\",\n    })\n    .then(({ product_types }) => product_types[0])\n}\n"
  },
  {
    "path": "storefront/src/lib/data/products.ts",
    "content": "\"use server\"\n\nimport { sdk } from \"@lib/config\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport { getRegion } from \"@lib/data/regions\"\nimport { SortOptions } from \"@modules/store/components/refinement-list/sort-products\"\nimport { sortProducts } from \"@lib/util/sort-products\"\n\nexport const getProductsById = async function ({\n  ids,\n  regionId,\n}: {\n  ids: string[]\n  regionId: string\n}) {\n  return sdk.client\n    .fetch<{ products: HttpTypes.StoreProduct[] }>(`/store/products`, {\n      query: {\n        id: ids,\n        region_id: regionId,\n        fields: \"*variants.calculated_price,+variants.inventory_quantity\",\n      } satisfies HttpTypes.StoreProductListParams,\n      next: { tags: [\"products\"] },\n      cache: \"force-cache\",\n    })\n    .then(({ products }) => products)\n}\n\nexport const getProductByHandle = async function (\n  handle: string,\n  regionId: string\n) {\n  return sdk.client\n    .fetch<{ products: HttpTypes.StoreProduct[] }>(`/store/products`, {\n      query: {\n        handle,\n        region_id: regionId,\n        fields: \"*variants.calculated_price,+variants.inventory_quantity\",\n      } satisfies HttpTypes.StoreProductListParams,\n      next: { tags: [\"products\"] },\n    })\n    .then(({ products }) => products[0])\n}\n\nexport const getProductFashionDataByHandle = async function (handle: string) {\n  return sdk.client.fetch<{\n    materials: {\n      id: string\n      name: string\n      colors: {\n        id: string\n        name: string\n        hex_code: string\n      }[]\n    }[]\n  }>(`/store/custom/fashion/${handle}`, {\n    method: \"GET\",\n    next: { tags: [\"products\"] },\n    cache: \"force-cache\",\n  })\n}\n\nexport const getProductsList = async function ({\n  pageParam = 1,\n  queryParams,\n  countryCode,\n}: {\n  pageParam?: number\n  queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductListParams\n  countryCode: string\n}): Promise<{\n  response: { products: HttpTypes.StoreProduct[]; count: number }\n  nextPage: number | null\n  queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductListParams\n}> {\n  const page = Math.max(1, pageParam || 1)\n  const limit = queryParams?.limit || 12\n  const offset = (page - 1) * limit\n  const region = await getRegion(countryCode)\n\n  if (!region) {\n    return {\n      response: { products: [], count: 0 },\n      nextPage: null,\n    }\n  }\n  return sdk.client\n    .fetch<{ products: HttpTypes.StoreProduct[]; count: number }>(\n      `/store/products`,\n      {\n        query: {\n          limit,\n          offset,\n          region_id: region.id,\n          fields: \"*variants.calculated_price\",\n          ...queryParams,\n        } satisfies HttpTypes.StoreProductListParams,\n        next: { tags: [\"products\"] },\n        cache: \"force-cache\",\n      }\n    )\n    .then(({ products, count }) => {\n      const nextPage = count > offset + limit ? page + 1 : null\n\n      return {\n        response: {\n          products,\n          count,\n        },\n        nextPage,\n        queryParams,\n      }\n    })\n}\n\n/**\n * This will fetch 100 products to the Next.js cache and sort them based on the sortBy parameter.\n * It will then return the paginated products based on the page and limit parameters.\n */\nexport const getProductsListWithSort = async function ({\n  page = 1,\n  queryParams,\n  sortBy = \"created_at\",\n  countryCode,\n}: {\n  page?: number\n  queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams\n  sortBy?: SortOptions\n  countryCode: string\n}): Promise<{\n  response: { products: HttpTypes.StoreProduct[]; count: number }\n  nextPage: number | null\n  queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams\n}> {\n  const limit = queryParams?.limit || 12\n\n  const {\n    response: { products, count },\n  } = await getProductsList({\n    pageParam: 0,\n    queryParams: {\n      ...queryParams,\n      limit: 100,\n    },\n    countryCode,\n  })\n\n  const sortedProducts = sortProducts(products, sortBy)\n  const pageParam = (page - 1) * limit\n  const nextPage = count > pageParam + limit ? pageParam + limit : null\n  const paginatedProducts = sortedProducts.slice(pageParam, pageParam + limit)\n\n  return {\n    response: {\n      products: paginatedProducts,\n      count,\n    },\n    nextPage,\n    queryParams,\n  }\n}\n"
  },
  {
    "path": "storefront/src/lib/data/regions.ts",
    "content": "\"use server\"\nimport { sdk } from \"@lib/config\"\nimport medusaError from \"@lib/util/medusa-error\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nexport const listRegions = async function () {\n  return sdk.client\n    .fetch<{ regions: HttpTypes.StoreRegion[] }>(`/store/regions`, {\n      method: \"GET\",\n      next: { tags: [\"regions\"] },\n      cache: \"force-cache\",\n    })\n    .then(({ regions }) => regions)\n    .catch(medusaError)\n}\n\nexport const retrieveRegion = async function (id: string) {\n  return sdk.client\n    .fetch<{ region: HttpTypes.StoreRegion }>(`/store/regions/${id}`, {\n      method: \"GET\",\n      next: { tags: [`regions`] },\n      cache: \"force-cache\",\n    })\n    .then(({ region }) => region)\n    .catch(medusaError)\n}\n\nconst regionMap = new Map<string, HttpTypes.StoreRegion>()\n\nexport const getRegion = async function (countryCode: string) {\n  try {\n    if (regionMap.has(countryCode)) {\n      return regionMap.get(countryCode)\n    }\n\n    const regions = await listRegions()\n\n    if (!regions) {\n      return null\n    }\n\n    regions.forEach((region) => {\n      region.countries?.forEach((c) => {\n        regionMap.set(c?.iso_2 ?? \"\", region)\n      })\n    })\n\n    const region = countryCode\n      ? regionMap.get(countryCode)\n      : regionMap.get(\"us\")\n\n    return region\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  } catch (e) {\n    return null\n  }\n}\n"
  },
  {
    "path": "storefront/src/lib/search-client.ts",
    "content": "import { MeiliSearch } from \"meilisearch\"\n\nconst endpoint =\n  process.env.NEXT_PUBLIC_SEARCH_ENDPOINT || \"http://localhost:7700\"\n\nconst apiKey = process.env.NEXT_PUBLIC_SEARCH_API_KEY || \"test_key\"\n\nexport interface MeiliSearchProductHit {\n  id: string\n  handle: string\n  title: string\n  thumbnail: string\n  variants: string[]\n}\n\nexport const searchClient = new MeiliSearch({\n  host: endpoint,\n  apiKey,\n})\n"
  },
  {
    "path": "storefront/src/lib/util/collections.ts",
    "content": "import { z } from \"zod\"\n\nexport const collectionMetadataCustomFieldsSchema = z.object({\n  image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n  description: z.string().optional(),\n  collection_page_image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n  collection_page_heading: z.string().optional(),\n  collection_page_content: z.string().optional(),\n  product_page_heading: z.string().optional(),\n  product_page_image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n  product_page_wide_image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n  product_page_cta_image: z\n    .object({\n      id: z.string(),\n      url: z.string().url(),\n    })\n    .optional(),\n  product_page_cta_heading: z.string().optional(),\n  product_page_cta_link: z.string().optional(),\n})\n"
  },
  {
    "path": "storefront/src/lib/util/compare-addresses.ts",
    "content": "import { HttpTypes } from \"@medusajs/types\"\nimport { isEqual, pick } from \"lodash\"\n\nexport default function compareAddresses(\n  address1: Pick<\n    HttpTypes.StoreCartAddress,\n    | \"first_name\"\n    | \"last_name\"\n    | \"address_1\"\n    | \"address_2\"\n    | \"company\"\n    | \"postal_code\"\n    | \"city\"\n    | \"country_code\"\n    | \"province\"\n    | \"phone\"\n  >,\n  address2: Pick<\n    HttpTypes.StoreCartAddress,\n    | \"first_name\"\n    | \"last_name\"\n    | \"address_1\"\n    | \"address_2\"\n    | \"company\"\n    | \"postal_code\"\n    | \"city\"\n    | \"country_code\"\n    | \"province\"\n    | \"phone\"\n  >\n) {\n  return isEqual(\n    pick(address1, [\n      \"first_name\",\n      \"last_name\",\n      \"address_1\",\n      \"address_2\",\n      \"company\",\n      \"postal_code\",\n      \"city\",\n      \"country_code\",\n      \"province\",\n      \"phone\",\n    ]),\n    pick(address2, [\n      \"first_name\",\n      \"last_name\",\n      \"address_1\",\n      \"address_2\",\n      \"company\",\n      \"postal_code\",\n      \"city\",\n      \"country_code\",\n      \"province\",\n      \"phone\",\n    ])\n  )\n}\n"
  },
  {
    "path": "storefront/src/lib/util/enrich-line-items.ts",
    "content": "import \"server-only\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport { omit } from \"lodash\"\n\nimport { getProductsById } from \"@lib/data/products\"\n\nexport async function enrichLineItems<\n  T extends HttpTypes.StoreCartLineItem[] | HttpTypes.StoreOrderLineItem[],\n>(lineItems: T | null, regionId: string): Promise<T> {\n  if (!lineItems) return [] as unknown as T\n\n  // Prepare query parameters\n  const queryParams = {\n    ids: lineItems.map((lineItem) => lineItem.product_id!),\n    regionId,\n  }\n\n  // Fetch products by their IDs\n  const products = await getProductsById(queryParams)\n  // If there are no line items or products, return an empty array\n  if (!lineItems?.length || !products) {\n    return [] as unknown as T\n  }\n\n  // Enrich line items with product and variant information\n  const enrichedItems = lineItems.map((item) => {\n    const product = products.find((p) => p.id === item.product_id)\n    const variant = product?.variants?.find((v) => v.id === item.variant_id)\n\n    // If product or variant is not found, return the original item\n    if (!product || !variant) {\n      return item\n    }\n\n    // If product and variant are found, enrich the item\n    return {\n      ...item,\n      variant: {\n        ...variant,\n        product: omit(product, \"variants\"),\n      },\n    }\n  }) as T\n\n  return enrichedItems\n}\n"
  },
  {
    "path": "storefront/src/lib/util/env.ts",
    "content": "export const getBaseURL = () => {\n  return process.env.NEXT_PUBLIC_BASE_URL || \"https://localhost:8000\"\n}\n"
  },
  {
    "path": "storefront/src/lib/util/get-precentage-diff.ts",
    "content": "export const getPercentageDiff = (original: number, calculated: number) => {\n  const diff = original - calculated\n  const decrease = (diff / original) * 100\n\n  return decrease.toFixed()\n}\n"
  },
  {
    "path": "storefront/src/lib/util/get-product-price.ts",
    "content": "import { HttpTypes } from \"@medusajs/types\"\nimport { getPercentageDiff } from \"@lib/util/get-precentage-diff\"\nimport { convertToLocale } from \"@lib/util/money\"\n\nexport const getPricesForVariant = (variant: HttpTypes.StoreProductVariant) => {\n  if (!variant?.calculated_price?.calculated_amount) {\n    return null\n  }\n\n  return {\n    calculated_price_number: variant.calculated_price.calculated_amount,\n    calculated_price: convertToLocale({\n      amount: variant.calculated_price.calculated_amount,\n      currency_code: variant.calculated_price.currency_code ?? \"\",\n    }),\n    original_price_number: variant.calculated_price.original_amount,\n    original_price: convertToLocale({\n      amount: variant.calculated_price.original_amount ?? 0,\n      currency_code: variant.calculated_price.currency_code ?? \"\",\n    }),\n    currency_code: variant.calculated_price.currency_code,\n    price_type: variant.calculated_price.calculated_price?.price_list_type,\n    percentage_diff: getPercentageDiff(\n      variant.calculated_price.original_amount ?? 0,\n      variant.calculated_price.calculated_amount\n    ),\n  }\n}\n\nexport function getProductPrice({\n  product,\n  variantId,\n}: {\n  product: HttpTypes.StoreProduct\n  variantId?: string\n}) {\n  if (!product || !product.id) {\n    throw new Error(\"No product provided\")\n  }\n\n  const cheapestPrice = () => {\n    if (!product || !product.variants?.length) {\n      return null\n    }\n\n    const cheapestVariant = product.variants\n      .filter((v) => !!v.calculated_price)\n      .sort((a, b) => {\n        return (\n          (a.calculated_price?.calculated_amount ?? 0) -\n          (b.calculated_price?.calculated_amount ?? 0)\n        )\n      })[0]\n\n    return getPricesForVariant(cheapestVariant)\n  }\n\n  const variantPrice = () => {\n    if (!product || !variantId) {\n      return null\n    }\n\n    const variant = product.variants?.find(\n      (v) => v.id === variantId || v.sku === variantId\n    )\n\n    if (!variant) {\n      return null\n    }\n\n    return getPricesForVariant(variant)\n  }\n\n  return {\n    product,\n    cheapestPrice: cheapestPrice(),\n    variantPrice: variantPrice(),\n  }\n}\n"
  },
  {
    "path": "storefront/src/lib/util/inventory.ts",
    "content": "import { HttpTypes } from \"@medusajs/types\"\n\nexport function getVariantItemsInStock(variant: HttpTypes.StoreProductVariant) {\n  // If we don't manage inventory, we can always add to cart\n  if (variant && !variant.manage_inventory) {\n    return Number.MAX_SAFE_INTEGER\n  }\n\n  // If we allow back orders on the variant, we can always add to cart\n  if (variant.allow_backorder) {\n    return Number.MAX_SAFE_INTEGER\n  }\n\n  // If there is inventory available, return the inventory quantity\n  if (variant.manage_inventory && (variant.inventory_quantity || 0) > 0) {\n    return variant.inventory_quantity!\n  }\n\n  // Otherwise, return 0\n  return 0\n}\n"
  },
  {
    "path": "storefront/src/lib/util/isEmpty.ts",
    "content": "export const isObject = (input: unknown) => input instanceof Object\nexport const isArray = (input: unknown) => Array.isArray(input)\nexport const isEmpty = (input: unknown) => {\n  return (\n    input === null ||\n    input === undefined ||\n    (isObject(input) && Object.keys(input).length === 0) ||\n    (isArray(input) && input.length === 0) ||\n    (typeof input === \"string\" && input.trim().length === 0)\n  )\n}\n"
  },
  {
    "path": "storefront/src/lib/util/medusa-error.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport default function medusaError(error: any): never {\n  if (error.response) {\n    // The request was made and the server responded with a status code\n    // that falls out of the range of 2xx\n    const u = new URL(error.config.url, error.config.baseURL)\n    console.error(\"Resource:\", u.toString())\n    console.error(\"Response data:\", error.response.data)\n    console.error(\"Status code:\", error.response.status)\n    console.error(\"Headers:\", error.response.headers)\n\n    // Extracting the error message from the response data\n    const message = error.response.data.message || error.response.data\n\n    throw new Error(message.charAt(0).toUpperCase() + message.slice(1) + \".\")\n  } else if (error.request) {\n    // The request was made but no response was received\n    throw new Error(\"No response received: \" + error.request)\n  } else {\n    // Something happened in setting up the request that triggered an Error\n    throw new Error(\"Error setting up the request: \" + error.message)\n  }\n}\n"
  },
  {
    "path": "storefront/src/lib/util/money.ts",
    "content": "import { isEmpty } from \"@lib/util/isEmpty\"\n\ntype ConvertToLocaleParams = {\n  amount: number\n  currency_code: string\n  minimumFractionDigits?: number\n  maximumFractionDigits?: number\n  locale?: string\n}\n\nexport const convertToLocale = ({\n  amount,\n  currency_code,\n  minimumFractionDigits,\n  maximumFractionDigits,\n  locale = \"en-US\",\n}: ConvertToLocaleParams) => {\n  return currency_code && !isEmpty(currency_code)\n    ? new Intl.NumberFormat(locale, {\n        style: \"currency\",\n        currency: currency_code,\n        minimumFractionDigits,\n        maximumFractionDigits,\n      }).format(amount)\n    : amount.toString()\n}\n"
  },
  {
    "path": "storefront/src/lib/util/react-query.tsx",
    "content": "import * as React from \"react\"\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\"\n\nconst queryClient = new QueryClient()\n\nexport const ReactQueryProvider: React.FC<{ children?: React.ReactNode }> = ({\n  children,\n}) => {\n  return (\n    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n  )\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport const withReactQueryProvider = <T extends {}>(\n  Component: React.FC<T>\n) => {\n  const WrappedComponent = (props: T) => (\n    <ReactQueryProvider>\n      <Component {...props} />\n    </ReactQueryProvider>\n  )\n\n  WrappedComponent.displayName = `withReactQueryProvider(${Component.displayName || Component.name || \"Component\"})`\n\n  return WrappedComponent\n}\n"
  },
  {
    "path": "storefront/src/lib/util/repeat.ts",
    "content": "const repeat = (times: number) => {\n  return Array.from(Array(times).keys())\n}\n\nexport default repeat\n"
  },
  {
    "path": "storefront/src/lib/util/sort-products.ts",
    "content": "import { HttpTypes } from \"@medusajs/types\"\nimport { SortOptions } from \"@modules/store/components/refinement-list/sort-products\"\n\ninterface MinPricedProduct extends HttpTypes.StoreProduct {\n  _minPrice?: number\n}\n\n/**\n * Helper function to sort products by price until the store API supports sorting by price\n * @param products\n * @param sortBy\n * @returns products sorted by price\n */\nexport function sortProducts(\n  products: HttpTypes.StoreProduct[],\n  sortBy: SortOptions\n): HttpTypes.StoreProduct[] {\n  const sortedProducts = products as MinPricedProduct[]\n\n  if ([\"price_asc\", \"price_desc\"].includes(sortBy)) {\n    // Precompute the minimum price for each product\n    sortedProducts.forEach((product) => {\n      if (product.variants && product.variants.length > 0) {\n        product._minPrice = Math.min(\n          ...product.variants.map(\n            (variant) => variant?.calculated_price?.calculated_amount || 0\n          )\n        )\n      } else {\n        product._minPrice = Infinity\n      }\n    })\n\n    // Sort products based on the precomputed minimum prices\n    sortedProducts.sort((a, b) => {\n      const diff = a._minPrice! - b._minPrice!\n      return sortBy === \"price_asc\" ? diff : -diff\n    })\n  }\n\n  if (sortBy === \"created_at\") {\n    sortedProducts.sort((a, b) => {\n      return (\n        new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()\n      )\n    })\n  }\n\n  return sortedProducts\n}\n"
  },
  {
    "path": "storefront/src/lib/webmcp/WebMCPProvider.tsx",
    "content": "\"use client\"\n\nimport React from \"react\"\nimport { registerWebMCPTools } from \"./register-tools\"\nimport { useRouter } from \"next/navigation\"\n\nexport const WebMCPProvider = () => {\n  const router = useRouter()\n\n  React.useEffect(() => {\n    const cleanup = registerWebMCPTools(router)\n    return cleanup\n  }, [router])\n\n  return null\n}\n"
  },
  {
    "path": "storefront/src/lib/webmcp/is-supported.ts",
    "content": "export const isWebMCPSupported = (): boolean => {\n  if (typeof window === \"undefined\") return false\n\n  const enabled = process.env.NEXT_PUBLIC_ENABLE_WEBMCP === \"true\"\n\n  if (!enabled) return false\n\n  if (!window.isSecureContext) return false\n\n  const nav = navigator as Navigator & {\n    modelContext?: {\n      registerTool?: (tool: unknown) => void\n    }\n  }\n\n  return (\n    !!nav.modelContext && typeof nav.modelContext.registerTool === \"function\"\n  )\n}\n"
  },
  {
    "path": "storefront/src/lib/webmcp/register-tools.ts",
    "content": "import { AppRouterInstance } from \"next/dist/shared/lib/app-router-context.shared-runtime\"\nimport { isWebMCPSupported } from \"./is-supported\"\nimport {\n  checkoutPrepareTool,\n  navigateToCartTool,\n  navigateToProductTool,\n} from \"./tools/checkout\"\nimport { productsSearchTool } from \"./tools/products-search\"\nimport { cartManageTool } from \"./tools/cart\"\nimport { WebMCPClient } from \"./types\"\nimport { applyPromotionTool, removePromotionTool } from \"./tools/promotion\"\n\ninterface Navigator extends globalThis.Navigator {\n  modelContext: {\n    registerTool: (\n      tool: {\n        name: string\n        description: string\n        inputSchema: object\n        execute: (input: unknown, client: WebMCPClient) => Promise<unknown>\n        annotations?: {\n          readOnlyHint?: boolean\n        }\n      },\n      options?: { signal?: AbortSignal }\n    ) => void\n    unregisterTool?: (name: string) => void\n  }\n}\n\nexport const registerWebMCPTools = (router?: AppRouterInstance) => {\n  if (!isWebMCPSupported()) {\n    console.info(\"WebMCP is not supported, skipping registration\")\n    return () => {}\n  }\n\n  const modelContext = (navigator as unknown as Navigator).modelContext\n  const controller = new AbortController()\n\n  try {\n    type RegisterableWebMCPTool = {\n      name: string\n      description: string\n      inputSchema: object\n      annotations?: {\n        readOnlyHint?: boolean\n      }\n      handler: (\n        input: unknown,\n        context?: {\n          router?: AppRouterInstance\n          client?: WebMCPClient\n        }\n      ) => Promise<unknown>\n    }\n\n    const tools: RegisterableWebMCPTool[] = [\n      productsSearchTool,\n      navigateToProductTool,\n      navigateToCartTool,\n      cartManageTool,\n      applyPromotionTool,\n      removePromotionTool,\n      checkoutPrepareTool,\n    ] as RegisterableWebMCPTool[]\n\n    tools.forEach((tool) => {\n      modelContext.registerTool(\n        {\n          name: tool.name,\n          description: tool.description,\n          inputSchema: tool.inputSchema,\n          annotations: tool.annotations,\n          execute: async (input, client) => {\n            return await tool.handler(input, { router, client })\n          },\n        },\n        { signal: controller.signal }\n      )\n    })\n  } catch (error) {\n    console.error(\"WebMCP registration failed\", error)\n  }\n\n  return () => controller.abort()\n}\n"
  },
  {
    "path": "storefront/src/lib/webmcp/tools/cart.ts",
    "content": "import {\n  addToCart,\n  deleteLineItem,\n  retrieveCart,\n  updateLineItem,\n} from \"@lib/data/cart\"\nimport {\n  CartSnapshot,\n  WebMCPTool,\n  WebMCPToolContext,\n  WebMCPToolResult,\n} from \"../types\"\nimport { mapCartToResult } from \"../utils\"\n\ninterface CartManageInput {\n  action: \"add\" | \"remove\" | \"update\" | \"view\"\n  variant_id?: string\n  quantity?: number\n  line_id?: string\n}\n\nexport const cartManage = async (\n  input: CartManageInput,\n  context?: WebMCPToolContext\n): Promise<WebMCPToolResult<CartSnapshot>> => {\n  const { action, variant_id: variantId, quantity, line_id: lineId } = input\n  const addQuantity = action === \"add\" ? (quantity ?? 1) : quantity\n  const pathNameParts = window.location.pathname.replace(/^\\//, \"\").split(\"/\")\n  const countryCode = pathNameParts[0]\n\n  if (!countryCode) {\n    return {\n      ok: false,\n      error: {\n        code: \"INVALID_COUNTRY_CODE\",\n        message: \"Your country code is invalid.\",\n      },\n    }\n  }\n\n  if (action !== \"view\" && context?.client) {\n    const actionConfirmed = await context.client.requestUserInteraction(() =>\n      Promise.resolve(\n        window.confirm(\n          `Confirm cart action: ${action}${\n            action === \"add\" ? ` ${addQuantity} item(s)` : \"\"\n          }?`\n        )\n      )\n    )\n\n    if (!actionConfirmed) {\n      return {\n        ok: false,\n        error: {\n          code: \"USER_CANCELLED\",\n          message: \"User cancelled cart action confirmation.\",\n        },\n      }\n    }\n  }\n\n  try {\n    switch (action) {\n      case \"add\":\n        if (!variantId) {\n          return {\n            ok: false,\n            error: {\n              code: \"MISSING_VARIANT\",\n              message: \"variant_id is required for add action\",\n            },\n          }\n        }\n\n        await addToCart({ variantId, quantity: addQuantity, countryCode })\n        break\n      case \"update\":\n        if (!lineId) {\n          return {\n            ok: false,\n            error: {\n              code: \"MISSING_LINE_ID\",\n              message: \"line_id is required for update action\",\n            },\n          }\n        }\n        if (quantity === undefined) {\n          return {\n            ok: false,\n            error: {\n              code: \"MISSING_QUANTITY\",\n              message: \"quantity is required for update action\",\n            },\n          }\n        }\n\n        await updateLineItem({ lineId, quantity })\n        break\n      case \"remove\":\n        if (!lineId) {\n          return {\n            ok: false,\n            error: {\n              code: \"MISSING_LINE_ID\",\n              message: \"line_id is required for remove action\",\n            },\n          }\n        }\n\n        await deleteLineItem(lineId)\n        break\n      case \"view\":\n        break\n    }\n\n    const cart = await retrieveCart()\n\n    if (!cart) {\n      return {\n        ok: false,\n        error: {\n          code: \"CART_MISSING\",\n          message: \"Cart is missing\",\n        },\n      }\n    }\n\n    return {\n      ok: true,\n      data: mapCartToResult(cart),\n      meta: {\n        tool: \"cart.manage\",\n      },\n    }\n  } catch (error: unknown) {\n    const message =\n      error instanceof Error ? error.message : \"Failed to perform cart action\"\n\n    return {\n      ok: false,\n      error: {\n        code: \"CART_OPERATION_FAILED\",\n        message,\n      },\n    }\n  }\n}\n\nexport const cartManageTool: WebMCPTool<CartManageInput, CartSnapshot> = {\n  name: \"cart.manage\",\n  description: \"Manage shopping cart (add, remove, update, view)\",\n  inputSchema: {\n    type: \"object\",\n    properties: {\n      action: {\n        type: \"string\",\n        enum: [\"add\", \"remove\", \"update\", \"view\"],\n        description: \"Action to perform\",\n      },\n      variant_id: {\n        type: \"string\",\n        description: \"Variant ID (required for add)\",\n      },\n      quantity: {\n        type: \"number\",\n        description:\n          \"Quantity for the action. Optional for add (defaults to 1), required for update.\",\n      },\n      line_id: {\n        type: \"string\",\n        description: \"Line item ID (required for remove/update)\",\n      },\n    },\n    required: [\"action\"],\n    oneOf: [\n      {\n        properties: {\n          action: { const: \"add\" },\n          variant_id: { type: \"string\" },\n        },\n        required: [\"action\", \"variant_id\"],\n      },\n      {\n        properties: {\n          action: { const: \"remove\" },\n          line_id: { type: \"string\" },\n        },\n        required: [\"action\", \"line_id\"],\n      },\n      {\n        properties: {\n          action: { const: \"update\" },\n          line_id: { type: \"string\" },\n        },\n        required: [\"action\", \"line_id\"],\n      },\n      {\n        properties: {\n          action: { const: \"view\" },\n        },\n        required: [\"action\"],\n      },\n    ],\n    additionalProperties: false,\n  },\n  handler: cartManage,\n}\n"
  },
  {
    "path": "storefront/src/lib/webmcp/tools/checkout.ts",
    "content": "import { retrieveCart } from \"@lib/data/cart\"\nimport { WebMCPTool, WebMCPToolResult } from \"../types\"\n\nexport interface NavigateToProductInput {\n  handle: string\n  options?: Record<string, string>\n}\n\ntype NavigateToResult = {\n  path: string\n}\n\nconst normalizeOptionKey = (key: string) =>\n  key.trim().toLowerCase().replace(/\\s+/g, \"_\")\n\nexport const navigateToProduct = async (\n  input: NavigateToProductInput,\n  context?: {\n    router?: {\n      push: (href: string) => void\n    }\n  }\n): Promise<WebMCPToolResult<NavigateToResult>> => {\n  const normalizedOptionKeys = new Set(\n    Object.keys(input.options ?? {}).map((key) => normalizeOptionKey(key))\n  )\n\n  const hasColor = normalizedOptionKeys.has(\"color\")\n  const hasMaterial = normalizedOptionKeys.has(\"material\")\n\n  if (hasColor && !hasMaterial) {\n    return {\n      ok: false,\n      error: {\n        code: \"MATERIAL_REQUIRED\",\n        message:\n          \"Material must be provided when Color is set. Example: { Material: 'Leather', Color: 'Red' }.\",\n      },\n    }\n  }\n\n  const queryParams = new URLSearchParams()\n\n  Object.entries(input.options ?? {}).forEach(([key, value]) => {\n    if (!key.trim() || !value.trim()) {\n      return\n    }\n\n    const normalizedKey = normalizeOptionKey(key)\n    queryParams.set(`mcp_opt_${normalizedKey}`, value)\n  })\n\n  const path = `/products/${input.handle}${\n    queryParams.toString() ? `?${queryParams.toString()}` : \"\"\n  }`\n\n  try {\n    context?.router?.push(path)\n\n    return {\n      ok: true,\n      data: {\n        path,\n      },\n      meta: {\n        tool: \"navigation.toProduct\",\n      },\n    }\n  } catch (error) {\n    console.error(error)\n    return {\n      ok: false,\n      error: {\n        code: \"NAVIGATION_FAILED\",\n        message: \"Failed to navigate to product\",\n      },\n    }\n  }\n}\n\nexport const navigateToCart = async (\n  _input: Record<string, never>,\n  context?: {\n    router?: {\n      push: (href: string) => void\n    }\n  }\n): Promise<WebMCPToolResult<NavigateToResult>> => {\n  const path = \"/cart\"\n\n  try {\n    context?.router?.push(path)\n\n    return {\n      ok: true,\n      data: {\n        path,\n      },\n      meta: {\n        tool: \"navigation.toCart\",\n      },\n    }\n  } catch (error) {\n    console.error(error)\n    return {\n      ok: false,\n      error: {\n        code: \"NAVIGATION_FAILED\",\n        message: \"Failed to navigate to cart\",\n      },\n    }\n  }\n}\n\nexport const checkoutPrepare = async (\n  _input: Record<string, never>,\n  context?: {\n    router?: {\n      push: (href: string) => void\n    }\n  }\n): Promise<WebMCPToolResult<NavigateToResult>> => {\n  const path = \"/checkout\"\n\n  try {\n    const cart = await retrieveCart()\n\n    if (!cart) {\n      return {\n        ok: false,\n        error: {\n          code: \"CART_MISSING\",\n          message: \"Cart is missing\",\n        },\n      }\n    }\n\n    if (!cart.items || cart.items.length < 1) {\n      return {\n        ok: false,\n        error: {\n          code: \"CART_EMPTY\",\n          message: \"Cart is empty\",\n        },\n      }\n    }\n\n    context?.router?.push(path)\n\n    return {\n      ok: true,\n      data: { path },\n      meta: { tool: \"checkout.prepare\" },\n    }\n  } catch (error: unknown) {\n    const message =\n      error instanceof Error ? error.message : \"Failed to prepare checkout\"\n\n    return {\n      ok: false,\n      error: {\n        code: \"CHECKOUT_PREPARE_FAILED\",\n        message,\n      },\n    }\n  }\n}\n\nexport const navigateToProductTool: WebMCPTool<\n  NavigateToProductInput,\n  NavigateToResult\n> = {\n  name: \"navigation.toProduct\",\n  description:\n    \"Navigate to product detail page, optionally preselect options. For products with Material and Color, Material must be set before Color.\",\n  annotations: {\n    readOnlyHint: false,\n  },\n  inputSchema: {\n    type: \"object\",\n    properties: {\n      handle: { type: \"string\", description: \"Product handle/slug\" },\n      options: {\n        type: \"object\",\n        description:\n          \"Optional option map like { Material: 'Cotton', Size: 'M' }. If providing Color, also provide Material.\",\n        additionalProperties: { type: \"string\" },\n      },\n    },\n    required: [\"handle\"],\n    additionalProperties: false,\n  },\n  handler: navigateToProduct,\n}\n\nexport const navigateToCartTool: WebMCPTool<\n  Record<string, never>,\n  NavigateToResult\n> = {\n  name: \"navigation.toCart\",\n  description: \"Navigate to shopping cart page\",\n  annotations: {\n    readOnlyHint: false,\n  },\n  inputSchema: {\n    type: \"object\",\n    properties: {},\n    additionalProperties: false,\n  },\n  handler: navigateToCart,\n}\n\nexport const checkoutPrepareTool: WebMCPTool<\n  Record<string, never>,\n  NavigateToResult\n> = {\n  name: \"checkout.prepare\",\n  description:\n    \"Validate that the shopping cart has items and navigate to the checkout page. Performs pre-checkout validation (checks cart exists and is not empty) before proceeding. Returns error if cart is missing or empty.\",\n  annotations: {\n    readOnlyHint: false,\n  },\n  inputSchema: {\n    type: \"object\",\n    properties: {},\n    additionalProperties: false,\n  },\n  handler: checkoutPrepare,\n}\n"
  },
  {
    "path": "storefront/src/lib/webmcp/tools/products-search.ts",
    "content": "import { getProductsListWithSort } from \"@lib/data/products\"\nimport { MeiliSearchProductHit, searchClient } from \"@lib/search-client\"\nimport { getProductPrice } from \"@lib/util/get-product-price\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport { WebMCPTool, WebMCPToolResult } from \"../types\"\n\nexport interface ProductSearchInput {\n  query?: string\n  collection_ids?: string[]\n  category_ids?: string[]\n  type_ids?: string[]\n  sort?: \"latest_arrivals\" | \"lowest_price\" | \"highest_price\"\n  limit?: number\n}\n\ninterface ProductSearchData {\n  products: Array<{\n    id: string\n    title: string\n    handle: string\n    thumbnail?: string\n    price?: { amount: number; currency_code: string }\n    collection_ids?: string[]\n    category_ids?: string[]\n    type_id?: string\n    tags?: string[]\n    variants?: Array<{\n      id: string\n      title?: string\n      inventory_quantity?: number | null\n    }>\n    options?: Array<{\n      id: string\n      title: string\n      values?: Array<{\n        id: string\n        value: string\n      }>\n    }>\n  }>\n}\n\nexport const productsSearch = async (\n  params: ProductSearchInput\n): Promise<WebMCPToolResult<ProductSearchData>> => {\n  const pathNameParts = window.location.pathname.replace(/^\\//, \"\").split(\"/\")\n  const countryCode = pathNameParts[0]\n\n  if (!countryCode) {\n    return {\n      ok: false,\n      error: {\n        code: \"INVALID_COUNTRY_CODE\",\n        message: \"Your country code is invalid.\",\n      },\n    }\n  }\n\n  try {\n    const results = params.query\n      ? await searchClient\n          .index(\"products\")\n          .search<MeiliSearchProductHit>(params.query)\n      : null\n\n    const queryParams: HttpTypes.StoreProductListParams = {\n      limit: Math.min(36, params.limit || 12),\n    }\n\n    if (params.collection_ids && params.collection_ids.length) {\n      queryParams[\"collection_id\"] = params.collection_ids\n    }\n\n    if (params.category_ids && params.category_ids.length) {\n      queryParams[\"category_id\"] = params.category_ids\n    }\n\n    if (params.type_ids) {\n      queryParams[\"type_id\"] = params.type_ids\n    }\n\n    if (results) {\n      queryParams[\"id\"] = results.hits.map((h) => h.id)\n    }\n\n    if (params.sort === \"latest_arrivals\") {\n      queryParams[\"order\"] = \"created_at\"\n    }\n\n    const medusaProducts = await getProductsListWithSort({\n      countryCode,\n      queryParams,\n      sortBy:\n        params.sort === \"highest_price\"\n          ? \"price_desc\"\n          : params.sort === \"lowest_price\"\n            ? \"price_asc\"\n            : \"created_at\",\n    })\n\n    return {\n      ok: true,\n      data: {\n        products: medusaProducts.response.products.map((product) => {\n          const { cheapestPrice } = getProductPrice({\n            product,\n          })\n\n          return {\n            id: product.id,\n            title: product.title,\n            handle: product.handle,\n            thumbnail: product.thumbnail ?? undefined,\n            price: cheapestPrice\n              ? {\n                  amount: cheapestPrice.calculated_price_number,\n                  currency_code: cheapestPrice.currency_code!,\n                }\n              : undefined,\n            variants: product.variants?.map((variant) => ({\n              id: variant.id,\n              title: variant.title ?? undefined,\n              inventory_quantity: variant.inventory_quantity,\n            })),\n            options: product.options?.map((option) => ({\n              id: option.id,\n              title: option.title,\n              values: option.values?.map((valopt) => ({\n                id: valopt.id,\n                value: valopt.value,\n              })),\n            })),\n            category_ids:\n              product.categories?.map((category) => category.id) ?? [],\n            collection_ids: product.collection ? [product.collection.id] : [],\n            tags: product.tags?.map((tag) => tag.value) ?? [],\n            type_id: product.type_id ?? undefined,\n          }\n        }),\n      },\n      meta: {\n        tool: \"products.search\",\n      },\n    }\n  } catch (error: unknown) {\n    const message =\n      error instanceof Error ? error.message : \"Failed to search products\"\n\n    return {\n      ok: false,\n      error: {\n        code: \"SEARCH_FAILED\",\n        message,\n      },\n    }\n  }\n}\n\nexport const productsSearchTool: WebMCPTool<\n  ProductSearchInput,\n  ProductSearchData\n> = {\n  name: \"products.search\",\n  description:\n    \"Search and retrieve product information (price, variants, options, categories, tags) with optional filters and sorting.\",\n  annotations: {\n    readOnlyHint: true,\n  },\n  inputSchema: {\n    type: \"object\",\n    properties: {\n      query: {\n        type: \"string\",\n        description:\n          \"Search query. Can be omitted to fetch products by filters only.\",\n      },\n      collection_ids: { type: \"array\", items: { type: \"string\" } },\n      category_ids: { type: \"array\", items: { type: \"string\" } },\n      type_ids: { type: \"array\", items: { type: \"string\" } },\n      sort: {\n        type: \"string\",\n        enum: [\"latest_arrivals\", \"lowest_price\", \"highest_price\"],\n      },\n      limit: { type: \"number\", minimum: 1, maximum: 36 },\n    },\n    additionalProperties: false,\n  },\n  handler: productsSearch,\n}\n"
  },
  {
    "path": "storefront/src/lib/webmcp/tools/promotion.ts",
    "content": "import { applyPromotions, removePromotions, retrieveCart } from \"@lib/data/cart\"\nimport { CartSnapshot, WebMCPTool, WebMCPToolResult } from \"../types\"\nimport { mapCartToResult } from \"../utils\"\n\ninterface PromotionInput {\n  code: string\n}\n\nexport const cartApplyPromotion = async (\n  input: PromotionInput\n): Promise<WebMCPToolResult<CartSnapshot>> => {\n  if (!input.code) {\n    return {\n      ok: false,\n      error: {\n        code: \"MISSING_CODE\",\n        message: \"Promotion code is required\",\n      },\n    }\n  }\n\n  try {\n    await applyPromotions([input.code])\n\n    const cart = await retrieveCart()\n\n    if (!cart) {\n      return {\n        ok: false,\n        error: {\n          code: \"CART_MISSING\",\n          message: \"No active cart found\",\n        },\n      }\n    }\n\n    return {\n      ok: true,\n      data: mapCartToResult(cart),\n      meta: {\n        tool: \"cart.applyPromotion\",\n      },\n    }\n  } catch (error) {\n    console.error(\"[cartApplyPromotion] Error:\", error)\n    return {\n      ok: false,\n      error: {\n        code: \"APPLY_FAILED\",\n        message: \"Failed to apply promotion code\",\n      },\n    }\n  }\n}\n\nexport const cartRemovePromotion = async (\n  input: PromotionInput\n): Promise<WebMCPToolResult<CartSnapshot>> => {\n  if (!input.code) {\n    return {\n      ok: false,\n      error: {\n        code: \"MISSING_CODE\",\n        message: \"Promotion code is required\",\n      },\n    }\n  }\n\n  try {\n    await removePromotions([input.code])\n\n    const cart = await retrieveCart()\n\n    if (!cart) {\n      return {\n        ok: false,\n        error: {\n          code: \"CART_MISSING\",\n          message: \"No active cart found\",\n        },\n      }\n    }\n\n    return {\n      ok: true,\n      data: mapCartToResult(cart),\n      meta: {\n        tool: \"cart.removePromotion\",\n      },\n    }\n  } catch (error) {\n    console.error(\"[cartRemovePromotion] Error:\", error)\n    return {\n      ok: false,\n      error: {\n        code: \"REMOVE_FAILED\",\n        message: \"Failed to remove promotion code\",\n      },\n    }\n  }\n}\n\nexport const applyPromotionTool: WebMCPTool<PromotionInput, CartSnapshot> = {\n  name: \"cart.applyPromotion\",\n  description:\n    \"Apply a discount/promotion code to the shopping cart. Returns updated cart with applied discount, including new subtotal, total, and discount amount. Common error codes: MISSING_CODE (promotion code is required), CART_MISSING (no active cart found), APPLY_FAILED (failed to apply promotion code).\",\n  annotations: {\n    readOnlyHint: false,\n  },\n  inputSchema: {\n    type: \"object\",\n    properties: {\n      code: {\n        type: \"string\",\n        description:\n          \"Promotion/discount code to apply (e.g., 'SUMMER25', 'FREESHIP')\",\n      },\n    },\n    additionalProperties: false,\n    required: [\"code\"],\n  },\n  handler: cartApplyPromotion,\n}\n\nexport const removePromotionTool: WebMCPTool<PromotionInput, CartSnapshot> = {\n  name: \"cart.removePromotion\",\n  description:\n    \"Remove a previously applied discount/promotion code from the shopping cart. Returns updated cart with recalculated totals after discount removal. Use this when the user wants to replace a code or remove an applied discount.\",\n  annotations: {\n    readOnlyHint: false,\n  },\n  inputSchema: {\n    type: \"object\",\n    properties: {\n      code: {\n        type: \"string\",\n        description:\n          \"Promotion/discount code to remove (must match an applied code)\",\n      },\n    },\n    additionalProperties: false,\n    required: [\"code\"],\n  },\n  handler: cartRemovePromotion,\n}\n"
  },
  {
    "path": "storefront/src/lib/webmcp/types.ts",
    "content": "import { AppRouterInstance } from \"next/dist/shared/lib/app-router-context.shared-runtime\"\n\nexport interface WebMCPClient {\n  requestUserInteraction: <T>(callback: () => Promise<T> | T) => Promise<T>\n}\n\nexport interface WebMCPToolContext {\n  router?: AppRouterInstance\n  client?: WebMCPClient\n}\n\nexport type WebMCPToolResult<TData> =\n  | {\n      ok: true\n      data: TData\n      meta: {\n        tool: string\n      }\n    }\n  | {\n      ok: false\n      error: {\n        code: string\n        message: string\n      }\n    }\n\nexport interface WebMCPTool<TInput, TData> {\n  name: string\n  description: string\n  inputSchema: Record<string, unknown>\n  annotations?: {\n    readOnlyHint?: boolean\n  }\n  handler: (\n    input: TInput,\n    context?: WebMCPToolContext\n  ) => Promise<WebMCPToolResult<TData>>\n}\n\nexport interface CartSnapshot {\n  cart: {\n    id: string\n    currency_code: string\n    subtotal: number\n    total: number\n    discount_total?: number\n    items: Array<{\n      id: string\n      title: string\n      variant_id: string\n      quantity: number\n      unit_price: number\n      total: number\n    }>\n    discount_codes?: string[]\n  }\n}\n"
  },
  {
    "path": "storefront/src/lib/webmcp/utils.ts",
    "content": "import {\n  StoreCart,\n  StoreCartLineItem,\n  StoreCartPromotion,\n} from \"@medusajs/types\"\n\nexport const mapCartToResult = (cart: StoreCart) => {\n  return {\n    cart: {\n      id: cart.id,\n      currency_code: cart.currency_code,\n      subtotal: cart.subtotal ?? 0,\n      total: cart.total ?? 0,\n      discount_total: cart.discount_total,\n      items:\n        cart.items?.map((item: StoreCartLineItem) => ({\n          id: item.id,\n          title: item.title,\n          variant_id: item.variant_id ?? \"\",\n          quantity: item.quantity,\n          unit_price: item.unit_price,\n          total: item.total ?? 0,\n        })) || [],\n      discount_codes:\n        cart.promotions\n          ?.map((p: StoreCartPromotion) => p.code)\n          .filter((code): code is string => code !== undefined) || [],\n    },\n  }\n}\n"
  },
  {
    "path": "storefront/src/middleware.ts",
    "content": "import { HttpTypes } from \"@medusajs/types\"\nimport { notFound } from \"next/navigation\"\nimport { NextRequest, NextResponse } from \"next/server\"\n\nconst BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL\nconst PUBLISHABLE_API_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY\nconst DEFAULT_REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || \"us\"\n\nconst regionMapCache = {\n  regionMap: new Map<string, HttpTypes.StoreRegion>(),\n  regionMapUpdated: Date.now(),\n}\n\nasync function getRegionMap() {\n  const { regionMap, regionMapUpdated } = regionMapCache\n\n  if (\n    !regionMap.keys().next().value ||\n    regionMapUpdated < Date.now() - 3600 * 1000\n  ) {\n    // Fetch regions from Medusa. We can't use the JS client here because middleware is running on Edge and the client needs a Node environment.\n    const { regions } = await fetch(`${BACKEND_URL}/store/regions`, {\n      headers: {\n        \"x-publishable-api-key\": PUBLISHABLE_API_KEY!,\n      },\n      next: {\n        revalidate: 3600,\n        tags: [\"regions\"],\n      },\n    }).then((res) => res.json())\n\n    if (!regions?.length) {\n      notFound()\n    }\n\n    // Create a map of country codes to regions.\n    regions.forEach((region: HttpTypes.StoreRegion) => {\n      region.countries?.forEach((c) => {\n        regionMapCache.regionMap.set(c.iso_2 ?? \"\", region)\n      })\n    })\n\n    regionMapCache.regionMapUpdated = Date.now()\n  }\n\n  return regionMapCache.regionMap\n}\n\n/**\n * Fetches regions from Medusa and sets the region cookie.\n * @param request\n * @param response\n */\nasync function getCountryCode(\n  request: NextRequest,\n  regionMap: Map<string, HttpTypes.StoreRegion | number>\n) {\n  try {\n    let countryCode\n\n    const vercelCountryCode = request.headers\n      .get(\"x-vercel-ip-country\")\n      ?.toLowerCase()\n\n    const urlCountryCode = request.nextUrl.pathname.split(\"/\")[1]?.toLowerCase()\n\n    if (urlCountryCode && regionMap.has(urlCountryCode)) {\n      countryCode = urlCountryCode\n    } else if (vercelCountryCode && regionMap.has(vercelCountryCode)) {\n      countryCode = vercelCountryCode\n    } else if (regionMap.has(DEFAULT_REGION)) {\n      countryCode = DEFAULT_REGION\n    } else if (regionMap.keys().next().value) {\n      countryCode = regionMap.keys().next().value\n    }\n\n    return countryCode\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  } catch (error) {\n    if (process.env.NODE_ENV === \"development\") {\n      console.error(\n        \"Middleware.ts: Error getting the country code. Did you set up regions in your Medusa Admin and define a NEXT_PUBLIC_MEDUSA_BACKEND_URL environment variable?\"\n      )\n    }\n  }\n}\n\n/**\n * Middleware to handle region selection and onboarding status.\n */\nexport async function middleware(request: NextRequest) {\n  const regionMap = await getRegionMap()\n  const countryCode = regionMap && (await getCountryCode(request, regionMap))\n\n  const urlHasCountryCode =\n    countryCode && request.nextUrl.pathname.split(\"/\")[1].includes(countryCode)\n\n  // check if one of the country codes is in the url\n  if (urlHasCountryCode) {\n    return NextResponse.next()\n  }\n\n  const redirectPath =\n    request.nextUrl.pathname === \"/\" ? \"\" : request.nextUrl.pathname\n\n  const queryString = request.nextUrl.search ? request.nextUrl.search : \"\"\n\n  let redirectUrl = request.nextUrl.href\n\n  let response = NextResponse.redirect(redirectUrl, 307)\n\n  // If no country code is set, we redirect to the relevant region.\n  if (!urlHasCountryCode && countryCode) {\n    redirectUrl = `${request.nextUrl.origin}/${countryCode}${redirectPath}${queryString}`\n    response = NextResponse.redirect(`${redirectUrl}`, 307)\n  }\n\n  return response\n}\n\nexport const config = {\n  matcher: [\n    \"/((?!api|_next/static|favicon.ico|_next/image|images|robots.txt).*)\",\n  ],\n}\n"
  },
  {
    "path": "storefront/src/modules/account/components/AddressMultiple.tsx",
    "content": "import { twMerge } from \"tailwind-merge\"\nimport { BaseRegionCountry } from \"@medusajs/types/dist/http/region/common\"\nimport { StoreCustomerAddress, StoreRegion } from \"@medusajs/types\"\nimport { UiCloseButton, UiDialog, UiDialogTrigger } from \"@/components/Dialog\"\nimport { UiModal, UiModalOverlay } from \"@/components/ui/Modal\"\nimport { DeleteAddressButton } from \"@modules/account/components/DeleteAddressButton\"\nimport { Button } from \"@/components/Button\"\nimport { UpsertAddressForm } from \"@modules/account/components/UpsertAddressForm\"\n\nexport const AddressMultiple: React.FC<{\n  address: StoreCustomerAddress\n  countries: BaseRegionCountry[]\n  region: StoreRegion | null | undefined\n  className?: string\n}> = ({ address, countries, region, className }) => {\n  return (\n    <div\n      className={twMerge(\n        \"border border-grayscale-200 rounded-xs py-4 px-6 flex flex-col gap-8 break-all\",\n        className\n      )}\n    >\n      <div className=\"flex flex-wrap justify-between gap-8\">\n        <div>\n          <p className=\"text-xs text-grayscale-500 mb-1.5\">Address</p>\n          <p>{address.address_1}</p>\n        </div>\n        <div>\n          <p className=\"text-xs text-grayscale-500 mb-1.5\">Country</p>\n          <p>\n            {countries.find((country) => country.iso_2 === address.country_code)\n              ?.display_name || address.country_code}\n          </p>\n        </div>\n      </div>\n      {Boolean(address.address_2) && (\n        <div>\n          <p className=\"text-xs text-grayscale-500 mb-1.5\">\n            Apartment, suite, etc.\n          </p>\n          <p>{address.address_2}</p>\n        </div>\n      )}\n      <div className=\"flex flex-wrap justify-between gap-8\">\n        <div>\n          <p className=\"text-xs text-grayscale-500 mb-1.5\">Postal Code</p>\n          <p>{address.postal_code}</p>\n        </div>\n        <div>\n          <p className=\"text-xs text-grayscale-500 mb-1.5\">City</p>\n          <p>{address.city}</p>\n        </div>\n      </div>\n      <div className=\"flex gap-4 mt-auto\">\n        <UiDialogTrigger>\n          <Button\n            iconName=\"trash\"\n            size=\"sm\"\n            variant=\"outline\"\n            className=\"w-8 px-0 shrink-0\"\n            aria-label=\"Delete address\"\n          />\n          <UiModalOverlay>\n            <UiModal>\n              <UiDialog className=\"text-center\">\n                <p className=\"text-md mb-8\">\n                  Do you want to delete this address?\n                </p>\n                <div className=\"flex gap-6 justify-center\">\n                  <DeleteAddressButton addressId={address.id}>\n                    Confirm\n                  </DeleteAddressButton>\n                  <UiCloseButton variant=\"outline\">Cancel</UiCloseButton>\n                </div>\n              </UiDialog>\n            </UiModal>\n          </UiModalOverlay>\n        </UiDialogTrigger>\n        <UiDialogTrigger>\n          <Button variant=\"outline\" size=\"sm\" className=\"shrink-0\">\n            Change\n          </Button>\n          <UiModalOverlay>\n            <UiModal>\n              <UiDialog>\n                <UpsertAddressForm\n                  region={region ?? undefined}\n                  addressId={address.id}\n                  defaultValues={{\n                    first_name: address.first_name ?? \"\",\n                    last_name: address.last_name ?? \"\",\n                    company: address.company ?? \"\",\n                    phone: address.phone ?? \"\",\n                    address_1: address.address_1 ?? \"\",\n                    address_2: address.address_2 ?? \"\",\n                    postal_code: address.postal_code ?? \"\",\n                    city: address.city ?? \"\",\n                    province: address.province ?? \"\",\n                    country_code: address.country_code ?? \"\",\n                  }}\n                />\n              </UiDialog>\n            </UiModal>\n          </UiModalOverlay>\n        </UiDialogTrigger>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/account/components/AddressSingle.tsx",
    "content": "import { twMerge } from \"tailwind-merge\"\nimport { StoreCustomerAddress, StoreRegion } from \"@medusajs/types\"\nimport { BaseRegionCountry } from \"@medusajs/types/dist/http/region/common\"\nimport { Button } from \"@/components/Button\"\nimport { UiCloseButton, UiDialog, UiDialogTrigger } from \"@/components/Dialog\"\nimport { Icon } from \"@/components/Icon\"\nimport { UiModal, UiModalOverlay } from \"@/components/ui/Modal\"\nimport { DeleteAddressButton } from \"@modules/account/components/DeleteAddressButton\"\nimport { UpsertAddressForm } from \"@modules/account/components/UpsertAddressForm\"\n\nexport const AddressSingle: React.FC<{\n  address: StoreCustomerAddress\n  countries: BaseRegionCountry[]\n  region: StoreRegion | null | undefined\n  className?: string\n}> = ({ address, countries, region, className }) => {\n  return (\n    <div\n      className={twMerge(\n        \"w-full border border-grayscale-200 rounded-xs p-4 flex flex-wrap gap-8 max-lg:flex-col\",\n        className\n      )}\n    >\n      <div className=\"flex flex-1 sm:gap-3\">\n        <Icon\n          name=\"user\"\n          className=\"w-6 h-6\"\n          wrapperClassName=\"max-sm:hidden\"\n        />\n        <div className=\"flex flex-col gap-8 flex-1\">\n          <div className=\"flex flex-wrap justify-between gap-6\">\n            <div className=\"sm:grow sm:basis-0\">\n              <p className=\"text-xs text-grayscale-500 mb-1.5\">Address</p>\n              <p>{address.address_1}</p>\n            </div>\n            <div className=\"sm:grow sm:basis-0\">\n              <p className=\"text-xs text-grayscale-500 mb-1.5\">Country</p>\n              <p>\n                {countries.find(\n                  (country) => country.iso_2 === address.country_code\n                )?.display_name || address.country_code}\n              </p>\n            </div>\n          </div>\n          {Boolean(address.address_2) && (\n            <div>\n              <p className=\"text-xs text-grayscale-500 mb-1.5\">\n                Apartment, suite, etc.\n              </p>\n              <p>{address.address_2}</p>\n            </div>\n          )}\n          <div className=\"flex flex-wrap justify-between gap-6\">\n            <div className=\"sm:grow sm:basis-0\">\n              <p className=\"text-xs text-grayscale-500 mb-1.5\">Postal Code</p>\n              <p>{address.postal_code}</p>\n            </div>\n            <div className=\"sm:grow sm:basis-0\">\n              <p className=\"text-xs text-grayscale-500 mb-1.5\">City</p>\n              <p>{address.city}</p>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex gap-2.5\">\n        <UiDialogTrigger>\n          <Button\n            iconName=\"trash\"\n            size=\"sm\"\n            variant=\"outline\"\n            className=\"w-8 px-0 shrink-0\"\n            aria-label=\"Delete address\"\n          />\n          <UiModalOverlay>\n            <UiModal>\n              <UiDialog className=\"text-center\">\n                <p className=\"text-md mb-8\">\n                  Do you want to delete this address?\n                </p>\n                <div className=\"flex gap-6 justify-center\">\n                  <DeleteAddressButton\n                    addressId={address.id}\n                    className=\"max-w-42 w-full\"\n                  >\n                    Confirm\n                  </DeleteAddressButton>\n                  <UiCloseButton variant=\"outline\" className=\"max-w-42 w-full\">\n                    Cancel\n                  </UiCloseButton>\n                </div>\n              </UiDialog>\n            </UiModal>\n          </UiModalOverlay>\n        </UiDialogTrigger>\n        <UiDialogTrigger>\n          <Button variant=\"outline\" size=\"sm\" className=\"shrink-0\">\n            Change\n          </Button>\n          <UiModalOverlay>\n            <UiModal>\n              <UiDialog>\n                <UpsertAddressForm\n                  region={region ?? undefined}\n                  addressId={address.id}\n                  defaultValues={{\n                    first_name: address.first_name ?? \"\",\n                    last_name: address.last_name ?? \"\",\n                    company: address.company ?? \"\",\n                    phone: address.phone ?? \"\",\n                    address_1: address.address_1 ?? \"\",\n                    address_2: address.address_2 ?? \"\",\n                    postal_code: address.postal_code ?? \"\",\n                    city: address.city ?? \"\",\n                    province: address.province ?? \"\",\n                    country_code: address.country_code ?? \"\",\n                  }}\n                />\n              </UiDialog>\n            </UiModal>\n          </UiModalOverlay>\n        </UiDialogTrigger>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/account/components/DefaultBillingAddressSelect.tsx",
    "content": "\"use client\"\nimport * as React from \"react\"\nimport * as ReactAria from \"react-aria-components\"\nimport { StoreCustomerAddress } from \"@medusajs/types\"\nimport { BaseRegionCountry } from \"@medusajs/types/dist/http/region/common\"\nimport { updateDefaultBillingAddress } from \"@lib/data/customer\"\nimport {\n  UiSelectIcon,\n  UiSelectListBox,\n  UiSelectListBoxItem,\n  UiSelectButton,\n  UiSelectValue,\n} from \"@/components/ui/Select\"\n\nexport const DefaultBillingAddressSelect: React.FC<{\n  addresses: StoreCustomerAddress[]\n  countries: BaseRegionCountry[]\n}> = ({ addresses, countries }) => {\n  const handleAddressSelect = async (value: string) => {\n    await updateDefaultBillingAddress(value)\n  }\n\n  return (\n    <>\n      <p className=\"text-xs text-grayscale-500 mb-1.5\">\n        Default billing address\n      </p>\n      <ReactAria.Select\n        aria-label=\"Select default shipping address\"\n        defaultSelectedKey={addresses.find((i) => i.is_default_billing)?.id}\n        placeholder=\"Select default shipping address\"\n        className=\"mb-8\"\n        onSelectionChange={(key) => {\n          if (typeof key === \"string\") {\n            handleAddressSelect(key)\n          }\n        }}\n      >\n        <UiSelectButton className=\"!h-14\">\n          <UiSelectValue className=\"text-base\" />\n          <UiSelectIcon />\n        </UiSelectButton>\n        <ReactAria.Popover className=\"w-[--trigger-width]\">\n          <UiSelectListBox>\n            {addresses?.map((address) => (\n              <UiSelectListBoxItem key={address.id} id={address.id}>\n                {[\n                  address.address_1,\n                  address.address_2,\n                  [address.postal_code, address.city].filter(Boolean).join(\" \"),\n                  countries.find(({ iso_2 }) => iso_2 === address.country_code)\n                    ?.display_name,\n                ]\n                  .filter(Boolean)\n                  .join(\", \")}\n              </UiSelectListBoxItem>\n            ))}\n          </UiSelectListBox>\n        </ReactAria.Popover>\n      </ReactAria.Select>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/account/components/DefaultShippingAddressSelect.tsx",
    "content": "\"use client\"\nimport * as React from \"react\"\nimport * as ReactAria from \"react-aria-components\"\nimport { StoreCustomerAddress } from \"@medusajs/types\"\nimport { BaseRegionCountry } from \"@medusajs/types/dist/http/region/common\"\nimport { updateDefaultShippingAddress } from \"@lib/data/customer\"\nimport {\n  UiSelectButton,\n  UiSelectValue,\n  UiSelectIcon,\n  UiSelectListBox,\n  UiSelectListBoxItem,\n} from \"@/components/ui/Select\"\n\nexport const DefaultShippingAddressSelect: React.FC<{\n  addresses: StoreCustomerAddress[]\n  countries: BaseRegionCountry[]\n}> = ({ addresses, countries }) => {\n  const handleAddressSelect = async (value: string) => {\n    await updateDefaultShippingAddress(value)\n  }\n\n  return (\n    <>\n      <p className=\"text-xs text-grayscale-500 mb-1.5\">\n        Default shipping address\n      </p>\n      <ReactAria.Select\n        aria-label=\"Select default shipping address\"\n        defaultSelectedKey={addresses.find((i) => i.is_default_shipping)?.id}\n        placeholder=\"Select default shipping address\"\n        className=\"mb-8\"\n        onSelectionChange={(key) => {\n          if (typeof key === \"string\") {\n            handleAddressSelect(key)\n          }\n        }}\n      >\n        <UiSelectButton className=\"!h-14\">\n          <UiSelectValue className=\"text-base\" />\n          <UiSelectIcon />\n        </UiSelectButton>\n        <ReactAria.Popover className=\"w-[--trigger-width]\">\n          <UiSelectListBox>\n            {addresses?.map((address) => (\n              <UiSelectListBoxItem key={address.id} id={address.id}>\n                {[\n                  address.address_1,\n                  address.address_2,\n                  [address.postal_code, address.city].filter(Boolean).join(\" \"),\n                  countries.find(({ iso_2 }) => iso_2 === address.country_code)\n                    ?.display_name,\n                ]\n                  .filter(Boolean)\n                  .join(\", \")}\n              </UiSelectListBoxItem>\n            ))}\n          </UiSelectListBox>\n        </ReactAria.Popover>\n      </ReactAria.Select>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/account/components/DeleteAddressButton.tsx",
    "content": "\"use client\"\n\nimport { UiConfirmButton } from \"@/components/Dialog\"\nimport { useDeleteCustomerAddress } from \"hooks/customer\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\nexport const DeleteAddressButton = withReactQueryProvider<{\n  addressId: string\n  className?: string\n  children: React.ReactNode\n}>(({ addressId, children, ...rest }) => {\n  const { mutateAsync, isPending } = useDeleteCustomerAddress()\n\n  return (\n    <UiConfirmButton\n      {...rest}\n      onConfirm={async () => {\n        await mutateAsync(addressId).catch((error) => {\n          console.error(error)\n        })\n      }}\n      isLoading={isPending}\n    >\n      {children}\n    </UiConfirmButton>\n  )\n})\n"
  },
  {
    "path": "storefront/src/modules/account/components/PersonalInfoForm.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ReactAria from \"react-aria-components\"\nimport { z } from \"zod\"\n\nimport { UiCloseButton } from \"@/components/Dialog\"\nimport { Form, InputField } from \"@/components/Forms\"\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport { updateCustomerFormSchema, useUpdateCustomer } from \"hooks/customer\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\nexport const PersonalInfoForm = withReactQueryProvider<{\n  defaultValues?: {\n    first_name: string\n    last_name: string\n    phone?: string\n  }\n}>(({ defaultValues }) => {\n  const { mutate, isPending, data } = useUpdateCustomer()\n\n  const { close } = React.useContext(ReactAria.OverlayTriggerStateContext)!\n  const onSubmit = (values: z.infer<typeof updateCustomerFormSchema>) => {\n    mutate(values, {\n      onSuccess: (res) => {\n        if (res.state === \"success\") {\n          close()\n        }\n      },\n    })\n  }\n  return (\n    <Form\n      onSubmit={onSubmit}\n      schema={updateCustomerFormSchema}\n      defaultValues={defaultValues}\n    >\n      {({ watch }) => {\n        const formData = watch()\n        const isDisabled =\n          !Object.values(formData).some((value) => value) ||\n          (defaultValues\n            ? !Object.entries(formData).some(\n                ([key, value]) =>\n                  defaultValues[key as keyof typeof defaultValues] !== value\n              )\n            : false)\n        return (\n          <>\n            <p className=\"text-md mb-8 sm:mb-10\">Personal information</p>\n            <div className=\"flex flex-col gap-4 sm:gap-8\">\n              <div className=\"flex max-xs:flex-col gap-y-4 gap-x-6\">\n                <InputField\n                  placeholder=\"First name\"\n                  name=\"first_name\"\n                  className=\" flex-1\"\n                  inputProps={{ autoComplete: \"given-name\" }}\n                />\n                <InputField\n                  placeholder=\"Last name\"\n                  name=\"last_name\"\n                  className=\"flex-1\"\n                  inputProps={{ autoComplete: \"family-name\" }}\n                />\n              </div>\n              <InputField\n                placeholder=\"Phone\"\n                name=\"phone\"\n                className=\"flex-1 mb-8 sm:mb-10\"\n                type=\"tel\"\n                inputProps={{ autoComplete: \"tel\" }}\n              />\n              {data?.state === \"error\" && (\n                <div className=\"text-sm text-red-primary\">{data?.error}</div>\n              )}\n            </div>\n            <div className=\"flex gap-6 justify-between\">\n              <SubmitButton isLoading={isPending} isDisabled={isDisabled}>\n                Save changes\n              </SubmitButton>\n              <UiCloseButton variant=\"outline\">Cancel</UiCloseButton>\n            </div>\n          </>\n        )\n      }}\n    </Form>\n  )\n})\n"
  },
  {
    "path": "storefront/src/modules/account/components/RequestPasswordResetButton.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { Button } from \"@/components/Button\"\nimport { UiCloseButton, UiDialog, UiDialogTrigger } from \"@/components/Dialog\"\nimport { Icon } from \"@/components/Icon\"\nimport { UiModal, UiModalOverlay } from \"@/components/ui/Modal\"\nimport { requestPasswordReset } from \"@lib/data/customer\"\n\nexport const RequestPasswordResetButton: React.FC = () => {\n  const [isModalOpen, setIsModalOpen] = React.useState(false)\n  const [isLoading, setIsLoading] = React.useState(false)\n  const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n\n  return (\n    <>\n      {errorMessage && (\n        <div className=\"text-sm text-red-primary\">{errorMessage}</div>\n      )}\n      <UiDialogTrigger\n        isOpen={isModalOpen}\n        onOpenChange={(isOpen) => {\n          if (!isOpen) {\n            setIsModalOpen(false)\n          }\n        }}\n      >\n        <Button\n          isLoading={isLoading}\n          onPress={async () => {\n            setIsLoading(true)\n            const result = await requestPasswordReset().catch((error) => {\n              console.error(error)\n\n              return { success: false, error: \"Something went wrong\" }\n            })\n\n            if (result.success) {\n              setIsModalOpen(true)\n            } else {\n              setErrorMessage(result.error)\n            }\n\n            setIsLoading(false)\n          }}\n          className=\"max-sm:w-full\"\n        >\n          Reset password\n        </Button>\n        <UiModalOverlay isDismissable={false} className=\"bg-transparent\">\n          <UiModal className=\"relative\">\n            <UiDialog>\n              <p className=\"text-md mb-12\">Reset password</p>\n              <p className=\"text-grayscale-500\">\n                We have sent an email with instructions on how to change the\n                password.\n              </p>\n              <UiCloseButton\n                variant=\"ghost\"\n                className=\"absolute top-4 right-6 p-0\"\n              >\n                <Icon name=\"close\" className=\"w-6 h-6\" />\n              </UiCloseButton>\n            </UiDialog>\n          </UiModal>\n        </UiModalOverlay>\n      </UiDialogTrigger>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/account/components/SidebarNav.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { usePathname } from \"next/navigation\"\nimport { twJoin } from \"tailwind-merge\"\n\nimport { useCountryCode } from \"hooks/country-code\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\n\nexport const SidebarNav: React.FC = () => {\n  const pathName = usePathname()\n  const countryCode = useCountryCode()\n  const currentPath = pathName.split(`/${countryCode}`)[1]\n\n  return (\n    <>\n      <LocalizedLink\n        href=\"/account\"\n        className={twJoin(\n          \"inline-flex items-start py-4 max-md:whitespace-nowrap\",\n          currentPath === \"/account\" && \"font-semibold\"\n        )}\n      >\n        Personal &amp; security\n      </LocalizedLink>\n      <LocalizedLink\n        href=\"/account/my-orders\"\n        className={twJoin(\n          \"inline-flex items-start py-4 max-md:whitespace-nowrap\",\n          currentPath.startsWith(\"/account/my-orders\") && \"font-semibold\"\n        )}\n      >\n        My orders\n      </LocalizedLink>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/account/components/SignOutButton.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport { useCountryCode } from \"hooks/country-code\"\nimport { ButtonProps } from \"@/components/Button\"\nimport { useSignout } from \"hooks/customer\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\nexport const SignOutButton = withReactQueryProvider<Omit<ButtonProps, \"type\">>(\n  (rest) => {\n    const countryCode = useCountryCode()\n    const { mutateAsync, isPending } = useSignout()\n\n    const handleSignout = async () => {\n      if (countryCode) {\n        await mutateAsync(countryCode ?? \"\")\n      }\n    }\n\n    return (\n      <form\n        onSubmit={(e) => {\n          e.preventDefault()\n          handleSignout()\n        }}\n      >\n        <SubmitButton {...rest} isLoading={isPending}>\n          Log out\n        </SubmitButton>\n      </form>\n    )\n  }\n)\n"
  },
  {
    "path": "storefront/src/modules/account/components/UpsertAddressForm.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ReactAria from \"react-aria-components\"\nimport { CountrySelectProps } from \"@modules/checkout/components/country-select\"\nimport { CountrySelectField, Form, InputField } from \"@/components/Forms\"\nimport { UiCloseButton } from \"@/components/Dialog\"\nimport { z } from \"zod\"\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport { customerAddressSchema, useAddressMutation } from \"hooks/customer\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\nexport const UpsertAddressForm = withReactQueryProvider<{\n  addressId?: string\n  region?: CountrySelectProps[\"region\"]\n  defaultValues?: {\n    first_name?: string\n    last_name?: string\n    company?: string\n    address_1?: string\n    address_2?: string\n    city?: string\n    postal_code?: string\n    province?: string\n    country_code?: string\n    phone?: string\n  }\n}>(({ addressId, region, defaultValues }) => {\n  const { close } = React.useContext(ReactAria.OverlayTriggerStateContext)!\n  const { mutate, isPending, data } = useAddressMutation(addressId)\n\n  const onSubmit = (values: z.infer<typeof customerAddressSchema>) => {\n    mutate(values, {\n      onSuccess: (res) => {\n        if (res.success) {\n          close()\n        }\n      },\n    })\n  }\n\n  return (\n    <Form\n      onSubmit={onSubmit}\n      schema={customerAddressSchema}\n      defaultValues={{\n        first_name: defaultValues?.first_name,\n        last_name: defaultValues?.last_name,\n        company: defaultValues?.company,\n        address_1: defaultValues?.address_1,\n        address_2: defaultValues?.address_2,\n        phone: defaultValues?.phone,\n        city: defaultValues?.city,\n        postal_code: defaultValues?.postal_code,\n        country_code: defaultValues?.country_code,\n        province: defaultValues?.province,\n      }}\n    >\n      {({ setValue, watch }) => {\n        const watchedValues = watch()\n        const isDisabled =\n          !Object.values(watchedValues).some((value) => value) ||\n          (defaultValues\n            ? !Object.entries(watchedValues).some(\n                ([key, value]) =>\n                  defaultValues[key as keyof typeof defaultValues] !== value\n              )\n            : false)\n        return (\n          <>\n            <p className=\"text-md mb-8 md:mb-10\">\n              {addressId ? \"Change address\" : \"Add another address\"}\n            </p>\n            <div className=\"flex flex-col gap-4 md:gap-8 mb-8 md:mb-10\">\n              <div className=\"flex max-xs:flex-col gap-4 md:gap-6\">\n                <InputField\n                  placeholder=\"First name\"\n                  name=\"first_name\"\n                  className=\" flex-1\"\n                  inputProps={{\n                    autoComplete: \"given-name\",\n                  }}\n                />\n                <InputField\n                  placeholder=\"Last name\"\n                  name=\"last_name\"\n                  className=\" flex-1\"\n                  inputProps={{\n                    autoComplete: \"family-name\",\n                  }}\n                />\n              </div>\n              <InputField\n                placeholder=\"Company (Optional)\"\n                name=\"company\"\n                className=\" flex-1\"\n                inputProps={{\n                  autoComplete: \"organization\",\n                }}\n              />\n              <InputField\n                placeholder=\"Address\"\n                name=\"address_1\"\n                inputProps={{\n                  autoComplete: \"address-line1\",\n                }}\n              />\n              <InputField\n                placeholder=\"Apartment, suite, etc. (Optional)\"\n                name=\"address_2\"\n                inputProps={{\n                  autoComplete: \"address-line2\",\n                }}\n              />\n              <InputField\n                placeholder=\"Phone (Optional)\"\n                name=\"phone\"\n                type=\"tel\"\n                inputProps={{\n                  autoComplete: \"tel\",\n                }}\n              />\n              <div className=\"flex max-xs:flex-col gap-4 md:gap-6\">\n                <InputField\n                  placeholder=\"Postal code\"\n                  name=\"postal_code\"\n                  className=\" flex-1\"\n                  inputProps={{\n                    autoComplete: \"postal-code\",\n                  }}\n                />\n                <InputField\n                  placeholder=\"City\"\n                  name=\"city\"\n                  className=\" flex-1\"\n                  inputProps={{ autoComplete: \"address-level2\" }}\n                />\n              </div>\n              <div className=\"flex max-xs:flex-col gap-4 md:gap-6\">\n                <InputField\n                  placeholder=\"Province (Optional)\"\n                  name=\"province\"\n                  className=\" flex-1\"\n                  inputProps={{ autoComplete: \"address-level1\" }}\n                />\n                <CountrySelectField\n                  selectProps={{\n                    region: region ?? undefined,\n                    defaultSelectedKey: defaultValues?.country_code,\n                    autoComplete: \"country\",\n                    onSelectionChange: (value) =>\n                      setValue(\"country_code\", `${value}`),\n                  }}\n                  name=\"country_code\"\n                  className=\"flex-1\"\n                />\n              </div>\n              {!data?.success && (\n                <p className=\"text-red-primary\">{data?.error}</p>\n              )}\n            </div>\n            <div className=\"flex gap-6 justify-between\">\n              <SubmitButton isLoading={isPending} isDisabled={isDisabled}>\n                {addressId ? \"Save changes\" : \"Add address\"}\n              </SubmitButton>\n              <UiCloseButton variant=\"outline\">Cancel</UiCloseButton>\n            </div>\n          </>\n        )\n      }}\n    </Form>\n  )\n})\n"
  },
  {
    "path": "storefront/src/modules/auth/components/ForgotPasswordForm.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Form, InputField } from \"@/components/Forms\"\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport { forgotPassword } from \"@lib/data/customer\"\nimport { LocalizedButtonLink } from \"@/components/LocalizedLink\"\nimport { z } from \"zod\"\n\nconst forgotPasswordFormSchema = z.object({\n  email: z.string().min(3).email(),\n})\n\nexport const ForgotPasswordForm: React.FC = () => {\n  const [formState, formAction] = React.useActionState(forgotPassword, {\n    state: \"initial\",\n  })\n\n  const onSubmit = (values: z.infer<typeof forgotPasswordFormSchema>) => {\n    React.startTransition(() => {\n      formAction(values)\n    })\n  }\n\n  if (formState.state === \"success\") {\n    return (\n      <>\n        <h1 className=\"text-xl md:text-2xl mb-8\">\n          Your password is waiting for you!\n        </h1>\n        <div className=\"mb-8\">\n          <p>\n            We&apos;ve sent you an email with further instructions on retrieving\n            your account.\n          </p>\n        </div>\n        <LocalizedButtonLink href=\"/\" isFullWidth>\n          Back to home page\n        </LocalizedButtonLink>\n      </>\n    )\n  }\n\n  return (\n    <Form onSubmit={onSubmit} schema={forgotPasswordFormSchema}>\n      <h1 className=\"text-xl md:text-2xl mb-8\">Forgot password?</h1>\n      <div className=\"mb-8\">\n        <p>\n          Enter your email address below and we will send you instructions on\n          how to reset your password.\n        </p>\n      </div>\n      <InputField\n        placeholder=\"Email\"\n        name=\"email\"\n        className=\"flex-1 mb-8\"\n        type=\"email\"\n      />\n      {formState.state === \"error\" && (\n        <p className=\"text-red-primary text-sm\">{formState.error}</p>\n      )}\n      <SubmitButton isFullWidth>Reset your password</SubmitButton>\n    </Form>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/auth/components/LoginForm.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport { Form, InputField } from \"@/components/Forms\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { twMerge } from \"tailwind-merge\"\nimport { z } from \"zod\"\nimport { useLogin } from \"hooks/customer\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\nimport { useRouter } from \"next/navigation\"\nimport { emailFormSchema } from \"@modules/checkout/components/email\"\n\nconst loginFormSchema = z.object({\n  email: z.string().email(),\n  password: z.string().min(6),\n})\n\nexport const LoginForm = withReactQueryProvider<{\n  className?: string\n  redirectUrl?: string\n  handleCheckout?: (values: z.infer<typeof emailFormSchema>) => void\n}>(({ className, redirectUrl, handleCheckout }) => {\n  const { isPending, data, mutate } = useLogin()\n\n  const router = useRouter()\n\n  const onSubmit = (values: z.infer<typeof loginFormSchema>) => {\n    mutate(\n      { ...values, redirect_url: redirectUrl },\n      {\n        onSuccess: (res) => {\n          if (handleCheckout && res.success) {\n            handleCheckout({ email: values.email })\n          } else if (res.success) {\n            router.push(res.redirectUrl || \"/\")\n          }\n        },\n      }\n    )\n  }\n  return (\n    <Form onSubmit={onSubmit} schema={loginFormSchema}>\n      <div className={twMerge(\"flex flex-col gap-6 md:gap-8\", className)}>\n        <InputField\n          placeholder=\"Email\"\n          name=\"email\"\n          inputProps={{ autoComplete: \"email\" }}\n          className=\"flex-1\"\n        />\n        <InputField\n          placeholder=\"Password\"\n          name=\"password\"\n          type=\"password\"\n          className=\"flex-1\"\n          inputProps={{ autoComplete: \"current-password\" }}\n        />\n        <LocalizedLink\n          href=\"/auth/forgot-password\"\n          variant=\"underline\"\n          className=\"self-start !pb-0 text-grayscale-500 leading-none\"\n        >\n          Forgot password?\n        </LocalizedLink>\n        {!data?.success && (\n          <p className=\"text-red-primary text-sm\">{data?.message}</p>\n        )}\n        <SubmitButton isLoading={isPending}>Log in</SubmitButton>\n      </div>\n    </Form>\n  )\n})\n"
  },
  {
    "path": "storefront/src/modules/auth/components/ResetPasswordForm.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { redirect } from \"next/navigation\"\n\nimport { resetPassword } from \"@lib/data/customer\"\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport { Form, InputField } from \"@/components/Forms\"\nimport { z } from \"zod\"\nimport { UiModal, UiModalOverlay } from \"@/components/ui/Modal\"\nimport { UiCloseButton, UiDialog } from \"@/components/Dialog\"\nimport { Icon } from \"@/components/Icon\"\n\nconst resetPasswordSchema = z.object({\n  type: z.literal(\"reset\"),\n  current_password: z.string().min(6),\n  new_password: z.string().min(6),\n  confirm_new_password: z.string().min(6),\n})\n\nconst forgotPasswordSchema = z.object({\n  type: z.literal(\"forgot\"),\n  new_password: z.string().min(6),\n  confirm_new_password: z.string().min(6),\n})\n\nconst baseSchema = z.discriminatedUnion(\"type\", [\n  resetPasswordSchema,\n  forgotPasswordSchema,\n])\n\nconst resetPasswordFormSchema = baseSchema.superRefine((data, ctx) => {\n  if (data.new_password !== data.confirm_new_password) {\n    ctx.addIssue({\n      code: z.ZodIssueCode.custom,\n      message: \"Passwords must match\",\n      path: [\"confirm_new_password\"],\n    })\n  }\n\n  if (data.type === \"reset\" && data.current_password === data.new_password) {\n    ctx.addIssue({\n      code: z.ZodIssueCode.custom,\n      message: \"New password must be different from the current password\",\n      path: [\"new_password\"],\n    })\n  }\n})\n\nexport const ChangePasswordForm: React.FC<{\n  email: string\n  token: string\n  customer?: boolean\n}> = ({ email, token, customer }) => {\n  const [formState, formAction, isPending] = React.useActionState(\n    resetPassword,\n    { email, token, state: \"initial\" }\n  )\n\n  const [isModalOpen, setIsModalOpen] = React.useState(false)\n\n  React.useEffect(() => {\n    if (formState.state === \"success\") {\n      setIsModalOpen(true)\n    }\n  }, [formState])\n\n  const onSubmit = (values: z.infer<typeof resetPasswordFormSchema>) => {\n    React.startTransition(() => formAction(values))\n  }\n\n  return (\n    <>\n      <Form\n        onSubmit={onSubmit}\n        schema={resetPasswordFormSchema}\n        defaultValues={customer ? { type: \"reset\" } : { type: \"forgot\" }}\n      >\n        <h1 className=\"text-lg mb-6 md:mb-8\">Reset password</h1>\n        <div className=\"flex flex-col gap-4 mb-6 md:mb-8\">\n          {customer && (\n            <InputField\n              type=\"password\"\n              placeholder=\"Current password\"\n              name=\"current_password\"\n              inputProps={{ autoComplete: \"current-password\" }}\n            />\n          )}\n          <InputField\n            type=\"password\"\n            placeholder=\"New password\"\n            name=\"new_password\"\n            inputProps={{ autoComplete: \"new-password\" }}\n          />\n          <InputField\n            type=\"password\"\n            placeholder=\"Confirm new password\"\n            name=\"confirm_new_password\"\n            inputProps={{ autoComplete: \"new-password\" }}\n          />\n        </div>\n        {formState.state === \"error\" && (\n          <p className=\"text-red-primary text-sm mb-6\">{formState.error}</p>\n        )}\n        <SubmitButton isLoading={isPending} isFullWidth>\n          Reset password\n        </SubmitButton>\n      </Form>\n      <UiModalOverlay\n        isOpen={isModalOpen}\n        isDismissable={false}\n        onOpenChange={(isOpen) => !isOpen && redirect(\"/auth/login\")}\n        className=\"bg-transparent\"\n      >\n        <UiModal className=\"relative\">\n          <UiDialog>\n            <p className=\"text-md mb-12\">Password reset successful!</p>\n            <p className=\"text-grayscale-500\">\n              Your password has been successfully reset. You may now use your\n              new password to log in.\n            </p>\n            <UiCloseButton\n              variant=\"ghost\"\n              className=\"absolute top-4 right-6 p-0\"\n            >\n              <Icon name=\"close\" className=\"w-6 h-6\" />\n            </UiCloseButton>\n          </UiDialog>\n        </UiModal>\n      </UiModalOverlay>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/auth/components/SignUpForm.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport { Form, InputField } from \"@/components/Forms\"\nimport { z } from \"zod\"\nimport { signupFormSchema, useSignup } from \"hooks/customer\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\nexport const SignUpForm = withReactQueryProvider(() => {\n  const { mutateAsync, isPending, data } = useSignup()\n\n  const onSubmit = async (values: z.infer<typeof signupFormSchema>) => {\n    await mutateAsync(values)\n  }\n\n  return (\n    <Form onSubmit={onSubmit} schema={signupFormSchema}>\n      {({ watch }) => {\n        const formData = watch()\n        const isDisabled = !Object.values(formData).some((value) => value)\n\n        return (\n          <div className=\"flex flex-col gap-6 md:gap-8 mb-8 md:mb-16\">\n            <div className=\"flex gap-4 md:gap-6\">\n              <InputField\n                placeholder=\"First name\"\n                name=\"first_name\"\n                className=\" flex-1\"\n                inputProps={{ autoComplete: \"given-name\" }}\n              />\n              <InputField\n                placeholder=\"Last name\"\n                name=\"last_name\"\n                className=\" flex-1\"\n                inputProps={{ autoComplete: \"family-name\" }}\n              />\n            </div>\n            <InputField\n              placeholder=\"Email\"\n              name=\"email\"\n              className=\" flex-1\"\n              type=\"email\"\n              inputProps={{ autoComplete: \"email\" }}\n            />\n            <InputField\n              placeholder=\"Phone\"\n              name=\"phone\"\n              className=\" flex-1\"\n              type=\"tel\"\n              inputProps={{ autoComplete: \"tel\" }}\n            />\n            <InputField\n              placeholder=\"Password\"\n              name=\"password\"\n              type=\"password\"\n              className=\" flex-1\"\n              inputProps={{ autoComplete: \"new-password\" }}\n            />\n            <InputField\n              placeholder=\"Confirm password\"\n              name=\"confirm_password\"\n              type=\"password\"\n              className=\" flex-1\"\n              inputProps={{ autoComplete: \"new-password\" }}\n            />\n            {data?.error && (\n              <p className=\"text-red-primary text-sm\">{data.error}</p>\n            )}\n            <SubmitButton isDisabled={isDisabled} isPending={isPending}>\n              Register\n            </SubmitButton>\n          </div>\n        )\n      }}\n    </Form>\n  )\n})\n"
  },
  {
    "path": "storefront/src/modules/cart/components/cart-totals/index.tsx",
    "content": "\"use client\"\n\nimport { HttpTypes } from \"@medusajs/types\"\nimport React from \"react\"\n\nimport { convertToLocale } from \"@lib/util/money\"\nimport { twJoin, twMerge } from \"tailwind-merge\"\n\ntype CartTotalsProps = {\n  cart: HttpTypes.StoreCart\n  isPartOfCartDrawer?: boolean\n  className?: string\n}\n\nconst CartTotals: React.FC<CartTotalsProps> = ({\n  cart,\n  isPartOfCartDrawer,\n  className,\n}) => {\n  const {\n    currency_code,\n    total,\n    subtotal,\n    tax_total,\n    shipping_total,\n    discount_total,\n    gift_card_total,\n  } = cart\n\n  return (\n    <div className={className}>\n      <div\n        className={twMerge(\n          \"flex flex-col gap-4\",\n          isPartOfCartDrawer && \"gap-2\"\n        )}\n      >\n        <div className=\"flex justify-between\">\n          <p className=\"text-grayscale-500\">Subtotal:</p>\n          <p\n            className=\"self-end\"\n            data-testid=\"cart-subtotal\"\n            data-value={subtotal || 0}\n          >\n            {convertToLocale({ amount: subtotal ?? 0, currency_code })}\n          </p>\n        </div>\n        {!!discount_total && (\n          <div className=\"flex justify-between\">\n            <p className=\"text-grayscale-500\">Discount:</p>\n            <p\n              className=\"self-end\"\n              data-testid=\"cart-discount\"\n              data-value={discount_total || 0}\n            >\n              -{\" \"}\n              {convertToLocale({ amount: discount_total ?? 0, currency_code })}\n            </p>\n          </div>\n        )}\n        <div className=\"flex justify-between\">\n          <p className=\"text-grayscale-500\">Shipping:</p>\n          <p\n            className=\"self-end\"\n            data-testid=\"cart-shipping\"\n            data-value={shipping_total || 0}\n          >\n            {convertToLocale({ amount: shipping_total ?? 0, currency_code })}\n          </p>\n        </div>\n        <div className=\"flex justify-between\">\n          <p className=\"text-grayscale-500\">Taxes:</p>\n          <p\n            className=\"self-end\"\n            data-testid=\"cart-taxes\"\n            data-value={tax_total || 0}\n          >\n            {convertToLocale({ amount: tax_total ?? 0, currency_code })}\n          </p>\n        </div>\n        {!!gift_card_total && (\n          <div className=\"flex justify-between\">\n            <p className=\"text-grayscale-500\">Gift card:</p>\n            <p\n              className=\"self-end\"\n              data-testid=\"cart-gift-card-amount\"\n              data-value={gift_card_total || 0}\n            >\n              -{\" \"}\n              {convertToLocale({ amount: gift_card_total ?? 0, currency_code })}\n            </p>\n          </div>\n        )}\n      </div>\n      <hr\n        className={twJoin(\n          \"my-8 md:my-6 text-grayscale-200\",\n          isPartOfCartDrawer && \"my-4 md:my-4\"\n        )}\n      />\n      <div className=\"flex justify-between text-md font-semibold\">\n        <p>Total:</p>\n        <p data-testid=\"cart-total\" data-value={total || 0}>\n          {convertToLocale({ amount: total ?? 0, currency_code })}\n        </p>\n      </div>\n    </div>\n  )\n}\n\nexport default CartTotals\n"
  },
  {
    "path": "storefront/src/modules/cart/components/discount-code/index.tsx",
    "content": "\"use client\"\n\nimport React from \"react\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nimport { Form, InputField } from \"@/components/Forms\"\nimport { twMerge } from \"tailwind-merge\"\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport { z } from \"zod\"\nimport { useApplyPromotions } from \"hooks/cart\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\ntype DiscountCodeProps = {\n  cart: HttpTypes.StoreCart\n  className?: string\n}\n\nexport const codeFormSchema = z.object({\n  code: z.string().min(1),\n})\n\nconst DiscountCode: React.FC<DiscountCodeProps> = ({ cart, className }) => {\n  const applyPromotions = useApplyPromotions()\n\n  const { promotions = [] } = cart\n  const addPromotionCode = async (values: { code: string }) => {\n    if (!values.code) {\n      return\n    }\n    const codes = promotions\n      .filter((p) => p.code === undefined)\n      .map((p) => p.code!)\n    codes.push(values.code)\n\n    await applyPromotions.mutateAsync(codes)\n  }\n\n  return (\n    <Form onSubmit={addPromotionCode} schema={codeFormSchema}>\n      <div className={twMerge(\"flex gap-2 mt-10\", className)}>\n        <InputField\n          name=\"code\"\n          inputProps={{ autoFocus: false, uiSize: \"md\" }}\n          placeholder=\"Discount code\"\n          className=\"flex flex-1 flex-col\"\n        />\n        <SubmitButton>Apply</SubmitButton>\n      </div>\n    </Form>\n  )\n}\n\nexport default withReactQueryProvider(DiscountCode)\n"
  },
  {
    "path": "storefront/src/modules/cart/components/empty-cart-message/index.tsx",
    "content": "import { LocalizedLink } from \"@/components/LocalizedLink\"\n\nconst EmptyCartMessage = () => {\n  return (\n    <div>\n      <div className=\"lg:h-22 pb-12 lg:pb-0 border-b border-b-grayscale-100\">\n        <h1 className=\"md:text-2xl text-lg leading-none\">Your shopping cart</h1>\n      </div>\n      <p className=\"text-base-regular mt-4 mb-6 max-w-[32rem]\">\n        You don&apos;t have anything in your cart. Let&apos;s change that, use\n        the link below to start browsing our products.\n      </p>\n      <div>\n        <LocalizedLink href=\"/store\">Explore products</LocalizedLink>\n      </div>\n    </div>\n  )\n}\n\nexport default EmptyCartMessage\n"
  },
  {
    "path": "storefront/src/modules/cart/components/item/index.tsx",
    "content": "\"use client\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport { getVariantItemsInStock } from \"@lib/util/inventory\"\nimport ErrorMessage from \"@modules/checkout/components/error-message\"\nimport DeleteButton from \"@modules/common/components/delete-button\"\nimport LineItemUnitPrice from \"@modules/common/components/line-item-unit-price\"\nimport Thumbnail from \"@modules/products/components/thumbnail\"\nimport { InputNumberField } from \"@/components/InputNumberField\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { twMerge } from \"tailwind-merge\"\nimport { useLineItemQuantityUpdater } from \"hooks/cart\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\ntype ItemProps = {\n  item: HttpTypes.StoreCartLineItem\n  className?: string\n}\n\nconst Item = ({ item, className }: ItemProps) => {\n  const { handle } = item.variant?.product ?? {}\n  const {\n    quantity,\n    error,\n    onQuantityChange,\n    onQuantityCommit,\n    onQuantityFocus,\n    onQuantityBlur,\n  } = useLineItemQuantityUpdater({\n    lineId: item.id,\n    initialQuantity: item.quantity,\n  })\n  const maxQuantity = item.variant ? getVariantItemsInStock(item.variant) : 0\n\n  return (\n    <div\n      className={twMerge(\n        \"border-b border-grayscale-100 py-8 lg:last:pb-0 lg:last:border-b-0\",\n        className\n      )}\n    >\n      <div className=\"flex gap-6\">\n        <LocalizedLink href={`/products/${handle}`}>\n          <Thumbnail\n            thumbnail={item.variant?.product?.thumbnail}\n            images={item.variant?.product?.images}\n            size=\"3/4\"\n            className=\"w-25 sm:w-30\"\n          />\n        </LocalizedLink>\n        <div className=\"flex-grow flex flex-col justify-between\">\n          <div>\n            <h2 className=\"sm:text-md text-base font-normal\">\n              <LocalizedLink href={`/products/${handle}`}>\n                {item.product_title}\n              </LocalizedLink>\n            </h2>\n            <p className=\"text-grayscale-500 text-xs sm:text-base max-sm:mb-4\">\n              {item.variant?.title}\n            </p>\n            <LineItemUnitPrice item={item} className=\"sm:hidden\" />\n          </div>\n          <InputNumberField\n            key={item.id}\n            size=\"sm\"\n            minValue={1}\n            maxValue={maxQuantity}\n            value={quantity}\n            onChange={onQuantityChange}\n            onCommit={onQuantityCommit}\n            onFocus={onQuantityFocus}\n            onBlur={onQuantityBlur}\n            className=\"w-25\"\n            aria-label=\"Quantity\"\n          />\n        </div>\n        <div className=\"flex flex-col justify-between items-end text-right\">\n          <LineItemUnitPrice item={item} className=\"max-sm:hidden\" />\n          <DeleteButton id={item.id} data-testid=\"product-delete-button\" />\n        </div>\n      </div>\n      <ErrorMessage\n        error={error?.message}\n        data-testid=\"product-error-message\"\n      />\n    </div>\n  )\n}\n\nexport default withReactQueryProvider(Item)\n"
  },
  {
    "path": "storefront/src/modules/cart/templates/index.tsx",
    "content": "\"use client\"\nimport EmptyCartMessage from \"@modules/cart/components/empty-cart-message\"\nimport ItemsTemplate from \"@modules/cart/templates/items\"\nimport Summary from \"@modules/cart/templates/summary\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { useCart } from \"hooks/cart\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\nimport SkeletonCartPage from \"@modules/skeletons/templates/skeleton-cart-page\"\n\n// TODO: Ask customer if they want to sign in or continue as guest\nconst CartTemplate = () => {\n  const { data: cart, isPending } = useCart({ enabled: true })\n  if (isPending) {\n    return <SkeletonCartPage />\n  }\n  return (\n    <Layout className=\"py-26 md:pb-36 md:pt-39\">\n      {cart?.items?.length ? (\n        <>\n          <LayoutColumn\n            start={1}\n            end={{ base: 13, lg: 9, xl: 10 }}\n            className=\"mb-8 lg:mb-0\"\n          >\n            <ItemsTemplate items={cart?.items} />\n          </LayoutColumn>\n          <LayoutColumn start={{ base: 1, lg: 9, xl: 10 }} end={13}>\n            {cart && cart.region && <Summary cart={cart} />}\n          </LayoutColumn>\n        </>\n      ) : (\n        <LayoutColumn start={1} end={{ base: 13 }} className=\"mb-14 lg:mb-0\">\n          <EmptyCartMessage />\n        </LayoutColumn>\n      )}\n    </Layout>\n  )\n}\n\nexport default withReactQueryProvider(CartTemplate)\n"
  },
  {
    "path": "storefront/src/modules/cart/templates/items.tsx",
    "content": "import { HttpTypes } from \"@medusajs/types\"\n\nimport Item from \"@modules/cart/components/item\"\n\ntype ItemsTemplateProps = {\n  items?: HttpTypes.StoreCartLineItem[]\n}\n\nconst ItemsTemplate = ({ items }: ItemsTemplateProps) => {\n  return (\n    <div>\n      <div className=\"pb-8 md:pb-12 border-b border-b-grayscale-100\">\n        <h1 className=\"md:text-2xl text-md leading-none\">Your shopping cart</h1>\n      </div>\n      <div>\n        {items\n          ? items\n              .sort((a, b) => {\n                return (a.created_at ?? \"\") > (b.created_at ?? \"\") ? -1 : 1\n              })\n              .map((item) => {\n                return <Item key={item.id} item={item} />\n              })\n          : null}\n      </div>\n    </div>\n  )\n}\n\nexport default ItemsTemplate\n"
  },
  {
    "path": "storefront/src/modules/cart/templates/summary.tsx",
    "content": "\"use client\"\n\nimport { HttpTypes } from \"@medusajs/types\"\n\nimport { LocalizedButtonLink, LocalizedLink } from \"@/components/LocalizedLink\"\nimport CartTotals from \"@modules/cart/components/cart-totals\"\nimport DiscountCode from \"@modules/cart/components/discount-code\"\nimport { getCheckoutStep } from \"@modules/cart/utils/getCheckoutStep\"\nimport { Icon } from \"@/components/Icon\"\nimport { useCustomer } from \"hooks/customer\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\ntype SummaryProps = {\n  cart: HttpTypes.StoreCart\n}\n\nconst Summary = ({ cart }: SummaryProps) => {\n  const step = getCheckoutStep(cart)\n\n  const { data: customer, isPending } = useCustomer()\n\n  return (\n    <>\n      <CartTotals cart={cart} className=\"lg:pt-8\" />\n      <DiscountCode cart={cart} />\n      <LocalizedButtonLink\n        href={\"/checkout?step=\" + step}\n        isFullWidth\n        className=\"mt-6\"\n      >\n        Proceed to checkout\n      </LocalizedButtonLink>\n      {!customer && !isPending && (\n        <div className=\"bg-grayscale-50 mt-8 rounded-xs p-4 flex items-center text-grayscale-500 gap-4\">\n          <Icon name=\"info\" />\n          <p>\n            Already have an account? No worries, just{\" \"}\n            <LocalizedLink\n              href=\"/auth/login\"\n              variant=\"underline\"\n              className=\"text-black !p-0\"\n            >\n              log in.\n            </LocalizedLink>\n          </p>\n        </div>\n      )}\n    </>\n  )\n}\n\nexport default withReactQueryProvider(Summary)\n"
  },
  {
    "path": "storefront/src/modules/cart/utils/getCheckoutStep.tsx",
    "content": "import { HttpTypes } from \"@medusajs/types\"\n\nexport function getCheckoutStep(cart?: HttpTypes.StoreCart) {\n  if (!cart?.email) {\n    return \"email\"\n  }\n\n  if (!cart?.shipping_address?.address_1) {\n    return \"delivery\"\n  }\n\n  if (cart?.shipping_methods?.length === 0) {\n    return \"shipping\"\n  }\n\n  if (!cart?.payment_collection) {\n    return \"payment\"\n  }\n\n  return \"review\"\n}\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/addresses/index.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { usePathname, useRouter, useSearchParams } from \"next/navigation\"\nimport { twJoin } from \"tailwind-merge\"\nimport compareAddresses from \"@lib/util/compare-addresses\"\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport BillingAddress from \"@modules/checkout/components/billing_address\"\nimport ErrorMessage from \"@modules/checkout/components/error-message\"\nimport ShippingAddress from \"@modules/checkout/components/shipping-address\"\nimport { Button } from \"@/components/Button\"\nimport { Form } from \"@/components/Forms\"\nimport { z } from \"zod\"\nimport { useCustomer } from \"hooks/customer\"\nimport { useSetShippingAddress } from \"hooks/cart\"\nimport { StoreCart } from \"@medusajs/types\"\n\nconst addressesFormSchema = z\n  .object({\n    shipping_address: z.object({\n      first_name: z.string().min(1),\n      last_name: z.string().min(1),\n      company: z.string().optional(),\n      address_1: z.string().min(1),\n      address_2: z.string().optional(),\n      city: z.string().min(1),\n      postal_code: z.string().min(1),\n      province: z.string().optional(),\n      country_code: z.string().min(2),\n      phone: z.string().optional(),\n    }),\n  })\n  .and(\n    z.discriminatedUnion(\"same_as_billing\", [\n      z.object({\n        same_as_billing: z.literal(\"on\"),\n      }),\n      z.object({\n        same_as_billing: z.literal(\"off\").optional(),\n        billing_address: z.object({\n          first_name: z.string().min(1),\n          last_name: z.string().min(1),\n          company: z.string().optional(),\n          address_1: z.string().min(1),\n          address_2: z.string().optional(),\n          city: z.string().min(1),\n          postal_code: z.string().min(1),\n          province: z.string().optional(),\n          country_code: z.string().min(2),\n          phone: z.string().optional(),\n        }),\n      }),\n    ])\n  )\n\nconst Addresses = ({ cart }: { cart: StoreCart }) => {\n  const searchParams = useSearchParams()\n  const router = useRouter()\n  const pathname = usePathname()\n\n  const isOpen = searchParams.get(\"step\") === \"delivery\"\n\n  const [sameAsBilling, setSameAsBilling] = React.useState(true)\n\n  const { data: customer } = useCustomer()\n\n  React.useEffect(() => {\n    if (cart?.shipping_address && cart?.billing_address) {\n      setSameAsBilling(\n        compareAddresses(cart.shipping_address, cart.billing_address)\n      )\n    }\n  }, [cart?.billing_address, cart?.shipping_address])\n\n  const toggleSameAsBilling = React.useCallback(() => {\n    setSameAsBilling((prev) => !prev)\n  }, [setSameAsBilling])\n\n  const { mutate, isPending, data } = useSetShippingAddress()\n\n  const onSubmit = (values: z.infer<typeof addressesFormSchema>) => {\n    mutate(values, {\n      onSuccess: (data) => {\n        if (isOpen && data.success) {\n          router.push(pathname + \"?step=shipping\", { scroll: false })\n        }\n      },\n    })\n  }\n  if (!cart) {\n    return null\n  }\n\n  return (\n    <>\n      <div className=\"flex justify-between mb-6 md:mb-8 border-t border-grayscale-200 pt-8 mt-8\">\n        <div>\n          <p\n            className={twJoin(\n              \"transition-fontWeight duration-75\",\n              isOpen && \"font-semibold\"\n            )}\n          >\n            2. Delivery details\n          </p>\n        </div>\n        {!isOpen && cart?.shipping_address && (\n          <Button\n            variant=\"link\"\n            onPress={() => {\n              router.push(pathname + \"?step=delivery\")\n            }}\n          >\n            Change\n          </Button>\n        )}\n      </div>\n      {isOpen ? (\n        <Form\n          schema={addressesFormSchema}\n          onSubmit={onSubmit}\n          formProps={{\n            id: `email`,\n          }}\n          defaultValues={\n            sameAsBilling\n              ? {\n                  shipping_address: cart?.shipping_address || {\n                    first_name: \"\",\n                    last_name: \"\",\n                    company: \"\",\n                    province: \"\",\n                    city: \"\",\n                    postal_code: \"\",\n                    country_code: \"\",\n                    address_1: \"\",\n                    address_2: \"\",\n                    phone: \"\",\n                  },\n                  same_as_billing: \"on\",\n                }\n              : {\n                  shipping_address: cart?.shipping_address || {\n                    first_name: \"\",\n                    last_name: \"\",\n                    company: \"\",\n                    province: \"\",\n                    city: \"\",\n                    postal_code: \"\",\n                    country_code: \"\",\n                    address_1: \"\",\n                    address_2: \"\",\n                    phone: \"\",\n                  },\n                  same_as_billing: \"off\",\n                  billing_address: cart?.billing_address || {\n                    first_name: \"\",\n                    last_name: \"\",\n                    company: \"\",\n                    province: \"\",\n                    city: \"\",\n                    postal_code: \"\",\n                    country_code: \"\",\n                    address_1: \"\",\n                    address_2: \"\",\n                    phone: \"\",\n                  },\n                }\n          }\n        >\n          {({ watch }) => {\n            const shippingData = watch(\"shipping_address\")\n            const isDisabled =\n              !customer?.addresses?.length &&\n              !Object.values(shippingData).some((value) => value)\n            return (\n              <>\n                <ShippingAddress\n                  customer={customer || null}\n                  checked={sameAsBilling}\n                  onChange={toggleSameAsBilling}\n                  cart={cart}\n                />\n\n                {!sameAsBilling && (\n                  <BillingAddress cart={cart} customer={customer || null} />\n                )}\n\n                <SubmitButton\n                  className=\"mt-8\"\n                  isLoading={isPending}\n                  isDisabled={isDisabled}\n                >\n                  Next\n                </SubmitButton>\n                <ErrorMessage error={data?.error} />\n              </>\n            )\n          }}\n        </Form>\n      ) : cart?.shipping_address ? (\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"flex max-sm:flex-col flex-wrap gap-y-2 gap-x-12\">\n            <div className=\"text-grayscale-500\">Shipping address</div>\n            <div className=\"text-grayscale-600\">\n              {[\n                cart.shipping_address.first_name,\n                cart.shipping_address.last_name,\n              ]\n                .filter(Boolean)\n                .join(\" \")}\n              <br />\n              {[\n                cart.shipping_address.address_1,\n                cart.shipping_address.address_2,\n              ]\n                .filter(Boolean)\n                .join(\" \")}\n              <br />\n              {[cart.shipping_address.postal_code, cart.shipping_address.city]\n                .filter(Boolean)\n                .join(\" \")}\n              <br />\n              {cart.shipping_address.country_code?.toUpperCase()}\n              <br />\n              {cart.shipping_address.phone}\n            </div>\n          </div>\n          {sameAsBilling || cart.billing_address ? (\n            <div className=\"flex max-sm:flex-col flex-wrap gap-y-2 gap-x-17\">\n              <div className=\"text-grayscale-500\">Billing address</div>\n              <div className=\"text-grayscale-600\">\n                {sameAsBilling ? (\n                  \"Same as shipping address\"\n                ) : (\n                  <>\n                    {[\n                      cart.billing_address?.first_name,\n                      cart.billing_address?.last_name,\n                    ]\n                      .filter(Boolean)\n                      .join(\" \")}\n                    <br />\n                    {[\n                      cart.billing_address?.address_1,\n                      cart.billing_address?.address_2,\n                    ]\n                      .filter(Boolean)\n                      .join(\" \")}\n                    <br />\n                    {[\n                      cart.billing_address?.postal_code,\n                      cart.billing_address?.city,\n                    ]\n                      .filter(Boolean)\n                      .join(\" \")}\n                    <br />\n                    {cart.billing_address?.country_code?.toUpperCase()}\n                  </>\n                )}\n              </div>\n            </div>\n          ) : null}\n        </div>\n      ) : null}\n    </>\n  )\n}\n\nexport default Addresses\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/billing_address/index.tsx",
    "content": "import React, { useEffect, useMemo } from \"react\"\nimport * as ReactAria from \"react-aria-components\"\n\nimport { HttpTypes } from \"@medusajs/types\"\nimport { CountrySelectField, InputField } from \"@/components/Forms\"\nimport { Icon } from \"@/components/Icon\"\nimport { UiCloseButton, UiDialog, UiDialogTrigger } from \"@/components/Dialog\"\nimport { Button } from \"@/components/Button\"\nimport { UiModal, UiModalOverlay } from \"@/components/ui/Modal\"\nimport compareAddresses from \"@lib/util/compare-addresses\"\nimport { UiRadio, UiRadioBox, UiRadioLabel } from \"@/components/ui/Radio\"\nimport { UpsertAddressForm } from \"@modules/account/components/UpsertAddressForm\"\nimport { useCountryCode } from \"hooks/country-code\"\nimport { twMerge } from \"tailwind-merge\"\nimport { useFormContext } from \"react-hook-form\"\n\nconst isBillingAddressEmpty = (formData: {\n  billing_address?: Pick<\n    HttpTypes.StoreCartAddress,\n    | \"first_name\"\n    | \"last_name\"\n    | \"address_1\"\n    | \"address_2\"\n    | \"company\"\n    | \"postal_code\"\n    | \"city\"\n    | \"country_code\"\n    | \"province\"\n    | \"phone\"\n  >\n}) => {\n  return (\n    !formData?.billing_address?.first_name &&\n    !formData?.billing_address?.last_name &&\n    !formData?.billing_address?.address_1 &&\n    !formData?.billing_address?.address_2 &&\n    !formData?.billing_address?.company &&\n    !formData?.billing_address?.postal_code &&\n    !formData?.billing_address?.city &&\n    !formData?.billing_address?.country_code &&\n    !formData?.billing_address?.province &&\n    !formData?.billing_address?.phone\n  )\n}\n\nconst BillingAddress = ({\n  cart,\n  customer,\n}: {\n  cart: HttpTypes.StoreCart | null\n  customer: HttpTypes.StoreCustomer | null\n}) => {\n  const countryCode = useCountryCode()\n\n  const { setValue, watch } = useFormContext()\n\n  const setFormAddress = (\n    address?: Pick<\n      HttpTypes.StoreCartAddress,\n      | \"first_name\"\n      | \"last_name\"\n      | \"address_1\"\n      | \"address_2\"\n      | \"company\"\n      | \"postal_code\"\n      | \"city\"\n      | \"country_code\"\n      | \"province\"\n      | \"phone\"\n    >\n  ) => {\n    if (address) {\n      setValue(\"billing_address\", {\n        first_name: address?.first_name || \"\",\n        last_name: address?.last_name || \"\",\n        address_1: address?.address_1 || \"\",\n        company: address?.company || \"\",\n        postal_code: address?.postal_code || \"\",\n        city: address?.city || \"\",\n        country_code: address?.country_code || \"\",\n        province: address?.province || \"\",\n        phone: address?.phone || \"\",\n      })\n    }\n  }\n  const formData = watch()\n  const handleChange = (\n    e:\n      | React.ChangeEvent<\n          HTMLInputElement | HTMLInputElement | HTMLSelectElement\n        >\n      | {\n          target: { name: string; value: string }\n        }\n  ) => {\n    setValue(e.target.name, e.target.value)\n  }\n  const countriesInRegion = useMemo(\n    () => cart?.region?.countries?.map((c) => c.iso_2),\n    [cart?.region]\n  )\n  const addressesInRegion = useMemo(\n    () =>\n      customer?.addresses.filter(\n        (a) => a.country_code && countriesInRegion?.includes(a.country_code)\n      ),\n    [customer?.addresses, countriesInRegion]\n  )\n  useEffect(() => {\n    // Ensure cart is not null and has a billing_address before setting form data\n    if (cart) {\n      if (cart.billing_address) {\n        setFormAddress(cart.billing_address)\n      } else if (\n        // If customer has saved addresses in the region and form data is empty\n        // set the first address in the region as the form data\n        customer &&\n        addressesInRegion &&\n        addressesInRegion.length &&\n        isBillingAddressEmpty(formData)\n      ) {\n        const defaultBillingAddress =\n          addressesInRegion.find((a) => a.is_default_billing) ||\n          addressesInRegion[0]\n\n        setFormAddress({\n          first_name: defaultBillingAddress.first_name ?? undefined,\n          last_name: defaultBillingAddress.last_name ?? undefined,\n          address_1: defaultBillingAddress.address_1 ?? undefined,\n          address_2: defaultBillingAddress.address_2 ?? undefined,\n          company: defaultBillingAddress.company ?? undefined,\n          postal_code: defaultBillingAddress.postal_code ?? undefined,\n          city: defaultBillingAddress.city ?? undefined,\n          country_code: defaultBillingAddress.country_code ?? undefined,\n          province: defaultBillingAddress.province ?? undefined,\n          phone: defaultBillingAddress.phone ?? undefined,\n        })\n      }\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [cart, customer, addressesInRegion])\n\n  return (\n    <>\n      {customer &&\n      (addressesInRegion?.length || 0) > 0 &&\n      !isBillingAddressEmpty(formData) ? (\n        <div className=\"w-full border border-grayscale-200 rounded-xs p-4 flex flex-wrap gap-8 max-lg:flex-col mt-8\">\n          <div className=\"flex flex-1 gap-8\">\n            <Icon name=\"user\" className=\"w-6 h-6 mt-2.5\" />\n            <div className=\"flex flex-col gap-8 flex-1\">\n              <div className=\"flex flex-wrap justify-between gap-6\">\n                <div className=\"grow basis-0\">\n                  <p className=\"text-xs text-grayscale-500 mb-1.5\">Country</p>\n                  <p>\n                    {cart?.region?.countries?.find(\n                      (c) => c.iso_2 === formData.billing_address?.country_code\n                    )?.display_name || formData.billing_address?.country_code}\n                  </p>\n                </div>\n                <div className=\"grow basis-0\">\n                  <p className=\"text-xs text-grayscale-500 mb-1.5\">Address</p>\n                  <p>{formData.billing_address?.address_1}</p>\n                </div>\n              </div>\n              {formData.billing_address?.address_2 && (\n                <div>\n                  <p className=\"text-xs text-grayscale-500 mb-1.5\">\n                    Apartment, suite, etc. (Optional)\n                  </p>\n                  <p>{formData.billing_address?.address_2}</p>\n                </div>\n              )}\n              <div className=\"flex flex-wrap justify-between gap-6\">\n                <div className=\"grow basis-0\">\n                  <p className=\"text-xs text-grayscale-500 mb-1.5\">\n                    Postal Code\n                  </p>\n                  <p>{formData.billing_address?.postal_code}</p>\n                </div>\n                <div className=\"grow basis-0\">\n                  <p className=\"text-xs text-grayscale-500 mb-1.5\">City</p>\n                  <p>{formData.billing_address?.city}</p>\n                </div>\n              </div>\n            </div>\n          </div>\n          <UiDialogTrigger>\n            <Button variant=\"outline\" size=\"sm\" className=\"shrink-0\">\n              Change\n            </Button>\n            <UiModalOverlay>\n              <UiModal>\n                <UiDialog>\n                  <p className=\"text-md mb-10\">Change address</p>\n                  <ReactAria.RadioGroup\n                    className=\"flex flex-col gap-4 mb-10\"\n                    aria-label=\"Shipping methods\"\n                    onChange={(value) => {\n                      const selectedAddress = addressesInRegion?.find(\n                        (a) => a.id === value\n                      )\n                      if (selectedAddress) {\n                        setFormAddress({\n                          address_1: selectedAddress.address_1 ?? undefined,\n                          address_2: selectedAddress.address_2 ?? undefined,\n                          city: selectedAddress.city ?? undefined,\n                          company: selectedAddress.company ?? undefined,\n                          country_code:\n                            selectedAddress.country_code ?? undefined,\n                          first_name: selectedAddress.first_name ?? undefined,\n                          last_name: selectedAddress.last_name ?? undefined,\n                          phone: selectedAddress.phone ?? undefined,\n                          postal_code: selectedAddress.postal_code ?? undefined,\n                          province: selectedAddress.province ?? undefined,\n                        })\n                      }\n                    }}\n                    value={\n                      addressesInRegion?.find((a) =>\n                        compareAddresses(\n                          {\n                            first_name: a.first_name ?? \"\",\n                            last_name: a.last_name ?? \"\",\n                            address_1: a.address_1 ?? \"\",\n                            address_2: a.address_2 ?? \"\",\n                            company: a.company ?? \"\",\n                            postal_code: a.postal_code ?? \"\",\n                            city: a.city ?? \"\",\n                            country_code: a.country_code ?? \"\",\n                            province: a.province ?? \"\",\n                            phone: a.phone ?? \"\",\n                          },\n                          {\n                            first_name: formData.billing_address?.first_name,\n                            last_name: formData.billing_address?.last_name,\n                            address_1: formData.billing_address?.address_1,\n                            address_2: formData.billing_address?.address_2,\n                            company: formData.billing_address?.company,\n                            postal_code: formData.billing_address?.postal_code,\n                            city: formData.billing_address?.city,\n                            country_code:\n                              formData.billing_address?.country_code,\n                            province: formData.billing_address?.province,\n                            phone: formData.billing_address?.phone,\n                          }\n                        )\n                      )?.id\n                    }\n                  >\n                    {addressesInRegion?.map((address) => (\n                      <UiRadio\n                        variant=\"outline\"\n                        value={address.id}\n                        className=\"gap-4\"\n                        key={address.id}\n                        id={address.id}\n                      >\n                        <UiRadioBox />\n                        <UiRadioLabel>\n                          {[address.first_name, address.last_name]\n                            .filter(Boolean)\n                            .join(\" \")}\n                        </UiRadioLabel>\n                        <UiRadioLabel className=\"ml-auto text-grayscale-500 group-data-[selected=true]:font-normal\">\n                          {[\n                            address.address_1,\n                            address.address_2,\n                            [address.postal_code, address.city]\n                              .filter(Boolean)\n                              .join(\" \"),\n                            cart?.region?.countries?.find(\n                              (c) => c.iso_2 === address.country_code\n                            )?.display_name || address.country_code,\n                          ]\n                            .filter(Boolean)\n                            .join(\", \")}\n                        </UiRadioLabel>\n                      </UiRadio>\n                    ))}\n                  </ReactAria.RadioGroup>\n                  <div className=\"flex justify-between\">\n                    <UiDialogTrigger>\n                      <Button>Add new address</Button>\n                      <UiModalOverlay>\n                        <UiModal>\n                          <UiDialog>\n                            <UpsertAddressForm\n                              region={cart?.region}\n                              defaultValues={{ country_code: countryCode }}\n                            />\n                          </UiDialog>\n                        </UiModal>\n                      </UiModalOverlay>\n                    </UiDialogTrigger>\n                    <UiCloseButton variant=\"outline\">Close</UiCloseButton>\n                  </div>\n                </UiDialog>\n              </UiModal>\n            </UiModalOverlay>\n          </UiDialogTrigger>\n        </div>\n      ) : (\n        <div className={twMerge(\"grid grid-cols-2 gap-4 mt-8\")}>\n          <InputField\n            placeholder=\"First name\"\n            name=\"billing_address.first_name\"\n            inputProps={{\n              autoComplete: \"given-name\",\n            }}\n            data-testid=\"billing-first-name-input\"\n          />\n          <InputField\n            placeholder=\"Last name\"\n            name=\"billing_address.last_name\"\n            inputProps={{\n              autoComplete: \"family-name\",\n            }}\n            data-testid=\"billing-last-name-input\"\n          />\n          <InputField\n            placeholder=\"Address\"\n            name=\"billing_address.address_1\"\n            inputProps={{\n              autoComplete: \"address-line1\",\n            }}\n            data-testid=\"billing-address-input\"\n          />\n          <InputField\n            placeholder=\"Company\"\n            name=\"billing_address.company\"\n            inputProps={{\n              autoComplete: \"company\",\n            }}\n            data-testid=\"billing-company-input\"\n          />\n          <InputField\n            placeholder=\"Postal code\"\n            name=\"billing_address.postal_code\"\n            inputProps={{\n              autoComplete: \"postal-code\",\n            }}\n            data-testid=\"billing-postal-input\"\n          />\n          <InputField\n            placeholder=\"City\"\n            name=\"billing_address.city\"\n            inputProps={{\n              autoComplete: \"address-level2\",\n            }}\n            data-testid=\"billing-city-input\"\n          />\n          <CountrySelectField\n            name=\"billing_address.country_code\"\n            selectProps={{\n              autoComplete: \"country\",\n              region: cart?.region,\n              selectedKey: formData.billing_address?.country_code || null,\n              onSelectionChange: (value) =>\n                handleChange({\n                  target: {\n                    name: \"billing_address.country_code\",\n                    value: `${value}`,\n                  },\n                }),\n            }}\n            data-testid=\"billing-country-select\"\n          />\n          <InputField\n            placeholder=\"State / Province\"\n            name=\"billing_address.province\"\n            inputProps={{ autoComplete: \"address-level1\" }}\n            data-testid=\"billing-province-input\"\n          />\n          <InputField\n            placeholder=\"Phone\"\n            name=\"billing_address.phone\"\n            inputProps={{ autoComplete: \"tel\" }}\n            data-testid=\"billing-phone-input\"\n          />\n        </div>\n      )}\n    </>\n  )\n}\n\nexport default BillingAddress\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/checkout-form/index.tsx",
    "content": "\"use client\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\nimport React from \"react\"\nimport { useRouter } from \"next/navigation\"\n\nimport Wrapper from \"@modules/checkout/components/payment-wrapper\"\nimport Email from \"@modules/checkout/components/email\"\nimport Addresses from \"@modules/checkout/components/addresses\"\nimport Shipping from \"@modules/checkout/components/shipping\"\nimport Payment from \"@modules/checkout/components/payment\"\nimport Review from \"@modules/checkout/components/review\"\nimport { useCart } from \"hooks/cart\"\nimport { getCheckoutStep } from \"@modules/cart/utils/getCheckoutStep\"\nimport { Icon } from \"@/components/Icon\"\n\nexport const CheckoutForm = withReactQueryProvider<{\n  countryCode: string\n  step: string | undefined\n}>(({ countryCode, step }) => {\n  const { data: cart, isPending } = useCart({ enabled: true })\n  const router = useRouter()\n  React.useEffect(() => {\n    if (!step && cart) {\n      const checkoutStep = getCheckoutStep(cart)\n      router.push(`/${countryCode}/checkout?step=${checkoutStep}`)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [step, countryCode, cart])\n  if (isPending) {\n    return (\n      <div className=\"absolute left-0 top-20 md:top-40 lg:top-0 w-[100vw] lg:max-w-[calc(100vw-((50vw-50%)+448px))] xl:max-w-[calc(100vw-((50vw-50%)+540px))] -ml-[calc(50vw-50%)] h-screen lg:w-full flex items-center justify-center\">\n        <Icon name=\"loader\" className=\"w-10 md:w-20 animate-spin\" />\n      </div>\n    )\n  }\n\n  if (!cart) {\n    return null\n  }\n\n  return (\n    <Wrapper cart={cart}>\n      <Email countryCode={countryCode} cart={cart} />\n      <Addresses cart={cart} />\n      <Shipping cart={cart} />\n      <Payment cart={cart} />\n      <Review cart={cart} />\n    </Wrapper>\n  )\n})\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/checkout-summary-wrapper/index.tsx",
    "content": "\"use client\"\nimport CheckoutSummary from \"@modules/checkout/templates/checkout-summary\"\nimport { useCart } from \"hooks/cart\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\nimport SkeletonCheckoutSummary from \"@modules/skeletons/templates/skeleton-checkout-summary\"\n\nfunction CheckoutSummaryWrapper() {\n  const { data: cart, isPending } = useCart({ enabled: true })\n  if (isPending || !cart) {\n    return <SkeletonCheckoutSummary />\n  }\n\n  return <CheckoutSummary cart={cart} />\n}\n\nexport default withReactQueryProvider(CheckoutSummaryWrapper)\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/country-select/index.tsx",
    "content": "\"use client\"\n\nimport { useMemo } from \"react\"\n\nimport { HttpTypes } from \"@medusajs/types\"\nimport * as ReactAria from \"react-aria-components\"\nimport {\n  UiSelectButton,\n  UiSelectIcon,\n  UiSelectListBox,\n  UiSelectListBoxItem,\n  UiSelectValue,\n} from \"@/components/ui/Select\"\n\nexport type CountrySelectProps = ReactAria.SelectProps<\n  Exclude<HttpTypes.StoreRegion[\"countries\"], undefined>[number]\n> & {\n  region?: HttpTypes.StoreRegion\n}\n\nconst CountrySelect: React.FC<CountrySelectProps> = ({\n  placeholder = \"Country\",\n  region,\n  ...props\n}) => {\n  const countryOptions = useMemo(() => {\n    if (!region) {\n      return []\n    }\n\n    return region.countries?.map((country) => ({\n      value: country.iso_2,\n      label: country.display_name,\n    }))\n  }, [region])\n\n  return (\n    <ReactAria.Select\n      aria-label=\"Select country\"\n      {...props}\n      placeholder={placeholder}\n    >\n      <UiSelectButton className=\"!h-14\">\n        <UiSelectValue className=\"text-base\" />\n        <UiSelectIcon />\n      </UiSelectButton>\n      <ReactAria.Popover className=\"w-[--trigger-width]\">\n        <UiSelectListBox>\n          {countryOptions?.map(({ value, label }, index) => (\n            // eslint-disable-next-line react/no-array-index-key\n            <UiSelectListBoxItem key={index} id={value}>\n              {label}\n            </UiSelectListBoxItem>\n          ))}\n        </UiSelectListBox>\n      </ReactAria.Popover>\n    </ReactAria.Select>\n  )\n}\n\nCountrySelect.displayName = \"CountrySelect\"\n\nexport default CountrySelect\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/discount-code/index.tsx",
    "content": "\"use client\"\n\nimport React from \"react\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nimport { Form, InputField } from \"@/components/Forms\"\nimport { codeFormSchema } from \"@modules/cart/components/discount-code\"\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport { useApplyPromotions } from \"hooks/cart\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\ntype DiscountCodeProps = {\n  cart: HttpTypes.StoreCart\n}\n\nconst DiscountCode: React.FC<DiscountCodeProps> = ({ cart }) => {\n  const applyPromotions = useApplyPromotions()\n\n  const { promotions = [] } = cart\n  const addPromotionCode = async (values: { code: string }) => {\n    if (!values.code) {\n      return\n    }\n    const codes = promotions\n      .filter((p) => p.code === undefined)\n      .map((p) => p.code!)\n    codes.push(values.code)\n\n    await applyPromotions.mutateAsync(codes)\n  }\n\n  return (\n    <Form onSubmit={addPromotionCode} schema={codeFormSchema}>\n      <div className=\"flex max-sm:flex-col gap-x-6 gap-y-4 mb-8\">\n        <InputField\n          name=\"code\"\n          inputProps={{ autoFocus: false, className: \"max-lg:h-12\" }}\n          placeholder=\"Discount code\"\n          className=\"flex-1\"\n        />\n        <SubmitButton className=\"lg:h-auto max-h-14 grow-0 h-12\">\n          Apply\n        </SubmitButton>\n      </div>\n    </Form>\n  )\n}\n\nexport default withReactQueryProvider(DiscountCode)\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/email/index.tsx",
    "content": "\"use client\"\n\nimport React from \"react\"\nimport { usePathname, useRouter, useSearchParams } from \"next/navigation\"\nimport { twJoin } from \"tailwind-merge\"\nimport { z } from \"zod\"\n\nimport { SubmitButton } from \"@modules/common/components/submit-button\"\nimport { Button } from \"@/components/Button\"\nimport { Form, InputField } from \"@/components/Forms\"\nimport { UiCloseButton, UiDialog, UiDialogTrigger } from \"@/components/Dialog\"\nimport { UiModal, UiModalOverlay } from \"@/components/ui/Modal\"\nimport { Icon } from \"@/components/Icon\"\nimport { LoginForm } from \"@modules/auth/components/LoginForm\"\nimport ErrorMessage from \"@modules/checkout/components/error-message\"\nimport { useCustomer } from \"hooks/customer\"\nimport { useSetEmail } from \"hooks/cart\"\nimport { StoreCart } from \"@medusajs/types\"\n\nexport const emailFormSchema = z.object({\n  email: z.string().min(3).email(\"Enter a valid email address.\"),\n})\n\nconst Email = ({\n  countryCode,\n  cart,\n}: {\n  countryCode: string\n  cart: StoreCart\n}) => {\n  const searchParams = useSearchParams()\n  const router = useRouter()\n  const pathname = usePathname()\n\n  const { data: customer, isPending: customerPending } = useCustomer()\n\n  const isOpen = searchParams.get(\"step\") === \"email\"\n\n  const { mutate, isPending, data } = useSetEmail()\n\n  const onSubmit = (values: z.infer<typeof emailFormSchema>) => {\n    mutate(\n      { ...values, country_code: countryCode },\n      {\n        onSuccess: (res) => {\n          if (isOpen && res?.success) {\n            router.push(pathname + \"?step=delivery\", { scroll: false })\n          }\n        },\n      }\n    )\n  }\n\n  return (\n    <>\n      <div className=\"flex justify-between mb-6 md:mb-8\">\n        <div className=\"flex justify-between flex-wrap gap-5 flex-1\">\n          <div>\n            <p\n              className={twJoin(\n                \"transition-fontWeight duration-75\",\n                isOpen && \"font-semibold\"\n              )}\n            >\n              1. Email\n            </p>\n          </div>\n          {isOpen && !customer && !customerPending && (\n            <div className=\"text-grayscale-500\">\n              <p>\n                Already have an account? No worries, just{\" \"}\n                <UiDialogTrigger>\n                  <Button variant=\"link\">log in.</Button>\n                  <UiModalOverlay>\n                    <UiModal className=\"relative max-w-108\">\n                      <UiDialog>\n                        <p className=\"text-md mb-10\">Log in</p>\n                        <LoginForm\n                          redirectUrl={`/${countryCode}/checkout?step=delivery`}\n                          handleCheckout={onSubmit}\n                        />\n                        <UiCloseButton\n                          variant=\"ghost\"\n                          className=\"absolute top-4 right-6 p-0\"\n                        >\n                          <Icon name=\"close\" className=\"w-6 h-6\" />\n                        </UiCloseButton>\n                      </UiDialog>\n                    </UiModal>\n                  </UiModalOverlay>\n                </UiDialogTrigger>\n              </p>\n            </div>\n          )}\n        </div>\n        {!isOpen && (\n          <Button\n            variant=\"link\"\n            onPress={() => {\n              router.push(pathname + \"?step=email\")\n            }}\n          >\n            Change\n          </Button>\n        )}\n      </div>\n      {isOpen ? (\n        <Form\n          schema={emailFormSchema}\n          onSubmit={onSubmit}\n          formProps={{\n            id: `email`,\n          }}\n          defaultValues={{ email: cart?.email || \"\" }}\n        >\n          {({ watch }) => {\n            const formValue = watch(\"email\")\n            return (\n              <>\n                <InputField\n                  placeholder=\"Email\"\n                  name=\"email\"\n                  inputProps={{\n                    autoComplete: \"email\",\n                    title: \"Enter a valid email address.\",\n                  }}\n                  data-testid=\"shipping-email-input\"\n                />\n                <SubmitButton\n                  className=\"mt-8\"\n                  isLoading={isPending}\n                  isDisabled={!formValue}\n                >\n                  Next\n                </SubmitButton>\n                <ErrorMessage error={data?.error} />\n              </>\n            )\n          }}\n        </Form>\n      ) : cart?.email ? (\n        <ul className=\"flex max-sm:flex-col flex-wrap gap-y-2 gap-x-34\">\n          <li className=\"text-grayscale-500\">Email</li>\n          <li className=\"text-grayscale-600 break-all\">{cart.email}</li>\n        </ul>\n      ) : null}\n    </>\n  )\n}\n\nexport default Email\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/error-message/index.tsx",
    "content": "const ErrorMessage = ({\n  error,\n  \"data-testid\": dataTestid,\n}: {\n  error?: string | null\n  \"data-testid\"?: string\n}) => {\n  if (!error) {\n    return null\n  }\n\n  return (\n    <div\n      className=\"pt-2 text-red-900 text-small-regular\"\n      data-testid={dataTestid}\n    >\n      <span>{error}</span>\n    </div>\n  )\n}\n\nexport default ErrorMessage\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/mobile-checkout-summary-wrapper/index.tsx",
    "content": "\"use client\"\nimport MobileCheckoutSummary from \"@modules/checkout/templates/mobile-checkout-summary\"\nimport { useCart } from \"hooks/cart\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\nimport SkeletonMobileCheckoutSummaryTrigger from \"@modules/skeletons/components/skeleton-mobile-summary-trigger\"\nfunction MobileCheckoutSummaryWrapper() {\n  const { data: cart, isPending } = useCart({ enabled: true })\n  if (isPending || !cart) {\n    return <SkeletonMobileCheckoutSummaryTrigger />\n  }\n\n  return <MobileCheckoutSummary cart={cart} />\n}\n\nexport default withReactQueryProvider(MobileCheckoutSummaryWrapper)\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/payment/index.tsx",
    "content": "\"use client\"\n\nimport { useCallback, useContext, useEffect, useMemo, useState } from \"react\"\nimport { usePathname, useRouter, useSearchParams } from \"next/navigation\"\nimport { CreditCard } from \"@medusajs/icons\"\nimport { CardElement } from \"@stripe/react-stripe-js\"\nimport { StripeCardElementOptions } from \"@stripe/stripe-js\"\nimport { twJoin } from \"tailwind-merge\"\nimport { capitalize } from \"lodash\"\n\nimport { isStripe as isStripeFunc, paymentInfoMap } from \"@lib/constants\"\nimport PaymentContainer from \"@modules/checkout/components/payment-container\"\nimport { StripeContext } from \"@modules/checkout/components/payment-wrapper\"\nimport ErrorMessage from \"@modules/checkout/components/error-message\"\nimport PaymentCardButton from \"@modules/checkout/components/payment-card-button\"\n\nimport { Button } from \"@/components/Button\"\nimport { UiRadioGroup } from \"@/components/ui/Radio\"\nimport { Input } from \"@/components/Forms\"\nimport {\n  useCartPaymentMethods,\n  useGetPaymentMethod,\n  useSetPaymentMethod,\n} from \"hooks/cart\"\nimport { StoreCart, StorePaymentSession } from \"@medusajs/types\"\n\nconst Payment = ({ cart }: { cart: StoreCart }) => {\n  const [isLoading, setIsLoading] = useState(false)\n  const [error, setError] = useState<string | null>(null)\n  const [cardBrand, setCardBrand] = useState<string | null>(null)\n  const [cardComplete, setCardComplete] = useState(false)\n\n  const searchParams = useSearchParams()\n  const router = useRouter()\n  const pathname = usePathname()\n\n  const isOpen = searchParams.get(\"step\") === \"payment\"\n\n  const useOptions: StripeCardElementOptions = useMemo(() => {\n    return {\n      style: {\n        base: {\n          fontFamily: \"Inter, sans-serif\",\n          color: \"#050505\",\n          \"::placeholder\": {\n            color: \"#808080\",\n          },\n          fontSize: \"16px\",\n        },\n      },\n      classes: {\n        base: \"pt-[18px] pb-1 block w-full h-14.5 px-4 mt-0 border rounded-xs appearance-none focus:outline-none focus:ring-0 border-grayscale-200 hover:border-grayscale-500 focus:border-grayscale-500 transition-all ease-in-out\",\n      },\n    }\n  }, [])\n\n  const createQueryString = useCallback(\n    (name: string, value: string) => {\n      const params = new URLSearchParams(searchParams)\n      params.set(name, value)\n\n      return params.toString()\n    },\n    [searchParams]\n  )\n\n  const handleEdit = () => {\n    router.push(pathname + \"?\" + createQueryString(\"step\", \"payment\"), {\n      scroll: false,\n    })\n  }\n\n  useEffect(() => {\n    setError(null)\n  }, [isOpen])\n\n  const setPaymentMethod = useSetPaymentMethod()\n\n  const activeSession = cart?.payment_collection?.payment_sessions?.find(\n    (paymentSession: StorePaymentSession) => paymentSession.status === \"pending\"\n  )\n  const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(\n    activeSession?.provider_id ?? \"\"\n  )\n  const { data: availablePaymentMethods } = useCartPaymentMethods(\n    cart?.region?.id ?? \"\"\n  )\n  const isStripe = isStripeFunc(activeSession?.provider_id)\n  const stripeReady = useContext(StripeContext)\n\n  const paymentMethodId = activeSession?.data?.payment_method_id as string\n  const { data: paymentMethod } = useGetPaymentMethod(paymentMethodId)\n\n  const paymentReady =\n    activeSession &&\n    cart?.shipping_methods &&\n    cart?.shipping_methods.length !== 0\n\n  const handleRemoveCard = useCallback(() => {\n    if (!activeSession?.id) {\n      return\n    }\n\n    try {\n      setPaymentMethod.mutate(\n        { sessionId: activeSession.id, token: null },\n\n        {\n          onSuccess: () => {\n            setCardBrand(null)\n            setCardComplete(false)\n          },\n          onError: () => setError(\"Failed to remove card\"),\n        }\n      )\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    } catch (err) {\n      setError(\"Failed to remove card\")\n    }\n  }, [activeSession?.id, setPaymentMethod])\n\n  useEffect(() => {\n    if (paymentMethod) {\n      setCardBrand(capitalize(paymentMethod?.card?.brand))\n      setCardComplete(true)\n    }\n  }, [paymentMethod])\n\n  if (!cart) {\n    return null\n  }\n  return (\n    <>\n      <div className=\"flex justify-between mb-6 md:mb-8 border-t border-grayscale-200 pt-8 mt-8\">\n        <div>\n          <p\n            className={twJoin(\n              \"transition-fontWeight duration-75\",\n              isOpen && \"font-semibold\"\n            )}\n          >\n            4. Payment\n          </p>\n        </div>\n        {!isOpen && paymentReady && (\n          <Button variant=\"link\" onPress={handleEdit}>\n            Change\n          </Button>\n        )}\n      </div>\n      <div className={isOpen ? \"block\" : \"hidden\"}>\n        {availablePaymentMethods?.length && (\n          <>\n            <UiRadioGroup\n              value={selectedPaymentMethod}\n              onChange={setSelectedPaymentMethod}\n              aria-label=\"Payment methods\"\n            >\n              {availablePaymentMethods\n                .sort((a, b) => {\n                  return a.id > b.id ? 1 : -1\n                })\n\n                .map((paymentMethod) => {\n                  return (\n                    <PaymentContainer\n                      paymentInfoMap={paymentInfoMap}\n                      paymentProviderId={paymentMethod.id}\n                      key={paymentMethod.id}\n                    />\n                  )\n                })}\n            </UiRadioGroup>\n            {isStripe && stripeReady && (\n              <div className=\"mt-5\">\n                {isStripeFunc(selectedPaymentMethod) &&\n                  (paymentMethod?.card?.brand ? (\n                    <Input\n                      value={\"**** **** **** \" + paymentMethod?.card.last4}\n                      placeholder=\"Card number\"\n                      disabled={true}\n                    />\n                  ) : (\n                    <CardElement\n                      options={useOptions as StripeCardElementOptions}\n                      onChange={(e) => {\n                        setCardBrand(\n                          e.brand &&\n                            e.brand.charAt(0).toUpperCase() + e.brand.slice(1)\n                        )\n                        setError(e.error?.message || null)\n                        setCardComplete(e.complete)\n                      }}\n                    />\n                  ))}\n              </div>\n            )}\n          </>\n        )}\n\n        {/* {paidByGiftcard && (\n          <div className=\"flex gap-10\">\n            <div className=\"text-grayscale-500\">Payment method</div>\n            <div>Gift card</div>\n          </div>\n        )} */}\n        <ErrorMessage\n          error={error}\n          data-testid=\"payment-method-error-message\"\n        />\n        {paymentMethod && isStripeFunc(selectedPaymentMethod) && (\n          <Button\n            className=\"mt-6 mr-6\"\n            onPress={handleRemoveCard}\n            isLoading={isLoading}\n            isDisabled={!cardComplete}\n            data-testid=\"submit-payment-button\"\n          >\n            Change card\n          </Button>\n        )}\n        <PaymentCardButton\n          setError={setError}\n          isLoading={isLoading}\n          setIsLoading={setIsLoading}\n          selectedPaymentMethod={selectedPaymentMethod}\n          createQueryString={createQueryString}\n          cart={cart}\n          cardComplete={cardComplete}\n        />\n      </div>\n\n      <div className={isOpen ? \"hidden\" : \"block\"}>\n        {cart && paymentReady && activeSession ? (\n          <div className=\"flex flex-col gap-4\">\n            <div className=\"flex max-sm:flex-col flex-wrap gap-y-2 gap-x-12\">\n              <div className=\"text-grayscale-500\">Payment method</div>\n              <div className=\"text-grayscale-600\">\n                {paymentInfoMap[selectedPaymentMethod]?.title ||\n                  selectedPaymentMethod}\n              </div>\n            </div>\n            <div className=\"flex max-sm:flex-col flex-wrap gap-y-2 gap-x-14.5\">\n              <div className=\"text-grayscale-500\">Payment details</div>\n              {isStripeFunc(selectedPaymentMethod) && cardBrand ? (\n                <div className=\"text-grayscale-600 flex items-center gap-2\">\n                  {paymentInfoMap[selectedPaymentMethod]?.icon || (\n                    <CreditCard />\n                  )}\n                  <p>{cardBrand}</p>\n                </div>\n              ) : (\n                <div>\n                  <p>Please enter card details</p>\n                </div>\n              )}\n            </div>\n          </div> /* : paidByGiftcard ? (\n          <div className=\"flex gap-10\">\n            <div className=\"text-grayscale-500\">Payment method</div>\n            <div>Gift card</div>\n          </div>\n        ) */\n        ) : null}\n      </div>\n    </>\n  )\n}\n\nexport default Payment\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/payment-button/index.tsx",
    "content": "\"use client\"\n\nimport { OnApproveActions, OnApproveData } from \"@paypal/paypal-js\"\nimport { PayPalButtons, usePayPalScriptReducer } from \"@paypal/react-paypal-js\"\nimport { useStripe } from \"@stripe/react-stripe-js\"\nimport React, { useState } from \"react\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport { useRouter } from \"next/navigation\"\n\nimport Spinner from \"@modules/common/icons/spinner\"\nimport { isManual, isPaypal, isStripe } from \"@lib/constants\"\nimport { Button } from \"@/components/Button\"\nimport ErrorMessage from \"@modules/checkout/components/error-message\"\nimport { usePlaceOrder } from \"hooks/cart\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\ntype PaymentButtonProps = {\n  cart: HttpTypes.StoreCart\n  selectPaymentMethod: () => void\n}\n\nconst PaymentButton: React.FC<PaymentButtonProps> = ({\n  cart,\n  selectPaymentMethod,\n}) => {\n  const notReady =\n    !cart ||\n    !cart.shipping_address ||\n    !cart.billing_address ||\n    !cart.email ||\n    (cart.shipping_methods?.length ?? 0) < 1\n\n  // TODO: Add this once gift cards are implemented\n  // const paidByGiftcard =\n  //   cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0\n\n  // if (paidByGiftcard) {\n  //   return <GiftCardPaymentButton />\n  // }\n\n  const paymentSession = cart.payment_collection?.payment_sessions?.[0]\n\n  switch (true) {\n    case isStripe(paymentSession?.provider_id):\n      return <StripePaymentButton notReady={notReady} cart={cart} />\n    case isManual(paymentSession?.provider_id):\n      return <ManualTestPaymentButton notReady={notReady} />\n    case isPaypal(paymentSession?.provider_id):\n      return <PayPalPaymentButton notReady={notReady} cart={cart} />\n    default:\n      return (\n        <Button\n          className=\"w-full\"\n          onPress={() => {\n            selectPaymentMethod()\n          }}\n        >\n          Select a payment method\n        </Button>\n      )\n  }\n}\n\n// const GiftCardPaymentButton = () => {\n//   const [submitting, setSubmitting] = useState(false)\n\n//   const handleOrder = async () => {\n//     setSubmitting(true)\n//     await placeOrder()\n//   }\n\n//   return (\n//     <Button onPress={handleOrder} isLoading={submitting} className=\"w-full\">\n//       Place order\n//     </Button>\n//   )\n// }\n\nconst StripePaymentButton = ({\n  cart,\n  notReady,\n}: {\n  cart: HttpTypes.StoreCart\n  notReady: boolean\n}) => {\n  const [submitting, setSubmitting] = useState(false)\n  const [errorMessage, setErrorMessage] = useState<string | null>(null)\n  const placeOrder = usePlaceOrder()\n  const router = useRouter()\n\n  const onPaymentCompleted = () => {\n    placeOrder.mutate(null, {\n      onSuccess: (data) => {\n        if (data?.type === \"order\") {\n          const countryCode =\n            data.order.shipping_address?.country_code?.toLowerCase()\n          router.push(`/${countryCode}/order/confirmed/${data.order.id}`)\n        } else if (data?.error) {\n          setErrorMessage(data.error.message)\n        }\n        setSubmitting(false)\n      },\n      onError: (error) => {\n        setErrorMessage(error.message)\n        setSubmitting(false)\n      },\n    })\n  }\n\n  const stripe = useStripe()\n\n  const session = cart.payment_collection?.payment_sessions?.find(\n    (s) => s.status === \"pending\"\n  )\n\n  const disabled = !stripe || !session?.data?.payment_method_id ? true : false\n\n  const handlePayment = async () => {\n    setSubmitting(true)\n\n    if (!stripe) {\n      setSubmitting(false)\n      return\n    }\n    const paymentMethodId = session?.data?.payment_method_id as string\n\n    await stripe\n      .confirmCardPayment(session?.data.client_secret as string, {\n        payment_method: paymentMethodId,\n      })\n      .then(({ error, paymentIntent }) => {\n        if (error) {\n          const pi = error.payment_intent\n\n          if (\n            (pi && pi.status === \"requires_capture\") ||\n            (pi && pi.status === \"succeeded\")\n          ) {\n            onPaymentCompleted()\n          }\n\n          setErrorMessage(error.message || null)\n          return\n        }\n\n        if (\n          (paymentIntent && paymentIntent.status === \"requires_capture\") ||\n          paymentIntent.status === \"succeeded\"\n        ) {\n          return onPaymentCompleted()\n        }\n\n        return\n      })\n  }\n\n  return (\n    <>\n      <Button\n        isDisabled={disabled || notReady}\n        onPress={handlePayment}\n        isLoading={submitting}\n        className=\"w-full\"\n      >\n        Place order\n      </Button>\n      <ErrorMessage error={errorMessage} />\n    </>\n  )\n}\n\nconst PayPalPaymentButton = ({\n  cart,\n  notReady,\n}: {\n  cart: HttpTypes.StoreCart\n  notReady: boolean\n}) => {\n  const [submitting, setSubmitting] = useState(false)\n  const [errorMessage, setErrorMessage] = useState<string | null>(null)\n\n  const router = useRouter()\n\n  const placeOrder = usePlaceOrder()\n\n  const onPaymentCompleted = () => {\n    placeOrder.mutate(null, {\n      onSuccess: (data) => {\n        if (data?.type === \"order\") {\n          const countryCode =\n            data.order.shipping_address?.country_code?.toLowerCase()\n          router.push(`/${countryCode}/order/confirmed/${data.order.id}`)\n        } else if (data?.error) {\n          setErrorMessage(data.error.message)\n        }\n        setSubmitting(false)\n      },\n      onError: (error) => {\n        setErrorMessage(error.message)\n        setSubmitting(false)\n      },\n    })\n  }\n\n  const session = cart.payment_collection?.payment_sessions?.find(\n    (s) => s.status === \"pending\"\n  )\n\n  const handlePayment = async (\n    _data: OnApproveData,\n    actions: OnApproveActions\n  ) => {\n    actions?.order\n      ?.authorize()\n      .then((authorization) => {\n        if (authorization.status !== \"COMPLETED\") {\n          setErrorMessage(`An error occurred, status: ${authorization.status}`)\n          return\n        }\n        onPaymentCompleted()\n      })\n      .catch(() => {\n        setErrorMessage(`An unknown error occurred, please try again.`)\n        setSubmitting(false)\n      })\n  }\n\n  const [{ isPending, isResolved }] = usePayPalScriptReducer()\n\n  if (isPending) {\n    return <Spinner />\n  }\n\n  if (isResolved) {\n    return (\n      <>\n        <PayPalButtons\n          style={{ layout: \"horizontal\" }}\n          createOrder={async () => session?.data.id as string}\n          onApprove={handlePayment}\n          disabled={notReady || submitting || isPending}\n        />\n        <ErrorMessage error={errorMessage} />\n      </>\n    )\n  }\n}\n\nconst ManualTestPaymentButton = ({ notReady }: { notReady: boolean }) => {\n  const [errorMessage, setErrorMessage] = useState<string | null>(null)\n  const placeOrder = usePlaceOrder()\n\n  const router = useRouter()\n\n  const onPaymentCompleted = () => {\n    placeOrder.mutate(null, {\n      onSuccess: (data) => {\n        if (data?.type === \"order\") {\n          const countryCode =\n            data.order.shipping_address?.country_code?.toLowerCase()\n          router.push(`/${countryCode}/order/confirmed/${data.order.id}`)\n        } else if (data?.error) {\n          setErrorMessage(data.error.message)\n        }\n      },\n      onError: (error) => {\n        setErrorMessage(error.message)\n      },\n    })\n  }\n\n  const handlePayment = () => {\n    onPaymentCompleted()\n  }\n\n  return (\n    <>\n      <Button\n        isDisabled={notReady}\n        isLoading={placeOrder.isPending}\n        onPress={handlePayment}\n        className=\"w-full\"\n      >\n        Place order\n      </Button>\n      <ErrorMessage error={errorMessage} />\n    </>\n  )\n}\n\nexport default withReactQueryProvider(PaymentButton)\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/payment-card-button/index.tsx",
    "content": "\"use client\"\n\nimport { useElements, useStripe } from \"@stripe/react-stripe-js\"\nimport * as React from \"react\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nimport { isStripe } from \"@lib/constants\"\nimport { Button } from \"@/components/Button\"\nimport { usePathname, useRouter } from \"next/navigation\"\nimport { useInitiatePaymentSession, useSetPaymentMethod } from \"hooks/cart\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\n\ntype PaymentButtonProps = {\n  cart: HttpTypes.StoreCart\n  isLoading: boolean\n  setIsLoading: (value: boolean) => void\n  cardComplete?: boolean\n  createQueryString: (name: string, value: string) => string\n  selectedPaymentMethod: string\n  setError: (value: string | null) => void\n}\n\nconst PaymentCardButton: React.FC<PaymentButtonProps> = ({\n  cart,\n  isLoading,\n  setIsLoading,\n  cardComplete,\n  createQueryString,\n  selectedPaymentMethod,\n  setError,\n}) => {\n  const session = cart.payment_collection?.payment_sessions?.find(\n    (s) => s.status === \"pending\"\n  )\n  if (isStripe(session?.provider_id) && isStripe(selectedPaymentMethod)) {\n    return (\n      <StripeCardPaymentButton\n        setError={setError}\n        cart={cart}\n        isLoading={isLoading}\n        setIsLoading={setIsLoading}\n        cardComplete={cardComplete}\n        createQueryString={createQueryString}\n      />\n    )\n  }\n\n  return (\n    <PaymentMethodButton\n      setError={setError}\n      cart={cart}\n      isLoading={isLoading}\n      setIsLoading={setIsLoading}\n      createQueryString={createQueryString}\n      selectedPaymentMethod={selectedPaymentMethod}\n    />\n  )\n}\n\nconst StripeCardPaymentButton = ({\n  cart,\n  isLoading,\n  setIsLoading,\n  cardComplete,\n  createQueryString,\n  setError,\n}: {\n  cart: HttpTypes.StoreCart\n  isLoading: boolean\n  setIsLoading: (value: boolean) => void\n  cardComplete?: boolean\n  createQueryString: (name: string, value: string) => string\n  setError: (value: string | null) => void\n}) => {\n  const stripe = useStripe()\n  const elements = useElements()\n  const card = elements?.getElement(\"card\")\n\n  const router = useRouter()\n\n  const setPaymentMethod = useSetPaymentMethod()\n\n  const session = cart.payment_collection?.payment_sessions?.find(\n    (s) => s.status === \"pending\"\n  )\n  const pathname = usePathname()\n\n  const initiatePaymentSession = useInitiatePaymentSession()\n\n  const handleSubmit = async () => {\n    setIsLoading(true)\n    try {\n      const shouldInputCard = !session\n\n      if (!isStripe(session?.provider_id)) {\n        await initiatePaymentSession.mutateAsync({ providerId: \"stripe\" })\n      }\n      if (!shouldInputCard) {\n        if (card) {\n          const token = await stripe?.createToken(card, {\n            name:\n              cart.billing_address?.first_name +\n              \" \" +\n              cart.billing_address?.last_name,\n            address_line1: cart.billing_address?.address_1 ?? undefined,\n            address_line2: cart.billing_address?.address_2 ?? undefined,\n            address_city: cart.billing_address?.city ?? undefined,\n            address_country: cart.billing_address?.country_code ?? undefined,\n            address_zip: cart.billing_address?.postal_code ?? undefined,\n            address_state: cart.billing_address?.province ?? undefined,\n          })\n          if (token) {\n            await setPaymentMethod.mutateAsync({\n              sessionId: session.id,\n              token: token.token?.id,\n            })\n          }\n        }\n        return router.push(\n          pathname + \"?\" + createQueryString(\"step\", \"review\"),\n          {\n            scroll: false,\n          }\n        )\n      }\n    } catch (err) {\n      setError(err instanceof Error ? err.message : `${err}`)\n    } finally {\n      setIsLoading(false)\n    }\n  }\n\n  return (\n    <Button\n      className=\"mt-6\"\n      onPress={handleSubmit}\n      isLoading={isLoading}\n      isDisabled={!cardComplete}\n      data-testid=\"submit-payment-button\"\n    >\n      {!session ? \"Enter card details\" : \"Continue to review\"}\n    </Button>\n  )\n}\n\nconst PaymentMethodButton = ({\n  isLoading,\n  setIsLoading,\n  createQueryString,\n  selectedPaymentMethod,\n  setError,\n}: {\n  cart: HttpTypes.StoreCart\n  isLoading: boolean\n  setIsLoading: (value: boolean) => void\n  createQueryString: (name: string, value: string) => string\n  selectedPaymentMethod: string\n  setError: (value: string | null) => void\n}) => {\n  const router = useRouter()\n  const pathname = usePathname()\n\n  const initiatePaymentSession = useInitiatePaymentSession()\n\n  const handleSubmit = () => {\n    setIsLoading(true)\n    initiatePaymentSession.mutate(\n      {\n        providerId: selectedPaymentMethod,\n      },\n      {\n        onSuccess: () => {\n          if (!isStripe(selectedPaymentMethod)) {\n            return router.push(\n              pathname + \"?\" + createQueryString(\"step\", \"review\"),\n              {\n                scroll: false,\n              }\n            )\n          }\n          setIsLoading(false)\n        },\n        onError: (err) => {\n          setError(err instanceof Error ? err.message : `${err}`)\n          setIsLoading(false)\n        },\n      }\n    )\n  }\n\n  return (\n    <Button\n      className=\"mt-6\"\n      onPress={handleSubmit}\n      isLoading={isLoading}\n      data-testid=\"submit-payment-button\"\n      isDisabled={!selectedPaymentMethod}\n    >\n      {isStripe(selectedPaymentMethod)\n        ? \"Enter card details\"\n        : \"Continue to review\"}\n    </Button>\n  )\n}\n\nexport default withReactQueryProvider(PaymentCardButton)\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/payment-container/index.tsx",
    "content": "import * as React from \"react\"\n\nimport { isManual } from \"@lib/constants\"\nimport { UiRadio, UiRadioBox, UiRadioLabel } from \"@/components/ui/Radio\"\nimport PaymentTest from \"@modules/checkout/components/payment-test\"\n\ntype PaymentContainerProps = {\n  paymentProviderId: string\n  disabled?: boolean\n  paymentInfoMap: Record<string, { title: string; icon: React.ReactNode }>\n}\n\nconst PaymentContainer: React.FC<PaymentContainerProps> = ({\n  paymentProviderId,\n  paymentInfoMap,\n  disabled = false,\n}) => {\n  const isDevelopment = process.env.NODE_ENV === \"development\"\n\n  return (\n    <UiRadio\n      key={paymentProviderId}\n      variant=\"outline\"\n      value={paymentProviderId}\n      isDisabled={disabled}\n      className=\"gap-4\"\n    >\n      <UiRadioBox />\n      <UiRadioLabel>\n        {paymentInfoMap[paymentProviderId]?.title || paymentProviderId}\n\n        {isManual(paymentProviderId) && isDevelopment && <PaymentTest />}\n      </UiRadioLabel>\n      <span className=\"ml-auto group-data-[selected=true]:font-normal\">\n        {paymentInfoMap[paymentProviderId]?.icon}\n      </span>\n    </UiRadio>\n  )\n}\n\nexport default PaymentContainer\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/payment-test/index.tsx",
    "content": "import { twMerge } from \"tailwind-merge\"\n\nconst PaymentTest = ({ className }: { className?: string }) => {\n  return (\n    <span\n      className={twMerge(\n        \"bg-ui-tag-orange-bg text-ui-tag-orange-text border-ui-tag-orange-border inline-flex items-center gap-x-0.5 border box-border txt-compact-small-plus py-[5px] h-8 rounded-md px-2.5\",\n        className\n      )}\n    >\n      <span className=\"font-semibold\">Attention:</span> For testing purposes\n      only.\n    </span>\n  )\n}\n\nexport default PaymentTest\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/payment-wrapper/index.tsx",
    "content": "\"use client\"\n\nimport { loadStripe } from \"@stripe/stripe-js\"\nimport * as React from \"react\"\nimport StripeWrapper from \"@modules/checkout/components/payment-wrapper/stripe-wrapper\"\nimport { PayPalScriptProvider } from \"@paypal/react-paypal-js\"\nimport { createContext } from \"react\"\nimport { isPaypal, isStripe } from \"@lib/constants\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\nimport { StoreCart } from \"@medusajs/types\"\n\ntype WrapperProps = {\n  children: React.ReactNode\n  cart: StoreCart\n}\n\nexport const StripeContext = createContext(false)\n\nconst stripeKey = process.env.NEXT_PUBLIC_STRIPE_KEY\nconst stripePromise = stripeKey ? loadStripe(stripeKey) : null\n\nconst paypalClientId = process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID\n\nconst Wrapper: React.FC<WrapperProps> = ({ children, cart }) => {\n  const paymentSession = cart.payment_collection?.payment_sessions?.find(\n    (s) => s.status === \"pending\"\n  )\n\n  if (\n    isStripe(paymentSession?.provider_id) &&\n    paymentSession &&\n    stripePromise\n  ) {\n    return (\n      <StripeContext.Provider value={true}>\n        <StripeWrapper\n          paymentSession={paymentSession}\n          stripeKey={stripeKey}\n          stripePromise={stripePromise}\n        >\n          {children}\n        </StripeWrapper>\n      </StripeContext.Provider>\n    )\n  }\n\n  if (\n    isPaypal(paymentSession?.provider_id) &&\n    paypalClientId !== undefined &&\n    cart\n  ) {\n    return (\n      <PayPalScriptProvider\n        options={{\n          clientId: process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID || \"test\",\n          currency: cart?.currency_code.toUpperCase(),\n          intent: \"authorize\",\n          components: \"buttons\",\n        }}\n      >\n        {children}\n      </PayPalScriptProvider>\n    )\n  }\n\n  return <div>{children}</div>\n}\n\nexport default withReactQueryProvider(Wrapper)\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx",
    "content": "\"use client\"\n\nimport { Stripe, StripeElementsOptions } from \"@stripe/stripe-js\"\nimport { Elements } from \"@stripe/react-stripe-js\"\nimport { HttpTypes } from \"@medusajs/types\"\n\ntype StripeWrapperProps = {\n  paymentSession: HttpTypes.StorePaymentSession\n  stripeKey?: string\n  stripePromise: Promise<Stripe | null> | null\n  children: React.ReactNode\n}\n\nconst StripeWrapper: React.FC<StripeWrapperProps> = ({\n  paymentSession,\n  stripeKey,\n  stripePromise,\n  children,\n}) => {\n  const options: StripeElementsOptions = {\n    clientSecret: paymentSession!.data?.client_secret as string | undefined,\n  }\n\n  if (!stripeKey) {\n    throw new Error(\n      \"Stripe key is missing. Set NEXT_PUBLIC_STRIPE_KEY environment variable.\"\n    )\n  }\n\n  if (!stripePromise) {\n    throw new Error(\n      \"Stripe promise is missing. Make sure you have provided a valid Stripe key.\"\n    )\n  }\n\n  if (!paymentSession?.data?.client_secret) {\n    throw new Error(\n      \"Stripe client secret is missing. Cannot initialize Stripe.\"\n    )\n  }\n\n  return (\n    <Elements options={options} stripe={stripePromise}>\n      {children}\n    </Elements>\n  )\n}\n\nexport default StripeWrapper\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/review/index.tsx",
    "content": "\"use client\"\n\nimport { twJoin } from \"tailwind-merge\"\nimport { usePathname, useRouter, useSearchParams } from \"next/navigation\"\n\nimport { Button } from \"@/components/Button\"\nimport PaymentButton from \"@modules/checkout/components/payment-button\"\nimport { StoreCart } from \"@medusajs/types\"\n\nconst Review = ({ cart }: { cart: StoreCart }) => {\n  const searchParams = useSearchParams()\n  const router = useRouter()\n  const pathname = usePathname()\n\n  const isOpen = searchParams.get(\"step\") === \"review\"\n\n  // const paidByGiftcard =\n  //   cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0\n  const previousStepsCompleted =\n    cart.shipping_address &&\n    cart.shipping_methods &&\n    cart.shipping_methods.length > 0 &&\n    cart.payment_collection\n\n  return (\n    <>\n      <div className=\"flex justify-between mb-6 md:mb-8 border-t border-grayscale-200 pt-8 mt-8\">\n        <div>\n          <p\n            className={twJoin(\n              \"transition-fontWeight duration-75\",\n              isOpen && \"font-semibold\"\n            )}\n          >\n            5. Review\n          </p>\n        </div>\n        {!isOpen &&\n          previousStepsCompleted &&\n          cart?.shipping_address &&\n          cart?.billing_address &&\n          cart?.email && (\n            <Button\n              variant=\"link\"\n              onPress={() => {\n                router.push(pathname + \"?step=review\", { scroll: false })\n              }}\n            >\n              View\n            </Button>\n          )}\n      </div>\n      {isOpen && previousStepsCompleted && (\n        <>\n          <p className=\"mb-8\">\n            By clicking the Place Order button, you confirm that you have read,\n            understand and accept our Terms of Use, Terms of Sale and Returns\n            Policy and acknowledge that you have read Medusa Store&apos;s\n            Privacy Policy.\n          </p>\n          <PaymentButton\n            cart={cart}\n            selectPaymentMethod={() => {\n              router.push(pathname + \"?step=payment\", { scroll: false })\n            }}\n          />\n        </>\n      )}\n    </>\n  )\n}\n\nexport default Review\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/shipping/index.tsx",
    "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport { useRouter, useSearchParams, usePathname } from \"next/navigation\"\nimport { twJoin } from \"tailwind-merge\"\nimport { convertToLocale } from \"@lib/util/money\"\nimport ErrorMessage from \"@modules/checkout/components/error-message\"\nimport { Button } from \"@/components/Button\"\nimport {\n  UiRadio,\n  UiRadioBox,\n  UiRadioGroup,\n  UiRadioLabel,\n} from \"@/components/ui/Radio\"\nimport { useCartShippingMethods, useSetShippingMethod } from \"hooks/cart\"\nimport { StoreCart } from \"@medusajs/types\"\n\nconst Shipping = ({ cart }: { cart: StoreCart }) => {\n  const [error, setError] = useState<string | null>(null)\n\n  const searchParams = useSearchParams()\n  const router = useRouter()\n  const pathname = usePathname()\n\n  const isOpen = searchParams.get(\"step\") === \"shipping\"\n\n  const { data: availableShippingMethods } = useCartShippingMethods(cart.id)\n\n  const { mutate, isPending } = useSetShippingMethod({ cartId: cart.id })\n  const selectedShippingMethod = availableShippingMethods?.find(\n    (method) => method.id === cart.shipping_methods?.[0]?.shipping_option_id\n  )\n\n  const handleSubmit = () => {\n    router.push(pathname + \"?step=payment\", { scroll: false })\n  }\n\n  const set = (id: string) => {\n    mutate(\n      { shippingMethodId: id },\n      { onError: (err) => setError(err.message) }\n    )\n  }\n\n  useEffect(() => {\n    setError(null)\n  }, [isOpen])\n\n  return (\n    <>\n      <div className=\"flex justify-between mb-6 md:mb-8 border-t border-grayscale-200 pt-8 mt-8\">\n        <div>\n          <p\n            className={twJoin(\n              \"transition-fontWeight duration-75\",\n              isOpen && \"font-semibold\"\n            )}\n          >\n            3. Shipping\n          </p>\n        </div>\n        {!isOpen &&\n          cart?.shipping_address &&\n          cart?.billing_address &&\n          cart?.email && (\n            <Button\n              variant=\"link\"\n              onPress={() => {\n                router.push(pathname + \"?step=shipping\", { scroll: false })\n              }}\n            >\n              Change\n            </Button>\n          )}\n      </div>\n      {isOpen ? (\n        availableShippingMethods?.length === 0 ? (\n          <div>\n            <p className=\"text-red-900\">\n              There are no shipping methods available for your location. Please\n              contact us for further assistance.\n            </p>\n          </div>\n        ) : (\n          <div>\n            <UiRadioGroup\n              className=\"flex flex-col gap-4 mb-8\"\n              value={selectedShippingMethod?.id}\n              onChange={set}\n              aria-label=\"Shipping methods\"\n            >\n              {availableShippingMethods?.map((option) => (\n                <UiRadio\n                  key={option.id}\n                  variant=\"outline\"\n                  value={option.id}\n                  className=\"gap-4\"\n                >\n                  <UiRadioBox />\n                  <UiRadioLabel>{option.name}</UiRadioLabel>\n                  <UiRadioLabel className=\"ml-auto group-data-[selected=true]:font-normal\">\n                    {convertToLocale({\n                      amount: option.amount!,\n                      currency_code: cart?.currency_code,\n                    })}\n                  </UiRadioLabel>\n                </UiRadio>\n              ))}\n            </UiRadioGroup>\n\n            <ErrorMessage error={error} />\n\n            <Button\n              onPress={handleSubmit}\n              isLoading={isPending}\n              isDisabled={!cart.shipping_methods?.[0]}\n            >\n              Next\n            </Button>\n          </div>\n        )\n      ) : cart &&\n        (cart.shipping_methods?.length ?? 0) > 0 &&\n        selectedShippingMethod ? (\n        <ul className=\"flex max-sm:flex-col flex-wrap gap-y-2 gap-x-28\">\n          <li className=\"text-grayscale-500\">Shipping</li>\n          <li className=\"text-grayscale-600\">{selectedShippingMethod.name}</li>\n        </ul>\n      ) : null}\n    </>\n  )\n}\n\nexport default Shipping\n"
  },
  {
    "path": "storefront/src/modules/checkout/components/shipping-address/index.tsx",
    "content": "import { HttpTypes } from \"@medusajs/types\"\nimport React, { useEffect, useMemo } from \"react\"\nimport * as ReactAria from \"react-aria-components\"\n\nimport compareAddresses from \"@lib/util/compare-addresses\"\nimport { UpsertAddressForm } from \"@modules/account/components/UpsertAddressForm\"\nimport { CountrySelectField, InputField } from \"@/components/Forms\"\nimport { UiDialogTrigger, UiDialog, UiCloseButton } from \"@/components/Dialog\"\nimport { UiModalOverlay, UiModal } from \"@/components/ui/Modal\"\nimport { UiRadio, UiRadioBox, UiRadioLabel } from \"@/components/ui/Radio\"\nimport { Icon } from \"@/components/Icon\"\nimport { Button } from \"@/components/Button\"\nimport { useCountryCode } from \"hooks/country-code\"\nimport {\n  UiCheckbox,\n  UiCheckboxBox,\n  UiCheckboxIcon,\n  UiCheckboxLabel,\n} from \"@/components/ui/Checkbox\"\nimport { useFormContext, useWatch } from \"react-hook-form\"\n\nconst isShippingAddressEmpty = (formData: {\n  shipping_address?: Pick<\n    HttpTypes.StoreCartAddress,\n    | \"first_name\"\n    | \"last_name\"\n    | \"address_1\"\n    | \"address_2\"\n    | \"company\"\n    | \"postal_code\"\n    | \"city\"\n    | \"country_code\"\n    | \"province\"\n    | \"phone\"\n  >\n}) => {\n  return (\n    !formData?.shipping_address?.first_name &&\n    !formData?.shipping_address?.last_name &&\n    !formData?.shipping_address?.address_1 &&\n    !formData?.shipping_address?.address_2 &&\n    !formData?.shipping_address?.company &&\n    !formData?.shipping_address?.postal_code &&\n    !formData?.shipping_address?.city &&\n    !formData?.shipping_address?.country_code &&\n    !formData?.shipping_address?.province &&\n    !formData?.shipping_address?.phone\n  )\n}\n// import AddressSelect from \"../address-select\"\n\nconst ShippingAddress = ({\n  customer,\n  cart,\n  checked,\n  onChange,\n}: {\n  customer: HttpTypes.StoreCustomer | null\n  cart: HttpTypes.StoreCart | null\n  checked: boolean\n  onChange: () => void\n}) => {\n  const countryCode = useCountryCode()\n\n  const { setValue, control } = useFormContext()\n\n  const formData = useWatch({ control })\n\n  const countriesInRegion = useMemo(\n    () => cart?.region?.countries?.map((c) => c.iso_2),\n    [cart?.region]\n  )\n\n  // check if customer has saved addresses that are in the current region\n  const addressesInRegion = useMemo(\n    () =>\n      customer?.addresses.filter(\n        (a) => a.country_code && countriesInRegion?.includes(a.country_code)\n      ),\n    [customer?.addresses, countriesInRegion]\n  )\n\n  const setFormAddress = (\n    address?: Pick<\n      HttpTypes.StoreCartAddress,\n      | \"first_name\"\n      | \"last_name\"\n      | \"address_1\"\n      | \"address_2\"\n      | \"company\"\n      | \"postal_code\"\n      | \"city\"\n      | \"country_code\"\n      | \"province\"\n      | \"phone\"\n    >\n  ) => {\n    if (address) {\n      setValue(\"shipping_address\", {\n        first_name: address?.first_name || \"\",\n        last_name: address?.last_name || \"\",\n        address_1: address?.address_1 || \"\",\n        company: address?.company || \"\",\n        postal_code: address?.postal_code || \"\",\n        city: address?.city || \"\",\n        country_code: address?.country_code || \"\",\n        province: address?.province || \"\",\n        phone: address?.phone || \"\",\n      })\n    }\n  }\n\n  useEffect(() => {\n    // Ensure cart is not null and has a shipping_address before setting form data\n    if (cart) {\n      if (cart.shipping_address) {\n        setFormAddress(cart.shipping_address)\n      } else if (\n        // If customer has saved addresses in the region and form data is empty\n        // set the first address in the region as the form data\n        customer &&\n        addressesInRegion &&\n        addressesInRegion.length &&\n        isShippingAddressEmpty(formData)\n      ) {\n        const defaultShippingAddress =\n          addressesInRegion.find((a) => a.is_default_shipping) ||\n          addressesInRegion[0]\n\n        setFormAddress({\n          first_name: defaultShippingAddress.first_name ?? undefined,\n          last_name: defaultShippingAddress.last_name ?? undefined,\n          address_1: defaultShippingAddress.address_1 ?? undefined,\n          address_2: defaultShippingAddress.address_2 ?? undefined,\n          company: defaultShippingAddress.company ?? undefined,\n          postal_code: defaultShippingAddress.postal_code ?? undefined,\n          city: defaultShippingAddress.city ?? undefined,\n          country_code: defaultShippingAddress.country_code ?? undefined,\n          province: defaultShippingAddress.province ?? undefined,\n          phone: defaultShippingAddress.phone ?? undefined,\n        })\n      }\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [cart, customer, addressesInRegion])\n\n  const handleChange = (\n    e:\n      | React.ChangeEvent<HTMLInputElement | HTMLSelectElement>\n      | { target: { name: string; value: string } }\n  ) => {\n    setValue(e.target.name, e.target.value)\n  }\n\n  return (\n    <>\n      {customer &&\n      (addressesInRegion?.length || 0) > 0 &&\n      !isShippingAddressEmpty(formData) ? (\n        <div className=\"w-full border border-grayscale-200 rounded-xs p-4 flex flex-wrap gap-8 max-lg:flex-col mb-8\">\n          <div className=\"flex flex-1 gap-8\">\n            <Icon name=\"user\" className=\"w-6 h-6 mt-2.5\" />\n            <div className=\"flex flex-col gap-8 flex-1\">\n              <div className=\"flex flex-wrap justify-between gap-6\">\n                <div className=\"grow basis-0\">\n                  <p className=\"text-xs text-grayscale-500 mb-1.5\">Country</p>\n                  <p>\n                    {cart?.region?.countries?.find(\n                      (c) => c.iso_2 === formData.shipping_address.country_code\n                    )?.display_name || formData.shipping_address.country_code}\n                  </p>\n                </div>\n                <div className=\"grow basis-0\">\n                  <p className=\"text-xs text-grayscale-500 mb-1.5\">Address</p>\n                  <p>{formData.shipping_address.address_1}</p>\n                </div>\n              </div>\n              {formData.shipping_address.address_2 && (\n                <div>\n                  <p className=\"text-xs text-grayscale-500 mb-1.5\">\n                    Apartment, suite, etc. (Optional)\n                  </p>\n                  <p>{formData.shipping_address.address_2}</p>\n                </div>\n              )}\n              <div className=\"flex flex-wrap justify-between gap-6\">\n                <div className=\"grow basis-0\">\n                  <p className=\"text-xs text-grayscale-500 mb-1.5\">\n                    Postal Code\n                  </p>\n                  <p>{formData.shipping_address.postal_code}</p>\n                </div>\n                <div className=\"grow basis-0\">\n                  <p className=\"text-xs text-grayscale-500 mb-1.5\">City</p>\n                  <p>{formData.shipping_address.city}</p>\n                </div>\n              </div>\n            </div>\n          </div>\n          <UiDialogTrigger>\n            <Button variant=\"outline\" size=\"sm\" className=\"shrink-0\">\n              Change\n            </Button>\n            <UiModalOverlay>\n              <UiModal>\n                <UiDialog>\n                  <p className=\"text-md mb-10\">Change address</p>\n                  <ReactAria.RadioGroup\n                    className=\"flex flex-col gap-4 mb-10\"\n                    aria-label=\"Shipping methods\"\n                    onChange={(value) => {\n                      const selectedAddress = addressesInRegion?.find(\n                        (a) => a.id === value\n                      )\n                      if (selectedAddress) {\n                        setFormAddress({\n                          address_1: selectedAddress.address_1 ?? undefined,\n                          address_2: selectedAddress.address_2 ?? undefined,\n                          city: selectedAddress.city ?? undefined,\n                          company: selectedAddress.company ?? undefined,\n                          country_code:\n                            selectedAddress.country_code ?? undefined,\n                          first_name: selectedAddress.first_name ?? undefined,\n                          last_name: selectedAddress.last_name ?? undefined,\n                          phone: selectedAddress.phone ?? undefined,\n                          postal_code: selectedAddress.postal_code ?? undefined,\n                          province: selectedAddress.province ?? undefined,\n                        })\n                      }\n                    }}\n                    value={\n                      addressesInRegion?.find((a) =>\n                        compareAddresses(\n                          {\n                            first_name: a.first_name ?? \"\",\n                            last_name: a.last_name ?? \"\",\n                            address_1: a.address_1 ?? \"\",\n                            address_2: a.address_2 ?? \"\",\n                            company: a.company ?? \"\",\n                            postal_code: a.postal_code ?? \"\",\n                            city: a.city ?? \"\",\n                            country_code: a.country_code ?? \"\",\n                            province: a.province ?? \"\",\n                            phone: a.phone ?? \"\",\n                          },\n                          {\n                            first_name: formData.shipping_address.first_name,\n                            last_name: formData.shipping_address.last_name,\n                            address_1: formData.shipping_address.address_1,\n                            address_2: formData.shipping_address.address_2,\n                            company: formData.shipping_address.company,\n                            postal_code: formData.shipping_address.postal_code,\n                            city: formData.shipping_address.city,\n                            country_code:\n                              formData.shipping_address.country_code,\n                            province: formData.shipping_address.province,\n                            phone: formData.shipping_address.phone,\n                          }\n                        )\n                      )?.id\n                    }\n                  >\n                    {addressesInRegion?.map((address) => (\n                      <UiRadio\n                        variant=\"outline\"\n                        value={address.id}\n                        className=\"gap-4\"\n                        key={address.id}\n                        id={address.id}\n                      >\n                        <UiRadioBox />\n                        <UiRadioLabel>\n                          {[address.first_name, address.last_name]\n                            .filter(Boolean)\n                            .join(\" \")}\n                        </UiRadioLabel>\n                        <UiRadioLabel className=\"ml-auto text-grayscale-500 group-data-[selected=true]:font-normal\">\n                          {[\n                            address.address_1,\n                            address.address_2,\n                            [address.postal_code, address.city]\n                              .filter(Boolean)\n                              .join(\" \"),\n                            cart?.region?.countries?.find(\n                              (c) => c.iso_2 === address.country_code\n                            )?.display_name || address.country_code,\n                          ]\n                            .filter(Boolean)\n                            .join(\", \")}\n                        </UiRadioLabel>\n                      </UiRadio>\n                    ))}\n                  </ReactAria.RadioGroup>\n                  <div className=\"flex justify-between\">\n                    <UiDialogTrigger>\n                      <Button>Add new address</Button>\n                      <UiModalOverlay>\n                        <UiModal>\n                          <UiDialog>\n                            <UpsertAddressForm\n                              region={cart?.region}\n                              defaultValues={{ country_code: countryCode }}\n                            />\n                          </UiDialog>\n                        </UiModal>\n                      </UiModalOverlay>\n                    </UiDialogTrigger>\n                    <UiCloseButton variant=\"outline\">Close</UiCloseButton>\n                  </div>\n                </UiDialog>\n              </UiModal>\n            </UiModalOverlay>\n          </UiDialogTrigger>\n        </div>\n      ) : (\n        <div className=\"grid grid-cols-2 gap-4 mb-8\">\n          <InputField\n            placeholder=\"First name\"\n            name=\"shipping_address.first_name\"\n            inputProps={{ autoComplete: \"given-name\" }}\n            data-testid=\"shipping-first-name-input\"\n          />\n          <InputField\n            placeholder=\"Last name\"\n            name=\"shipping_address.last_name\"\n            inputProps={{ autoComplete: \"family-name\" }}\n            data-testid=\"shipping-last-name-input\"\n          />\n          <InputField\n            placeholder=\"Address\"\n            name=\"shipping_address.address_1\"\n            inputProps={{ autoComplete: \"address-line1\" }}\n            data-testid=\"shipping-address-input\"\n          />\n          <InputField\n            placeholder=\"Company\"\n            name=\"shipping_address.company\"\n            inputProps={{ autoComplete: \"organization\" }}\n            data-testid=\"shipping-company-input\"\n          />\n          <InputField\n            placeholder=\"Postal code\"\n            name=\"shipping_address.postal_code\"\n            inputProps={{ autoComplete: \"postal-code\" }}\n            data-testid=\"shipping-postal-code-input\"\n          />\n          <InputField\n            placeholder=\"City\"\n            name=\"shipping_address.city\"\n            inputProps={{ autoComplete: \"address-level2\" }}\n            data-testid=\"shipping-city-input\"\n          />\n          <CountrySelectField\n            name=\"shipping_address.country_code\"\n            selectProps={{\n              autoComplete: \"country\",\n              region: cart?.region,\n              selectedKey: formData[\"shipping_address.country_code\"] || null,\n              onSelectionChange: (value) => {\n                handleChange({\n                  target: {\n                    name: \"shipping_address.country_code\",\n                    value: `${value}`,\n                  },\n                })\n              },\n            }}\n            data-testid=\"shipping-country-select\"\n          />\n          <InputField\n            placeholder=\"State / Province\"\n            name=\"shipping_address.province\"\n            inputProps={{ autoComplete: \"address-level1\" }}\n            data-testid=\"shipping-province-input\"\n          />\n          <InputField\n            placeholder=\"Phone\"\n            name=\"shipping_address.phone\"\n            inputProps={{ autoComplete: \"tel\" }}\n            data-testid=\"shipping-phone-input\"\n          />\n        </div>\n      )}\n      <div>\n        <input\n          type=\"hidden\"\n          name=\"same_as_billing\"\n          value={checked ? \"on\" : \"off\"}\n        />\n        <UiCheckbox\n          name=\"same_as_billing\"\n          isSelected={checked}\n          onChange={() => {\n            setValue(\"same_as_billing\", checked ? \"off\" : \"on\")\n            onChange()\n          }}\n          data-testid=\"billing-address-checkbox\"\n        >\n          <UiCheckboxBox>\n            <UiCheckboxIcon />\n          </UiCheckboxBox>\n          <UiCheckboxLabel>\n            Billing address same as shipping address\n          </UiCheckboxLabel>\n        </UiCheckbox>\n      </div>\n    </>\n  )\n}\n\nexport default ShippingAddress\n"
  },
  {
    "path": "storefront/src/modules/checkout/templates/checkout-summary/index.tsx",
    "content": "import { HttpTypes } from \"@medusajs/types\"\n\nimport { getPricesForVariant } from \"@lib/util/get-product-price\"\nimport DiscountCode from \"@modules/checkout/components/discount-code\"\nimport CartTotals from \"@modules/common/components/cart-totals\"\nimport Thumbnail from \"@modules/products/components/thumbnail\"\nimport { LocalizedButtonLink, LocalizedLink } from \"@/components/LocalizedLink\"\n\nconst ItemPrice: React.FC<{\n  item: HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem\n}> = ({ item }) => {\n  const {\n    original_price,\n    calculated_price,\n    original_price_number,\n    calculated_price_number,\n  } = item.variant ? (getPricesForVariant(item.variant) ?? {}) : {}\n  const hasReducedPrice =\n    (calculated_price_number ?? 0) < (original_price_number ?? 0)\n\n  return (\n    <div>\n      {hasReducedPrice ? (\n        <>\n          <p className=\"text-red-primary\">{calculated_price}</p>\n          <p className=\"text-grayscale-500 line-through\">{original_price}</p>\n        </>\n      ) : (\n        <p>{calculated_price}</p>\n      )}\n    </div>\n  )\n}\n\nconst CheckoutSummary = ({ cart }: { cart: HttpTypes.StoreCart }) => {\n  const items = cart.items ?? []\n  const numOfItems = items.length\n\n  return (\n    <>\n      <div className=\"flex justify-between items-center mb-8 lg:mb-16\">\n        <div>\n          <p>\n            Order — {numOfItems} item{numOfItems > 1 ? \"s\" : \"\"}\n          </p>\n        </div>\n        <LocalizedButtonLink href=\"/cart\" variant=\"link\">\n          Edit cart\n        </LocalizedButtonLink>\n      </div>\n      {numOfItems > 0 &&\n        items\n          .sort((a, b) => {\n            return (a.created_at ?? \"\") > (b.created_at ?? \"\") ? -1 : 1\n          })\n          .map((item) => (\n            <div key={item.id} className=\"flex gap-4 lg:gap-6 mb-8\">\n              <LocalizedLink\n                href={`/products/${item.variant?.product?.handle}`}\n              >\n                <Thumbnail\n                  thumbnail={item.variant?.product?.thumbnail}\n                  images={item.variant?.product?.images}\n                  size=\"3/4\"\n                  className=\"w-25 lg:w-33\"\n                />\n              </LocalizedLink>\n              <div className=\"flex flex-col flex-1 justify-between\">\n                <div className=\"flex flex-wrap gap-x-4 gap-y-1 justify-between\">\n                  <div>\n                    <LocalizedLink\n                      href={`/products/${item.variant?.product?.handle}`}\n                      className=\"font-semibold\"\n                    >\n                      {item.product_title}\n                    </LocalizedLink>\n                  </div>\n                  <ItemPrice item={item} />\n                </div>\n                <div className=\"flex flex-col gap-1.5 max-lg:text-xs\">\n                  {item.variant?.title && (\n                    <p>\n                      Variant:{\" \"}\n                      <span className=\"ml-1\">{item.variant.title}</span>\n                    </p>\n                  )}\n                  <p>\n                    Quantity: <span className=\"ml-1\">{item.quantity}</span>\n                  </p>\n                </div>\n              </div>\n            </div>\n          ))}\n      <DiscountCode cart={cart} />\n      <CartTotals cart={cart} />\n    </>\n  )\n}\n\nexport default CheckoutSummary\n"
  },
  {
    "path": "storefront/src/modules/checkout/templates/mobile-checkout-summary/index.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nimport CheckoutSummary from \"@modules/checkout/templates/checkout-summary\"\nimport { Icon } from \"@/components/Icon\"\nimport { convertToLocale } from \"@lib/util/money\"\n\nconst MobileCheckoutSummary = ({ cart }: { cart: HttpTypes.StoreCart }) => {\n  const { currency_code, total } = cart\n  const wrapperRef = React.useRef<HTMLDivElement>(null)\n  const onClickHandler = React.useCallback<\n    React.MouseEventHandler<HTMLButtonElement>\n  >((event) => {\n    event.preventDefault()\n\n    const button = event.currentTarget\n    const wrapper = wrapperRef.current\n    if (!wrapper || !button) {\n      return\n    }\n\n    const currentHeight = wrapper.clientHeight\n    const isOpen = currentHeight > 0\n    const newHeight = !isOpen ? wrapper.scrollHeight : 0\n\n    wrapper.style.height = `${currentHeight}px`\n    wrapper.style.overflow = \"hidden\"\n\n    requestAnimationFrame(() => {\n      button.dataset.open = isOpen ? \"no\" : \"yes\"\n      wrapper.style.height = `${newHeight}px`\n    })\n  }, [])\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        className=\"h-18 flex justify-between items-center w-full group\"\n        onClick={onClickHandler}\n        data-open=\"no\"\n      >\n        <p>Order summary</p>\n        <div className=\"flex items-center gap-4\">\n          <span>{convertToLocale({ amount: total ?? 0, currency_code })}</span>\n          <Icon\n            name=\"chevron-down\"\n            className=\"w-6 group-data-[open=yes]:rotate-180 transition-transform\"\n          />\n        </div>\n      </button>\n      <div\n        className=\"overflow-hidden transition-[height]\"\n        ref={wrapperRef}\n        style={{\n          height: \"0px\",\n        }}\n      >\n        <div className=\"py-8\">\n          <CheckoutSummary cart={cart} />\n        </div>\n      </div>\n    </>\n  )\n}\n\nexport default MobileCheckoutSummary\n"
  },
  {
    "path": "storefront/src/modules/collections/templates/index.tsx",
    "content": "import { Suspense } from \"react\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport Image from \"next/image\"\n\nimport { collectionMetadataCustomFieldsSchema } from \"@lib/util/collections\"\nimport SkeletonProductGrid from \"@modules/skeletons/templates/skeleton-product-grid\"\nimport RefinementList from \"@modules/store/components/refinement-list\"\nimport { SortOptions } from \"@modules/store/components/refinement-list/sort-products\"\nimport PaginatedProducts from \"@modules/store/templates/paginated-products\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { getCategoriesList } from \"@lib/data/categories\"\nimport { getProductTypesList } from \"@lib/data/product-types\"\nimport { getRegion } from \"@lib/data/regions\"\n\nexport default async function CollectionTemplate({\n  sortBy,\n  collection,\n  category,\n  type,\n  page,\n  countryCode,\n}: {\n  sortBy?: SortOptions\n  collection: HttpTypes.StoreCollection\n  category?: string[]\n  type?: string[]\n  page?: string\n  countryCode: string\n}) {\n  const pageNumber = page ? parseInt(page) : 1\n\n  const collectionDetails = collectionMetadataCustomFieldsSchema.safeParse(\n    collection.metadata ?? {}\n  )\n\n  const [categories, types, region] = await Promise.all([\n    getCategoriesList(0, 100, [\"id\", \"name\", \"handle\"]),\n    getProductTypesList(0, 100, [\"id\", \"value\"]),\n    getRegion(countryCode),\n  ])\n\n  return (\n    <>\n      <div className=\"max-md:mt-18 relative aspect-[2/1] md:h-screen w-full max-w-full mb-8 md:mb-19\">\n        <Image\n          src={\n            collectionDetails.data?.collection_page_image?.url ||\n            \"/images/content/living-room-gray-two-seater-puffy-sofa.png\"\n          }\n          fill\n          alt={collection.title + \" image\"}\n          className=\"object-cover z-0\"\n        />\n      </div>\n      {collectionDetails.success &&\n        ((typeof collectionDetails.data.collection_page_heading === \"string\" &&\n          collectionDetails.data.collection_page_heading.length > 0) ||\n          (typeof collectionDetails.data.collection_page_content === \"string\" &&\n            collectionDetails.data.collection_page_content.length > 0)) && (\n          <Layout className=\"mb-26 md:mb-36\">\n            {collectionDetails.data.collection_page_heading && (\n              <LayoutColumn start={1} end={{ base: 13, lg: 7 }}>\n                <h3 className=\"text-md max-md:mb-6 md:text-2xl\">\n                  {collectionDetails.data.collection_page_heading}\n                </h3>\n              </LayoutColumn>\n            )}\n            {collectionDetails.data.collection_page_content && (\n              <LayoutColumn start={{ base: 1, lg: 8 }} end={13}>\n                <div className=\"md:text-md md:mt-18 flex flex-col gap-5 md:gap-9\">\n                  {collectionDetails.data.collection_page_content\n                    .split(\"\\n\")\n                    .map((p) => p.trim())\n                    .filter(Boolean)\n                    .map((p, i) => (\n                      // eslint-disable-next-line react/no-array-index-key\n                      <p key={i}>{p}</p>\n                    ))}\n                </div>\n              </LayoutColumn>\n            )}\n          </Layout>\n        )}\n      <RefinementList\n        sortBy={sortBy}\n        title={collection.title}\n        categories={Object.fromEntries(\n          categories.product_categories.map((c) => [c.handle, c.name])\n        )}\n        category={category}\n        types={Object.fromEntries(\n          types.productTypes.map((t) => [t.value, t.value])\n        )}\n        type={type}\n      />\n      <Suspense fallback={<SkeletonProductGrid />}>\n        {region && (\n          <PaginatedProducts\n            sortBy={sortBy}\n            page={pageNumber}\n            collectionId={collection.id}\n            countryCode={countryCode}\n            categoryId={\n              !category\n                ? undefined\n                : categories.product_categories\n                    .filter((c) => category.includes(c.handle))\n                    .map((c) => c.id)\n            }\n            typeId={\n              !type\n                ? undefined\n                : types.productTypes\n                    .filter((t) => type.includes(t.value))\n                    .map((t) => t.id)\n            }\n          />\n        )}\n      </Suspense>\n      <div className=\"pb-10 md:pb-20\" />\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/common/components/cart-totals/index.tsx",
    "content": "\"use client\"\n\nimport React from \"react\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nimport { convertToLocale } from \"@lib/util/money\"\n\ntype CartTotalsProps = {\n  cart: HttpTypes.StoreCart\n}\n\nconst CartTotals: React.FC<CartTotalsProps> = ({ cart }) => {\n  const {\n    currency_code,\n    total,\n    subtotal,\n    tax_total,\n    discount_total,\n    shipping_total,\n    gift_card_total,\n  } = cart\n\n  return (\n    <div>\n      <div className=\"flex flex-col gap-2 lg:gap-1 mb-8\">\n        <div className=\"flex justify-between max-lg:text-xs\">\n          <div>\n            <p>Subtotal</p>\n          </div>\n          <div className=\"self-end\">\n            <p>{convertToLocale({ amount: subtotal ?? 0, currency_code })}</p>\n          </div>\n        </div>\n        {!!discount_total && (\n          <div className=\"flex justify-between max-lg:text-xs\">\n            <div>\n              <p>Discount</p>\n            </div>\n            <div className=\"self-end\">\n              <p>\n                -{\" \"}\n                {convertToLocale({\n                  amount: discount_total ?? 0,\n                  currency_code,\n                })}\n              </p>\n            </div>\n          </div>\n        )}\n        <div className=\"flex justify-between max-lg:text-xs\">\n          <div>\n            <p>Shipping</p>\n          </div>\n          <div className=\"self-end\">\n            <p>\n              {convertToLocale({ amount: shipping_total ?? 0, currency_code })}\n            </p>\n          </div>\n        </div>\n        <div className=\"flex justify-between max-lg:text-xs\">\n          <div>\n            <p>Taxes</p>\n          </div>\n          <div className=\"self-end\">\n            <p>{convertToLocale({ amount: tax_total ?? 0, currency_code })}</p>\n          </div>\n        </div>\n        {!!gift_card_total && (\n          <div className=\"flex justify-between max-lg:text-xs\">\n            <div>\n              <p>Gift card</p>\n            </div>\n            <div className=\"self-end\">\n              <p>\n                -{\" \"}\n                {convertToLocale({\n                  amount: gift_card_total ?? 0,\n                  currency_code,\n                })}\n              </p>\n            </div>\n          </div>\n        )}\n      </div>\n      <div className=\"flex justify-between text-md\">\n        <div>\n          <p>Total</p>\n        </div>\n        <div className=\"self-end\">\n          <p>{convertToLocale({ amount: total ?? 0, currency_code })}</p>\n        </div>\n      </div>\n      <div className=\"absolute h-full w-auto top-0 right-0 bg-black\" />\n    </div>\n  )\n}\n\nexport default CartTotals\n"
  },
  {
    "path": "storefront/src/modules/common/components/delete-button/index.tsx",
    "content": "\"use client\"\nimport { Icon } from \"@/components/Icon\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\nimport { useDeleteLineItem } from \"hooks/cart\"\n\nconst DeleteButton = ({ id }: { id: string }) => {\n  const { mutate, isPending } = useDeleteLineItem()\n\n  return (\n    <button\n      type=\"button\"\n      onClick={() => mutate({ lineId: id })}\n      disabled={isPending}\n      className=\"p-1\"\n      aria-label=\"Delete\"\n    >\n      <Icon name=\"trash\" className=\"w-4 h-4 sm:w-6 sm:h-6\" />\n    </button>\n  )\n}\n\nexport default withReactQueryProvider(DeleteButton)\n"
  },
  {
    "path": "storefront/src/modules/common/components/line-item-unit-price/index.tsx",
    "content": "import { getPricesForVariant } from \"@lib/util/get-product-price\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport { twMerge } from \"tailwind-merge\"\n\ntype LineItemUnitPriceProps = {\n  item: HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem\n  className?: string\n  regularPriceClassName?: string\n}\n\nconst LineItemUnitPrice = ({\n  item,\n  className,\n  regularPriceClassName,\n}: LineItemUnitPriceProps) => {\n  const {\n    original_price,\n    calculated_price,\n    original_price_number,\n    calculated_price_number,\n  } = item.variant ? (getPricesForVariant(item.variant) ?? {}) : {}\n  const hasReducedPrice =\n    (calculated_price_number ?? 0) < (original_price_number ?? 0)\n\n  return (\n    <div className={className}>\n      {hasReducedPrice ? (\n        <>\n          <p className=\"text-base sm:text-sm font-semibold text-red-primary\">\n            {calculated_price}\n          </p>\n          <p className=\"text-grayscale-500 line-through\">{original_price}</p>\n        </>\n      ) : (\n        <p\n          className={twMerge(\n            \"text-xs sm:text-sm font-semibold\",\n            regularPriceClassName\n          )}\n        >\n          {calculated_price}\n        </p>\n      )}\n    </div>\n  )\n}\n\nexport default LineItemUnitPrice\n"
  },
  {
    "path": "storefront/src/modules/common/components/submit-button/index.tsx",
    "content": "\"use client\"\n\nimport React from \"react\"\nimport { useFormStatus } from \"react-dom\"\n\nimport { Button, ButtonProps } from \"@/components/Button\"\n\nexport function SubmitButton(props: Omit<ButtonProps, \"type\">) {\n  const { pending } = useFormStatus()\n\n  return (\n    <Button {...props} type=\"submit\" isLoading={pending || props.isLoading} />\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/common/icons/bancontact.tsx",
    "content": "import React from \"react\"\n\nimport { IconProps } from \"types/icon\"\n\nconst Ideal: React.FC<IconProps> = ({\n  color = \"currentColor\",\n  ...attributes\n}) => {\n  return (\n    <svg\n      width=\"24px\"\n      height=\"24px\"\n      viewBox=\"0 0 24 24\"\n      role=\"img\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill={color}\n      {...attributes}\n    >\n      <title>Bancontact icon</title>\n      <path d=\"M21.385 9.768h-7.074l-4.293 5.022H1.557L3.84 12.1H1.122C.505 12.1 0 12.616 0 13.25v2.428c0 .633.505 1.15 1.122 1.15h12.933c.617 0 1.46-.384 1.874-.854l1.956-2.225 3.469-3.946.031-.035zm-1.123 1.279l-.751.855.75-.855zm2.616-3.875H9.982c-.617 0-1.462.384-1.876.853l-5.49 6.208h7.047l4.368-5.02h8.424l-2.263 2.689h2.686c.617 0 1.122-.518 1.122-1.151V8.323c0-.633-.505-1.15-1.122-1.15zm-1.87 3.024l-.374.427-.1.114.474-.54z\" />\n    </svg>\n  )\n}\n\nexport default Ideal\n"
  },
  {
    "path": "storefront/src/modules/common/icons/ideal.tsx",
    "content": "import React from \"react\"\n\nimport { IconProps } from \"types/icon\"\n\nconst Ideal: React.FC<IconProps> = ({\n  color = \"currentColor\",\n  ...attributes\n}) => {\n  return (\n    <svg\n      width=\"20px\"\n      height=\"20px\"\n      viewBox=\"0 0 24 24\"\n      role=\"img\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill={color}\n      {...attributes}\n    >\n      <title>iDEAL icon</title>\n      <path d=\"M.975 2.61v18.782h11.411c6.89 0 10.64-3.21 10.64-9.415 0-6.377-4.064-9.367-10.64-9.367H.975zm11.411-.975C22.491 1.635 24 8.115 24 11.977c0 6.7-4.124 10.39-11.614 10.39H0V1.635h12.386z M2.506 13.357h3.653v6.503H2.506z M6.602 10.082a2.27 2.27 0 1 1-4.54 0 2.27 2.27 0 0 1 4.54 0m1.396-1.057v2.12h.65c.45 0 .867-.13.867-1.077 0-.924-.463-1.043-.867-1.043h-.65zm10.85-1.054h1.053v3.174h1.56c-.428-5.758-4.958-7.002-9.074-7.002H7.999v3.83h.65c1.183 0 1.92.803 1.92 2.095 0 1.333-.719 2.129-1.92 2.129h-.65v7.665h4.388c6.692 0 9.021-3.107 9.103-7.665h-2.64V7.97zm-2.93 2.358h.76l-.348-1.195h-.063l-.35 1.195zm-1.643 1.87l1.274-4.228h1.497l1.274 4.227h-1.095l-.239-.818H15.61l-.24.818h-1.095zm-.505-1.054v1.052h-2.603V7.973h2.519v1.052h-1.467v.49h1.387v1.05H12.22v.58h1.55z\" />\n    </svg>\n  )\n}\n\nexport default Ideal\n"
  },
  {
    "path": "storefront/src/modules/common/icons/paypal.tsx",
    "content": "const PayPal = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      height=\"20\"\n      width=\"20\"\n      viewBox=\"0 0 26 25\"\n      id=\"paypalIcon\"\n    >\n      <path\n        fill=\"none\"\n        stroke=\"#303c42\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        d=\"M6.9 20.5H2c-.6 0-.5-.1-.5-.5s2.9-18 3-18.5.5-1 1-1h10c2.8 0 5 2.2 5 5h0c0 4.4-3.6 8-8 8H7.9\"\n      />\n      <path\n        fill=\"none\"\n        stroke=\"#303c42\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        d=\"M7 23.5c-.3 0-.5-.2-.5-.5 0 0 0 0 0 0 0-.3 2.4-16 2.5-16.5s.3-1 1-1h7.5c2.8 0 5 2.2 5 5h0c0 3.9-3.1 7-7 7h-2l-1 6H7z\"\n      />\n    </svg>\n  )\n}\n\nexport default PayPal\n"
  },
  {
    "path": "storefront/src/modules/common/icons/placeholder-image.tsx",
    "content": "import React from \"react\"\n\nimport { IconProps } from \"types/icon\"\n\nconst PlaceholderImage: React.FC<IconProps> = ({\n  size = \"20\",\n  color = \"currentColor\",\n  ...attributes\n}) => {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 20 20\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...attributes}\n    >\n      <path\n        d=\"M15.3141 3.16699H4.68453C3.84588 3.16699 3.16602 3.84685 3.16602 4.6855V15.3151C3.16602 16.1537 3.84588 16.8336 4.68453 16.8336H15.3141C16.1527 16.8336 16.8326 16.1537 16.8326 15.3151V4.6855C16.8326 3.84685 16.1527 3.16699 15.3141 3.16699Z\"\n        stroke={color}\n        strokeWidth=\"1.53749\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M7.91699 9.16699C8.60735 9.16699 9.16699 8.60735 9.16699 7.91699C9.16699 7.22664 8.60735 6.66699 7.91699 6.66699C7.22664 6.66699 6.66699 7.22664 6.66699 7.91699C6.66699 8.60735 7.22664 9.16699 7.91699 9.16699Z\"\n        stroke={color}\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M16.6667 12.5756L13.0208 9.1665L5 16.6665\"\n        stroke={color}\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  )\n}\n\nexport default PlaceholderImage\n"
  },
  {
    "path": "storefront/src/modules/common/icons/spinner.tsx",
    "content": "import React from \"react\"\n\nimport { IconProps } from \"types/icon\"\n\nconst Spinner: React.FC<IconProps> = ({\n  size = \"16\",\n  color = \"currentColor\",\n  ...attributes\n}) => {\n  return (\n    <svg\n      className=\"animate-spin\"\n      width={size}\n      height={size}\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      {...attributes}\n    >\n      <circle\n        className=\"opacity-25\"\n        cx=\"12\"\n        cy=\"12\"\n        r=\"10\"\n        stroke={color}\n        strokeWidth=\"4\"\n      />\n      <path\n        className=\"opacity-75\"\n        fill={color}\n        d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n      />\n    </svg>\n  )\n}\n\nexport default Spinner\n"
  },
  {
    "path": "storefront/src/modules/header/components/LoginLink.tsx",
    "content": "\"use client\"\n\nimport { Icon } from \"@/components/Icon\"\nimport { LocalizedButtonLink } from \"@/components/LocalizedLink\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\nimport { useCustomer } from \"hooks/customer\"\n\nconst LoginLink = ({ className }: { className: string }) => {\n  const { data: customer } = useCustomer()\n  return (\n    <LocalizedButtonLink\n      href={customer ? \"/account\" : \"/auth/login\"}\n      prefetch={false}\n      variant=\"ghost\"\n      className={className}\n      aria-label=\"Open account\"\n    >\n      <Icon name=\"user\" className=\"w-6 h-6\" />\n    </LocalizedButtonLink>\n  )\n}\n\nexport default withReactQueryProvider(LoginLink)\n"
  },
  {
    "path": "storefront/src/modules/order/components/OrderTotals.tsx",
    "content": "import { convertToLocale } from \"@lib/util/money\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nexport const OrderTotals: React.FC<{\n  order: HttpTypes.StoreOrder\n}> = ({ order }) => {\n  const {\n    currency_code,\n    total,\n    subtotal,\n    tax_total,\n    shipping_total,\n    discount_total,\n    gift_card_total,\n  } = order\n\n  return (\n    <div className=\"sm:max-w-65 w-full flex-1\">\n      <div className=\"flex justify-between gap-4 mb-2\">\n        <div className=\"text-grayscale-500\">\n          <p>Subtotal</p>\n        </div>\n        <div className=\"self-end\">\n          <p>\n            {convertToLocale({\n              currency_code,\n              amount: subtotal ?? 0,\n            })}\n          </p>\n        </div>\n      </div>\n      {!!discount_total && (\n        <div className=\"flex justify-between gap-4 mb-2\">\n          <div className=\"text-grayscale-500\">\n            <p>Discount</p>\n          </div>\n          <div className=\"self-end\">\n            <p>\n              -{\" \"}\n              {convertToLocale({ amount: discount_total ?? 0, currency_code })}\n            </p>\n          </div>\n        </div>\n      )}\n      <div className=\"flex justify-between gap-4 mb-2\">\n        <div className=\"text-grayscale-500\">\n          <p>Shipping</p>\n        </div>\n        <div className=\"self-end\">\n          <p>\n            {convertToLocale({\n              currency_code,\n              amount: shipping_total ?? 0,\n            })}\n          </p>\n        </div>\n      </div>\n      {!!gift_card_total && (\n        <div className=\"flex justify-between gap-4 mb-2\">\n          <div className=\"text-grayscale-500\">\n            <p>Gift card</p>\n          </div>\n          <div className=\"self-end\">\n            <p>\n              -{\" \"}\n              {convertToLocale({ amount: gift_card_total ?? 0, currency_code })}\n            </p>\n          </div>\n        </div>\n      )}\n      <div className=\"flex justify-between gap-4 text-md mb-1 mt-6\">\n        <div>\n          <p>Total</p>\n        </div>\n        <div className=\"self-end\">\n          <p>\n            {convertToLocale({\n              currency_code,\n              amount: total ?? 0,\n            })}\n          </p>\n        </div>\n      </div>\n      <p className=\"text-xs text-grayscale-500\">\n        Including {convertToLocale({ amount: tax_total ?? 0, currency_code })}{\" \"}\n        tax\n      </p>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/order/components/item/index.tsx",
    "content": "import { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport LineItemUnitPrice from \"@modules/common/components/line-item-unit-price\"\nimport Thumbnail from \"@modules/products/components/thumbnail\"\nimport { twMerge } from \"tailwind-merge\"\n\ntype ItemProps = {\n  item: HttpTypes.StoreOrderLineItem\n  className?: string\n}\n\nconst Item = ({ item, className }: ItemProps) => {\n  return (\n    <div\n      className={twMerge(\n        \"flex gap-x-6 sm:gap-x-8 gap-y-6 mb-6 pb-6 border-b border-grayscale-100 last:border-0 last:mb-0 last:pb-0\",\n        className\n      )}\n    >\n      <LocalizedLink href={`/products/${item.product_handle}`}>\n        <Thumbnail\n          thumbnail={item.variant?.product?.thumbnail}\n          images={item.variant?.product?.images}\n          size=\"3/4\"\n          className=\"w-27 sm:w-37\"\n        />\n      </LocalizedLink>\n      <div className=\"flex flex-col flex-1\">\n        <p className=\"mb-2 sm:text-md\">\n          <LocalizedLink href={`/products/${item.product_handle}`}>\n            {item.product_title}\n          </LocalizedLink>\n        </p>\n        <div className=\"text-xs flex flex-col flex-1\">\n          <div>\n            {item.variant?.options?.map((option) => (\n              <p className=\"mb-1\" key={option.id}>\n                <span className=\"text-grayscale-500 mr-2\">\n                  {option.option?.title}:\n                </span>\n                {option.value}\n              </p>\n            ))}\n          </div>\n          <div className=\"sm:mt-auto flex max-sm:flex-col gap-x-10 gap-y-6 max-sm:h-full sm:items-center justify-between relative\">\n            <div className=\"sm:self-end sm:mb-1\">\n              <p>\n                <span className=\"text-grayscale-500 mr-2\">Quantity:</span>\n                {item.quantity}\n              </p>\n            </div>\n            <LineItemUnitPrice\n              item={item}\n              regularPriceClassName=\"text-base sm:text-md font-normal\"\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport default Item\n"
  },
  {
    "path": "storefront/src/modules/order/components/payment-details/index.tsx",
    "content": "import { HttpTypes } from \"@medusajs/types\"\n\nimport { isStripe, paymentInfoMap } from \"@lib/constants\"\nimport { convertToLocale } from \"@lib/util/money\"\n\ntype PaymentDetailsProps = {\n  order: HttpTypes.StoreOrder\n}\n\nconst PaymentDetails = ({ order }: PaymentDetailsProps) => {\n  const payment = order.payment_collections?.[0].payments?.[0]\n\n  if (!payment) {\n    return (\n      <p className=\"text-grayscale-500\">No payment information available</p>\n    )\n  }\n\n  return (\n    <p className=\"text-grayscale-500\">\n      {paymentInfoMap[payment.provider_id].title}\n      <br />\n      {isStripe(payment.provider_id) && payment.data?.card_last4\n        ? `**** **** **** ${payment.data.card_last4}`\n        : `${convertToLocale({\n            amount: payment.amount,\n            currency_code: order.currency_code,\n          })} paid at ${new Date(payment.created_at ?? \"\").toLocaleString()}`}\n    </p>\n  )\n}\n\nexport default PaymentDetails\n"
  },
  {
    "path": "storefront/src/modules/order/templates/order-completed-template.tsx",
    "content": "import { HttpTypes } from \"@medusajs/types\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { LocalizedButtonLink } from \"@/components/LocalizedLink\"\nimport { Icon } from \"@/components/Icon\"\nimport Item from \"@modules/order/components/item\"\nimport { OrderTotals } from \"@modules/order/components/OrderTotals\"\nimport { listOrders } from \"@lib/data/orders\"\nimport { getCustomer } from \"@lib/data/customer\"\n\ntype OrderCompletedTemplateProps = {\n  order: HttpTypes.StoreOrder\n}\n\nexport default async function OrderCompletedTemplate({\n  order,\n}: OrderCompletedTemplateProps) {\n  const customer = await getCustomer()\n  let matchingOrders = []\n\n  if (customer) {\n    const { orders } = await listOrders()\n    matchingOrders = orders?.filter((o) => o.id === order?.id)\n  }\n\n  return (\n    <Layout className=\"py-26 md:pt-39 md:pb-36\">\n      <LayoutColumn\n        start={{ base: 1, lg: 3, xl: 4 }}\n        end={{ base: 13, lg: 11, xl: 10 }}\n      >\n        <h1 className=\"text-md md:text-2xl mb-8 md:mb-16\">\n          Thank you for your order!\n        </h1>\n        <p className=\"mb-4\">\n          We are pleased to confirm that your order has been successfully placed\n          and will be processed shortly.\n        </p>\n        <p className=\"mb-8\">\n          We have sent you the receipt and order details via{\" \"}\n          <strong>e-mail</strong>.<br />\n          Your order number is <strong>#{order.display_id}</strong>.\n        </p>\n        <div className=\"flex gap-x-6 gap-y-4 max-sm:flex-col mb-16\">\n          {Boolean(matchingOrders.length) && (\n            <LocalizedButtonLink href={`/account/my-orders/${order.id}`}>\n              Check order details\n            </LocalizedButtonLink>\n          )}\n          <LocalizedButtonLink href=\"/\" variant=\"outline\">\n            Back to home\n          </LocalizedButtonLink>\n        </div>\n        <div className=\"flex max-sm:flex-col gap-x-4 gap-y-4 md:flex-col lg:flex-row mb-5\">\n          <div className=\"flex-1 overflow-hidden rounded-xs border border-grayscale-200 p-4\">\n            <div className=\"flex gap-4 items-center mb-8\">\n              <Icon name=\"map-pin\" />\n              <p className=\"text-grayscale-500\">Shipping address</p>\n            </div>\n            <p>\n              {[\n                order.shipping_address?.first_name,\n                order.shipping_address?.last_name,\n              ]\n                .filter(Boolean)\n                .join(\" \")}\n              <br />\n              {[\n                order.shipping_address?.address_1,\n                [\n                  order.shipping_address?.postal_code,\n                  order.shipping_address?.city,\n                ]\n                  .filter(Boolean)\n                  .join(\" \"),\n                order.shipping_address?.country?.display_name,\n              ]\n                .filter(Boolean)\n                .join(\", \")}\n              <br />\n              {order.shipping_address?.phone}\n            </p>\n          </div>\n          <div className=\"flex-1 overflow-hidden rounded-xs border border-grayscale-200 p-4\">\n            <div className=\"flex gap-4 items-center mb-8\">\n              <Icon name=\"receipt\" />\n              <p className=\"text-grayscale-500\">Billing address</p>\n            </div>\n            <p>\n              {[\n                order.billing_address?.first_name,\n                order.billing_address?.last_name,\n              ]\n                .filter(Boolean)\n                .join(\" \")}\n              <br />\n              {[\n                order.billing_address?.address_1,\n                [\n                  order.billing_address?.postal_code,\n                  order.billing_address?.city,\n                ]\n                  .filter(Boolean)\n                  .join(\" \"),\n                order.billing_address?.country?.display_name,\n              ]\n                .filter(Boolean)\n                .join(\", \")}\n              <br />\n              {order.billing_address?.phone}\n            </p>\n          </div>\n        </div>\n        <div className=\"rounded-xs border border-grayscale-200 p-4 mb-5\">\n          {order.items?.map((item) => <Item key={item.id} item={item} />)}\n        </div>\n        <div className=\"rounded-xs border border-grayscale-200 p-4 flex max-sm:flex-col gap-y-8 gap-x-10 md:flex-wrap justify-between\">\n          <div className=\"flex items-center self-baseline gap-4\">\n            <Icon name=\"credit-card\" />\n            <div>\n              <p className=\"text-grayscale-500\">Payment</p>\n            </div>\n          </div>\n          <OrderTotals order={order} />\n        </div>\n      </LayoutColumn>\n    </Layout>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/products/components/image-gallery/index.tsx",
    "content": "import { ProductPageGallery } from \"@/components/ProductPageGallery\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport Image from \"next/image\"\n\ntype ImageGalleryProps = {\n  images: HttpTypes.StoreProductImage[]\n  className?: string\n}\n\nconst ImageGallery = ({ images, className }: ImageGalleryProps) => {\n  const filteredImages = images.filter((image) => Boolean(image.url))\n\n  if (!filteredImages.length) {\n    return null\n  }\n\n  return (\n    <ProductPageGallery className={className}>\n      {filteredImages.map((image, index) => (\n        <div\n          key={image.id}\n          className=\"relative aspect-[3/4] w-full overflow-hidden\"\n        >\n          <Image\n            key={image.id}\n            src={image.url}\n            priority={index <= 2 ? true : false}\n            alt={`Product image ${index + 1}`}\n            fill\n            sizes=\"(max-width: 768px) 100vw, (max-width: 1024px) 589px, (max-width: 1279px) 384px, 456px\"\n            className=\"object-cover\"\n          />\n        </div>\n      ))}\n    </ProductPageGallery>\n  )\n}\n\nexport default ImageGallery\n"
  },
  {
    "path": "storefront/src/modules/products/components/product-actions/index.tsx",
    "content": "\"use client\"\n\nimport { isEqual } from \"lodash\"\nimport { useEffect, useMemo, useState } from \"react\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport * as ReactAria from \"react-aria-components\"\nimport { useSearchParams } from \"next/navigation\"\nimport { getVariantItemsInStock } from \"@lib/util/inventory\"\nimport { Button } from \"@/components/Button\"\nimport { InputNumberField } from \"@/components/InputNumberField\"\nimport {\n  UiSelectButton,\n  UiSelectIcon,\n  UiSelectListBox,\n  UiSelectListBoxItem,\n  UiSelectValue,\n} from \"@/components/ui/Select\"\nimport { useCountryCode } from \"hooks/country-code\"\nimport ProductPrice from \"@modules/products/components/product-price\"\nimport { UiRadioGroup } from \"@/components/ui/Radio\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\nimport { useAddLineItem } from \"hooks/cart\"\n\ntype ProductActionsProps = {\n  product: HttpTypes.StoreProduct\n  materials: {\n    id: string\n    name: string\n    colors: {\n      id: string\n      name: string\n      hex_code: string\n    }[]\n  }[]\n  region: HttpTypes.StoreRegion\n  disabled?: boolean\n}\n\nconst optionsAsKeymap = (\n  variantOptions: HttpTypes.StoreProductVariant[\"options\"]\n) => {\n  return variantOptions?.reduce((acc: Record<string, string>, varopt) => {\n    if (varopt.option_id) {\n      acc[varopt.option_id] = varopt.value\n    }\n    return acc\n  }, {})\n}\n\nconst priorityOptions = [\"Material\", \"Color\", \"Size\"]\n\nconst normalizeOptionKey = (key: string) =>\n  key.trim().toLowerCase().replace(/\\s+/g, \"_\")\n\nconst getInitialOptions = (product: ProductActionsProps[\"product\"]) => {\n  if (product.variants?.length === 1) {\n    const variantOptions = optionsAsKeymap(product.variants[0].options)\n    return variantOptions ?? {}\n  }\n\n  if (product.options) {\n    const singleOptionValues = product.options\n      .filter((option) => option.values)\n      .filter((option) => option.values!.length === 1)\n      .reduce(\n        (acc, option) => {\n          acc[option.id] = option.values![0].value\n          return acc\n        },\n        {} as Record<string, string>\n      )\n\n    return singleOptionValues\n  }\n\n  return null\n}\n\nfunction ProductActions({ product, materials, disabled }: ProductActionsProps) {\n  const searchParams = useSearchParams()\n  const [options, setOptions] = useState<Record<string, string | undefined>>(\n    getInitialOptions(product) ?? {}\n  )\n  const [quantity, setQuantity] = useState(1)\n  const countryCode = useCountryCode()\n\n  const { mutateAsync, isPending } = useAddLineItem()\n\n  // If there is only 1 variant, preselect the options\n  useEffect(() => {\n    const initialOptions = getInitialOptions(product)\n    if (initialOptions) {\n      setOptions(initialOptions)\n    }\n  }, [product])\n\n  useEffect(() => {\n    const optionEntries = Array.from(searchParams.entries()).filter(([key]) =>\n      key.startsWith(\"mcp_opt_\")\n    )\n\n    if (!optionEntries.length || !product.options?.length) {\n      return\n    }\n\n    const requestedValues = optionEntries.reduce(\n      (acc, [key, value]) => {\n        const normalizedKey = normalizeOptionKey(key.replace(/^mcp_opt_/, \"\"))\n        acc[normalizedKey] = value\n        return acc\n      },\n      {} as Record<string, string>\n    )\n\n    const mappedOptions = (product.options ?? []).reduce(\n      (acc, option) => {\n        const optionIdKey = normalizeOptionKey(option.id)\n        const optionTitleKey = normalizeOptionKey(option.title ?? \"\")\n        const selectedValue =\n          requestedValues[optionIdKey] ?? requestedValues[optionTitleKey]\n\n        if (!selectedValue) {\n          return acc\n        }\n\n        const allowedValues = new Set(\n          (option.values ?? []).map((value) => value.value)\n        )\n\n        if (allowedValues.size && !allowedValues.has(selectedValue)) {\n          return acc\n        }\n\n        acc[option.id] = selectedValue\n        return acc\n      },\n      {} as Record<string, string>\n    )\n\n    if (!Object.keys(mappedOptions).length) {\n      return\n    }\n\n    setOptions((prev) => ({\n      ...prev,\n      ...mappedOptions,\n    }))\n  }, [searchParams, product.options])\n\n  const selectedVariant = useMemo(() => {\n    if (!product.variants || product.variants.length === 0) {\n      return\n    }\n\n    return product.variants.find((v) => {\n      const variantOptions = optionsAsKeymap(v.options)\n      return isEqual(variantOptions, options)\n    })\n  }, [product.variants, options])\n\n  // update the options when a variant is selected\n  const setOptionValue = (optionId: string, value: string) => {\n    setOptions((prev) => ({\n      ...prev,\n      [optionId]: value,\n    }))\n  }\n\n  // check if the selected variant is in stock\n  const itemsInStock = selectedVariant\n    ? getVariantItemsInStock(selectedVariant)\n    : 0\n\n  // add the selected variant to the cart\n  const handleAddToCart = async () => {\n    if (!selectedVariant?.id) return null\n\n    await mutateAsync({\n      variantId: selectedVariant.id,\n      quantity,\n      countryCode,\n    })\n  }\n\n  const hasMultipleVariants = (product.variants?.length ?? 0) > 1\n  const productOptions = (product.options || []).sort((a, b) => {\n    let aPriority = priorityOptions.indexOf(a.title ?? \"\")\n    let bPriority = priorityOptions.indexOf(b.title ?? \"\")\n\n    if (aPriority === -1) {\n      aPriority = priorityOptions.length\n    }\n\n    if (bPriority === -1) {\n      bPriority = priorityOptions.length\n    }\n\n    return aPriority - bPriority\n  })\n\n  const materialOption = productOptions.find((o) => o.title === \"Material\")\n  const colorOption = productOptions.find((o) => o.title === \"Color\")\n  const otherOptions =\n    materialOption && colorOption\n      ? productOptions.filter(\n          (o) => o.id !== materialOption.id && o.id !== colorOption.id\n        )\n      : productOptions\n\n  const selectedMaterial =\n    materialOption && options[materialOption.id]\n      ? materials.find((m) => m.name === options[materialOption.id])\n      : undefined\n\n  const showOtherOptions =\n    !materialOption ||\n    !colorOption ||\n    (selectedMaterial &&\n      (selectedMaterial.colors.length < 2 || options[colorOption.id]))\n\n  return (\n    <>\n      <ProductPrice product={product} variant={selectedVariant} />\n      <div className=\"max-md:text-xs mb-8 md:mb-16 max-w-120\">\n        <p>{product.description}</p>\n      </div>\n      {hasMultipleVariants && (\n        <div className=\"flex flex-col gap-8 md:gap-6 mb-4 md:mb-26\">\n          {materialOption && colorOption && (\n            <>\n              <div>\n                <p className=\"mb-4\">\n                  Materials\n                  {options[materialOption.id] && (\n                    <span className=\"text-grayscale-500 ml-6\">\n                      {options[materialOption.id]}\n                    </span>\n                  )}\n                </p>\n                <ReactAria.Select\n                  selectedKey={options[materialOption.id] ?? null}\n                  onSelectionChange={(value) => {\n                    setOptions({ [materialOption.id]: `${value}` })\n                  }}\n                  placeholder=\"Choose material\"\n                  className=\"w-full md:w-60\"\n                  isDisabled={!!disabled || isPending}\n                  aria-label=\"Material\"\n                >\n                  <UiSelectButton className=\"!h-12 px-4 gap-2 max-md:text-base\">\n                    <UiSelectValue />\n                    <UiSelectIcon className=\"h-6 w-6\" />\n                  </UiSelectButton>\n                  <ReactAria.Popover className=\"w-[--trigger-width]\">\n                    <UiSelectListBox>\n                      {materials.map((material) => (\n                        <UiSelectListBoxItem\n                          key={material.id}\n                          id={material.name}\n                        >\n                          {material.name}\n                        </UiSelectListBoxItem>\n                      ))}\n                    </UiSelectListBox>\n                  </ReactAria.Popover>\n                </ReactAria.Select>\n              </div>\n              {selectedMaterial && (\n                <div className=\"mb-6\">\n                  <p className=\"mb-4\">\n                    Colors\n                    <span className=\"text-grayscale-500 ml-6\">\n                      {options[colorOption.id]}\n                    </span>\n                  </p>\n                  <UiRadioGroup\n                    value={options[colorOption.id] ?? null}\n                    onChange={(value) => {\n                      setOptionValue(colorOption.id, value)\n                    }}\n                    aria-label=\"Color\"\n                    className=\"flex gap-6\"\n                    isDisabled={!!disabled || isPending}\n                  >\n                    {selectedMaterial.colors.map((color) => (\n                      <ReactAria.Radio\n                        key={color.id}\n                        value={color.name}\n                        aria-label={color.name}\n                        className=\"h-8 w-8 cursor-pointer relative before:transition-colors before:absolute before:content-[''] before:-bottom-2 before:left-0 before:w-full before:h-px data-[selected]:before:bg-black shadow-sm hover:shadow\"\n                        style={{ background: color.hex_code }}\n                      />\n                    ))}\n                  </UiRadioGroup>\n                </div>\n              )}\n            </>\n          )}\n          {showOtherOptions &&\n            otherOptions.map((option) => {\n              return (\n                <div key={option.id}>\n                  <p className=\"mb-4\">\n                    {option.title}\n                    {options[option.id] && (\n                      <span className=\"text-grayscale-500 ml-6\">\n                        {options[option.id]}\n                      </span>\n                    )}\n                  </p>\n                  <ReactAria.Select\n                    selectedKey={options[option.id] ?? null}\n                    onSelectionChange={(value) => {\n                      setOptionValue(option.id, `${value}`)\n                    }}\n                    placeholder={`Choose ${option.title.toLowerCase()}`}\n                    className=\"w-full md:w-60\"\n                    isDisabled={!!disabled || isPending}\n                    aria-label={option.title}\n                  >\n                    <UiSelectButton className=\"!h-12 px-4 gap-2 max-md:text-base\">\n                      <UiSelectValue />\n                      <UiSelectIcon className=\"h-6 w-6\" />\n                    </UiSelectButton>\n                    <ReactAria.Popover className=\"w-[--trigger-width]\">\n                      <UiSelectListBox>\n                        {(option.values ?? [])\n                          .filter((value) => Boolean(value.value))\n                          .map((value) => (\n                            <UiSelectListBoxItem\n                              key={value.id}\n                              id={value.value}\n                            >\n                              {value.value}\n                            </UiSelectListBoxItem>\n                          ))}\n                      </UiSelectListBox>\n                    </ReactAria.Popover>\n                  </ReactAria.Select>\n                </div>\n              )\n            })}\n        </div>\n      )}\n      <div className=\"flex max-sm:flex-col gap-4\">\n        <InputNumberField\n          isDisabled={\n            !itemsInStock || !selectedVariant || !!disabled || isPending\n          }\n          value={quantity}\n          onChange={setQuantity}\n          minValue={1}\n          maxValue={itemsInStock}\n          className=\"w-full sm:w-35 max-md:justify-center max-md:gap-2\"\n          aria-label=\"Quantity\"\n        />\n        <Button\n          onPress={handleAddToCart}\n          isDisabled={!itemsInStock || !selectedVariant || !!disabled}\n          isLoading={isPending}\n          className=\"sm:flex-1\"\n        >\n          {!selectedVariant\n            ? \"Select variant\"\n            : !itemsInStock\n              ? \"Out of stock\"\n              : \"Add to cart\"}\n        </Button>\n      </div>\n    </>\n  )\n}\n\nexport default withReactQueryProvider(ProductActions)\n"
  },
  {
    "path": "storefront/src/modules/products/components/product-preview/index.tsx",
    "content": "import { HttpTypes } from \"@medusajs/types\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport Thumbnail from \"@modules/products/components/thumbnail\"\nimport { getProductPrice } from \"@lib/util/get-product-price\"\n\nexport default function ProductPreview({\n  product,\n}: {\n  product: HttpTypes.StoreProduct\n}) {\n  const { cheapestPrice } = getProductPrice({\n    product,\n  })\n\n  const hasReducedPrice =\n    cheapestPrice &&\n    cheapestPrice.calculated_price_number <\n      (cheapestPrice?.original_price_number || 0)\n\n  return (\n    <LocalizedLink href={`/products/${product.handle}`}>\n      <Thumbnail\n        thumbnail={product.thumbnail}\n        images={product.images}\n        size=\"square\"\n        className=\"mb-4 md:mb-6\"\n      />\n      <div className=\"flex justify-between max-md:flex-col\">\n        <div className=\"max-md:text-xs\">\n          <p className=\"mb-1\">{product.title}</p>\n          {product.collection && (\n            <p className=\"text-grayscale-500 text-xs max-md:hidden\">\n              {product.collection.title}\n            </p>\n          )}\n        </div>\n        {cheapestPrice ? (\n          hasReducedPrice ? (\n            <div>\n              <p className=\"font-semibold max-md:text-xs text-red-primary\">\n                {cheapestPrice.calculated_price}\n              </p>\n              <p className=\"max-md:text-xs text-grayscale-500 line-through\">\n                {cheapestPrice.original_price}\n              </p>\n            </div>\n          ) : (\n            <div>\n              <p className=\"font-semibold max-md:text-xs\">\n                {cheapestPrice.calculated_price}\n              </p>\n            </div>\n          )\n        ) : null}\n      </div>\n    </LocalizedLink>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/products/components/product-price/index.tsx",
    "content": "import { getProductPrice } from \"@lib/util/get-product-price\"\nimport { HttpTypes } from \"@medusajs/types\"\n\nexport default function ProductPrice({\n  product,\n  variant,\n}: {\n  product: HttpTypes.StoreProduct\n  variant?: HttpTypes.StoreProductVariant\n}) {\n  const { cheapestPrice, variantPrice } = getProductPrice({\n    product,\n    variantId: variant?.id,\n  })\n\n  const selectedPrice = variant ? variantPrice : cheapestPrice\n\n  if (!selectedPrice) {\n    return <div className=\"block w-32 h-9 bg-grayscale-50 animate-pulse\" />\n  }\n\n  const hasReducedPrice =\n    selectedPrice.calculated_price_number <\n    (selectedPrice.original_price_number ?? 0)\n\n  if (hasReducedPrice && variant) {\n    return (\n      <div>\n        <p className=\"text-sm mb-1 text-grayscale-500 line-through\">\n          {selectedPrice.original_price}\n        </p>\n        <p className=\"text-md mb-8 text-red-primary\">\n          {selectedPrice.calculated_price}\n        </p>\n      </div>\n    )\n  }\n\n  return (\n    <>\n      <p className=\"text-md mb-8\">\n        {!variant && \"From \"}\n        {selectedPrice.calculated_price}\n      </p>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/products/components/related-products/index.tsx",
    "content": "import Product from \"@modules/products/components/product-preview\"\nimport { getRegion } from \"@lib/data/regions\"\nimport { getProductsList } from \"@lib/data/products\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\n\ntype RelatedProductsProps = {\n  product: HttpTypes.StoreProduct\n  countryCode: string\n}\n\nexport default async function RelatedProducts({\n  product,\n  countryCode,\n}: RelatedProductsProps) {\n  const region = await getRegion(countryCode)\n\n  if (!region) {\n    return null\n  }\n\n  // edit this function to define your related products logic\n  const queryParams: HttpTypes.StoreProductListParams = {\n    limit: 3,\n  }\n  if (region?.id) {\n    queryParams.region_id = region.id\n  }\n  if (product.collection_id) {\n    queryParams.collection_id = [product.collection_id]\n  }\n  if (product.tags) {\n    queryParams.tag_id = product.tags.map((t) => t.value).filter(Boolean)\n  }\n  queryParams.is_giftcard = false\n\n  const products = await getProductsList({\n    queryParams,\n    countryCode,\n  }).then(({ response }) => {\n    return response.products.filter(\n      (responseProduct) => responseProduct.id !== product.id\n    )\n  })\n\n  if (!products.length) {\n    return null\n  }\n\n  return (\n    <>\n      <Layout>\n        <LayoutColumn className=\"mt-26 md:mt-36\">\n          <h4 className=\"text-md md:text-2xl mb-8 md:mb-16\">\n            Related products\n          </h4>\n        </LayoutColumn>\n      </Layout>\n      <Layout className=\"gap-y-10 md:gap-y-16\">\n        {products.map((product) => (\n          <LayoutColumn key={product.id} className=\"!col-span-6 md:!col-span-4\">\n            <Product product={product} />\n          </LayoutColumn>\n        ))}\n      </Layout>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/products/components/thumbnail/index.tsx",
    "content": "import * as React from \"react\"\nimport Image from \"next/image\"\nimport type { HttpTypes } from \"@medusajs/types\"\nimport { twMerge } from \"tailwind-merge\"\n\nimport PlaceholderImage from \"@modules/common/icons/placeholder-image\"\n\ntype ThumbnailProps = {\n  thumbnail?: HttpTypes.StoreProduct[\"thumbnail\"]\n  images?: HttpTypes.StoreProduct[\"images\"]\n  size?: \"small\" | \"medium\" | \"large\" | \"full\" | \"square\" | \"3/4\"\n  isFeatured?: boolean\n  className?: string\n  \"data-testid\"?: string\n}\n\nconst Thumbnail: React.FC<ThumbnailProps> = ({\n  thumbnail,\n  images,\n  size = \"small\",\n  isFeatured,\n  className,\n  \"data-testid\": dataTestid,\n}) => {\n  const initialImage = thumbnail || images?.[0]?.url\n\n  return (\n    <div\n      className={twMerge(\n        \"relative w-full overflow-hidden\",\n        className,\n        isFeatured && \"aspect-[11/14]\",\n        !isFeatured && size !== \"square\" && size !== \"3/4\" && \"aspect-[9/16]\",\n        size === \"square\" && \"aspect-[1/1]\",\n        size === \"3/4\" && \"aspect-[3/4]\",\n        size === \"small\" && \"w-[180px]\",\n        size === \"medium\" && \"w-[290px]\",\n        size === \"large\" && \"w-[440px]\",\n        size === \"full\" && \"w-full\"\n      )}\n      data-testid={dataTestid}\n    >\n      <ImageOrPlaceholder image={initialImage} size={size} />\n    </div>\n  )\n}\n\nconst ImageOrPlaceholder = ({\n  image,\n  size,\n}: Pick<ThumbnailProps, \"size\"> & { image?: string }) => {\n  return image ? (\n    <Image\n      src={image}\n      alt=\"Thumbnail\"\n      className=\"absolute inset-0 object-cover object-center\"\n      draggable={false}\n      quality={50}\n      sizes=\"(max-width: 576px) 280px, (max-width: 768px) 360px, (max-width: 992px) 480px, 800px\"\n      fill\n    />\n  ) : (\n    <div className=\"w-full h-full absolute inset-0 flex items-center justify-center\">\n      <PlaceholderImage size={size === \"small\" ? 16 : 24} />\n    </div>\n  )\n}\n\nexport default Thumbnail\n"
  },
  {
    "path": "storefront/src/modules/products/templates/index.tsx",
    "content": "import React, { Suspense } from \"react\"\nimport { notFound } from \"next/navigation\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport Image from \"next/image\"\n\nimport { collectionMetadataCustomFieldsSchema } from \"@lib/util/collections\"\nimport ImageGallery from \"@modules/products/components/image-gallery\"\nimport ProductActions from \"@modules/products/components/product-actions\"\nimport RelatedProducts from \"@modules/products/components/related-products\"\nimport ProductInfo from \"@modules/products/templates/product-info\"\nimport SkeletonRelatedProducts from \"@modules/skeletons/templates/skeleton-related-products\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\n\ntype ProductTemplateProps = {\n  product: HttpTypes.StoreProduct\n  materials: {\n    id: string\n    name: string\n    colors: {\n      id: string\n      name: string\n      hex_code: string\n    }[]\n  }[]\n  region: HttpTypes.StoreRegion\n  countryCode: string\n}\n\nconst ProductTemplate: React.FC<ProductTemplateProps> = ({\n  product,\n  materials,\n  region,\n  countryCode,\n}) => {\n  if (!product || !product.id) {\n    return notFound()\n  }\n\n  const images = product.images || []\n  const hasImages = Boolean(\n    product.images &&\n    product.images.filter((image) => Boolean(image.url)).length > 0\n  )\n\n  const collectionDetails = collectionMetadataCustomFieldsSchema.safeParse(\n    product.collection?.metadata ?? {}\n  )\n\n  return (\n    <div\n      className=\"pt-18 md:pt-26 lg:pt-37 pb-26 md:pb-36\"\n      data-testid=\"product-container\"\n    >\n      <ImageGallery className=\"md:hidden\" images={images} />\n      <Layout>\n        <LayoutColumn className=\"mb-26 md:mb-52\">\n          <div className=\"flex max-lg:flex-col gap-8 xl:gap-27\">\n            {hasImages && (\n              <div className=\"lg:w-1/2 flex flex-1 flex-col gap-8\">\n                <ImageGallery className=\"max-md:hidden\" images={images} />\n              </div>\n            )}\n            <div className=\"sticky flex-1 top-0\">\n              <ProductInfo product={product} />\n              <Suspense>\n                <ProductActions\n                  product={product}\n                  materials={materials}\n                  region={region}\n                />\n              </Suspense>\n            </div>\n            {!hasImages && <div className=\"flex-1\" />}\n          </div>\n        </LayoutColumn>\n      </Layout>\n      {collectionDetails.success &&\n        ((typeof collectionDetails.data.product_page_heading === \"string\" &&\n          collectionDetails.data.product_page_heading.length > 0) ||\n          typeof collectionDetails.data.product_page_image?.url ===\n            \"string\") && (\n          <Layout>\n            <LayoutColumn>\n              {typeof collectionDetails.data.product_page_heading ===\n                \"string\" &&\n                collectionDetails.data.product_page_heading.length > 0 && (\n                  <h2 className=\"text-md md:text-2xl mb-8\">\n                    {collectionDetails.data.product_page_heading}\n                  </h2>\n                )}\n              {typeof collectionDetails.data.product_page_image?.url ===\n                \"string\" && (\n                <div className=\"relative mb-8 md:mb-20 aspect-[3/2]\">\n                  <Image\n                    src={collectionDetails.data.product_page_image.url}\n                    alt=\"Collection product page image\"\n                    fill\n                    className=\"object-cover\"\n                  />\n                </div>\n              )}\n            </LayoutColumn>\n          </Layout>\n        )}\n      {collectionDetails.success &&\n        collectionDetails.data.product_page_wide_image &&\n        typeof collectionDetails.data.product_page_wide_image.url ===\n          \"string\" && (\n          <div className=\"relative mb-8 md:mb-20 aspect-[3/2] md:aspect-[7/3]\">\n            <Image\n              src={collectionDetails.data.product_page_wide_image.url}\n              alt=\"Collection product page wide image\"\n              fill\n              className=\"object-cover\"\n            />\n          </div>\n        )}\n      {collectionDetails.success &&\n        (typeof collectionDetails.data.product_page_cta_image?.url ===\n          \"string\" ||\n          (typeof collectionDetails.data.product_page_cta_heading ===\n            \"string\" &&\n            collectionDetails.data.product_page_cta_heading.length > 0) ||\n          (typeof collectionDetails.data.product_page_cta_link === \"string\" &&\n            collectionDetails.data.product_page_cta_link.length > 0)) && (\n          <Layout>\n            {typeof collectionDetails.data.product_page_cta_image?.url ===\n              \"string\" && (\n              <LayoutColumn start={1} end={{ base: 10, md: 6 }}>\n                <div className=\"relative aspect-[3/4]\">\n                  <Image\n                    src={collectionDetails.data.product_page_cta_image.url}\n                    fill\n                    alt=\"Collection product page CTA image\"\n                  />\n                </div>\n              </LayoutColumn>\n            )}\n            {((typeof collectionDetails.data.product_page_cta_heading ===\n              \"string\" &&\n              collectionDetails.data.product_page_cta_heading.length > 0) ||\n              (typeof collectionDetails.data.product_page_cta_link ===\n                \"string\" &&\n                collectionDetails.data.product_page_cta_link.length > 0)) && (\n              <LayoutColumn start={{ base: 1, md: 7 }} end={13}>\n                {typeof collectionDetails.data.product_page_cta_heading ===\n                  \"string\" &&\n                  collectionDetails.data.product_page_cta_heading.length >\n                    0 && (\n                    <h3 className=\"text-md md:text-2xl my-8 md:mt-20\">\n                      {collectionDetails.data.product_page_cta_heading}\n                    </h3>\n                  )}\n                {typeof collectionDetails.data.product_page_cta_link ===\n                  \"string\" &&\n                  collectionDetails.data.product_page_cta_link.length > 0 &&\n                  typeof product.collection?.handle === \"string\" && (\n                    <p className=\"text-base md:text-md\">\n                      <LocalizedLink\n                        href={`/collections/${product.collection.handle}`}\n                        variant=\"underline\"\n                      >\n                        {collectionDetails.data.product_page_cta_link}\n                      </LocalizedLink>\n                    </p>\n                  )}\n              </LayoutColumn>\n            )}\n          </Layout>\n        )}\n\n      <Suspense fallback={<SkeletonRelatedProducts />}>\n        <RelatedProducts product={product} countryCode={countryCode} />\n      </Suspense>\n    </div>\n  )\n}\n\nexport default ProductTemplate\n"
  },
  {
    "path": "storefront/src/modules/products/templates/product-actions-wrapper/index.tsx",
    "content": "import { getProductsById } from \"@lib/data/products\"\nimport { HttpTypes } from \"@medusajs/types\"\nimport ProductActions from \"@modules/products/components/product-actions\"\n\n/**\n * Fetches real time pricing for a product and renders the product actions component.\n */\nexport default async function ProductActionsWrapper({\n  id,\n  materials,\n  region,\n}: {\n  id: string\n  materials: {\n    id: string\n    name: string\n    colors: {\n      id: string\n      name: string\n      hex_code: string\n    }[]\n  }[]\n  region: HttpTypes.StoreRegion\n}) {\n  const [product] = await getProductsById({\n    ids: [id],\n    regionId: region.id,\n  })\n\n  if (!product) {\n    return null\n  }\n\n  return (\n    <ProductActions product={product} materials={materials} region={region} />\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/products/templates/product-info/index.tsx",
    "content": "import { HttpTypes } from \"@medusajs/types\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\n\ntype ProductInfoProps = {\n  product: HttpTypes.StoreProduct\n}\n\nconst ProductInfo = ({ product }: ProductInfoProps) => {\n  return (\n    <>\n      {product.collection && (\n        <LocalizedLink\n          href={`/collections/${product.collection.handle}`}\n          className=\"text-medium text-fg-muted dark:text-fg-muted-dark hover:text-fg-subtle dark:hover:text-fg-subtle-dark\"\n        >\n          <p className=\"text-grayscale-500 mb-2\">{product.collection.title}</p>\n        </LocalizedLink>\n      )}\n      <h2 className=\"text-md md:text-xl mb-2\">{product.title}</h2>\n    </>\n  )\n}\n\nexport default ProductInfo\n"
  },
  {
    "path": "storefront/src/modules/skeletons/components/skeleton-button/index.tsx",
    "content": "import { twMerge } from \"tailwind-merge\"\nimport { Skeleton } from \"@/components/ui/Skeleton\"\n\nconst SkeletonButton: React.FC<{ className?: string }> = ({ className }) => {\n  return <Skeleton className={twMerge(\"w-30 h-12\", className)} />\n}\n\nexport default SkeletonButton\n"
  },
  {
    "path": "storefront/src/modules/skeletons/components/skeleton-cart-item/index.tsx",
    "content": "import { Skeleton } from \"@/components/ui/Skeleton\"\n\nconst SkeletonCartItem = () => {\n  return (\n    <div className=\"flex gap-6 border-b border-grayscale-100 py-8 lg:last:pb-0 lg:last:border-b-0\">\n      <Skeleton className=\"w-25 sm:w-30 aspect-[3/4]\" />\n      <div className=\"flex-grow flex flex-col justify-between\">\n        <div className=\"flex gap-1 flex-col\">\n          <Skeleton className=\"h-7 md:h-6 w-34 md:w-39\" />\n          <Skeleton className=\"h-5 md:h-5 w-24 md:w-32 max-sm:mb-2\" />\n          <Skeleton className=\"sm:hidden h-4 w-20\" />\n        </div>\n        <Skeleton className=\"w-25 h-8\" />\n      </div>\n      <div className=\"flex flex-col justify-between items-end\">\n        <Skeleton className=\"max-sm:hidden w-22 h-6\" />\n        <Skeleton className=\"h-6 w-6 md:h-8 md:w-8\" />\n      </div>\n    </div>\n  )\n}\n\nexport default SkeletonCartItem\n"
  },
  {
    "path": "storefront/src/modules/skeletons/components/skeleton-cart-totals/index.tsx",
    "content": "import { Skeleton } from \"@/components/ui/Skeleton\"\n\nconst SkeletonCartTotals = ({ header = true }) => {\n  return (\n    <div>\n      <div className=\"flex flex-col gap-4\">\n        {header && <Skeleton className=\"w-32 h-4\" />}\n        <div className=\"flex justify-between\">\n          <Skeleton className=\"w-25 h-6\" />\n          <Skeleton className=\"w-25 h-6\" />\n        </div>\n        <div className=\"flex justify-between\">\n          <Skeleton className=\"w-20 h-6\" />\n          <Skeleton className=\"w-25 h-6\" />\n        </div>\n        <div className=\"flex justify-between\">\n          <Skeleton className=\"w-15 h-6\" />\n          <Skeleton className=\"w-25 h-6\" />\n        </div>\n      </div>\n      <hr className=\"my-6 text-grayscale-200\" />\n      <div className=\"flex justify-between mb-11\">\n        <Skeleton className=\"w-20 h-8\" />\n        <Skeleton className=\"w-30 h-8\" />\n      </div>\n      <div className=\"flex justify-between gap-2\">\n        <Skeleton className=\"flex-1 lg:w-50 h-12\" />\n        <Skeleton className=\"w-22 h-12\" />\n      </div>\n    </div>\n  )\n}\n\nexport default SkeletonCartTotals\n"
  },
  {
    "path": "storefront/src/modules/skeletons/components/skeleton-mobile-summary-trigger/index.tsx",
    "content": "import { Skeleton } from \"@/components/ui/Skeleton\"\n\nexport const SkeletonMobileCheckoutSummaryTrigger = () => (\n  <div className=\"h-18 flex justify-between items-center w-full\">\n    <Skeleton colorScheme=\"white\" className=\"h-6 w-30\" />\n    <Skeleton colorScheme=\"white\" className=\"h-6 w-30\" />\n  </div>\n)\n\nexport default SkeletonMobileCheckoutSummaryTrigger\n"
  },
  {
    "path": "storefront/src/modules/skeletons/components/skeleton-order-summary/index.tsx",
    "content": "import SkeletonButton from \"@modules/skeletons/components/skeleton-button\"\nimport SkeletonCartTotals from \"@modules/skeletons/components/skeleton-cart-totals\"\n\nconst SkeletonOrderSummary = () => {\n  return (\n    <div className=\"grid-cols-1\">\n      <SkeletonCartTotals header={false} />\n      <div className=\"mt-6\">\n        <SkeletonButton className=\"w-full\" />\n      </div>\n    </div>\n  )\n}\n\nexport default SkeletonOrderSummary\n"
  },
  {
    "path": "storefront/src/modules/skeletons/components/skeleton-product-preview/index.tsx",
    "content": "import { Skeleton } from \"@/components/ui/Skeleton\"\n\nconst SkeletonProductPreview = () => {\n  return (\n    <div>\n      <Skeleton className=\"mb-4 md:mb-6 w-full aspect-square\" />\n      <div className=\"flex justify-between max-md:flex-col\">\n        <div>\n          <Skeleton className=\"mb-2.5 h-3 md:h-5 w-22\" />\n          <Skeleton className=\"max-md:hidden h-3 md:h-3 w-18\" />\n        </div>\n        <Skeleton className=\"h-3 md:h-6 w-18 md:w-22\" />\n      </div>\n    </div>\n  )\n}\n\nexport default SkeletonProductPreview\n"
  },
  {
    "path": "storefront/src/modules/skeletons/templates/skeleton-account-page/index.tsx",
    "content": "import { Skeleton } from \"@/components/ui/Skeleton\"\nimport SkeletonButton from \"@modules/skeletons/components/skeleton-button\"\n\nconst SkeletonAccountPage = () => {\n  return (\n    <>\n      <Skeleton className=\"h-11 w-75 mb-8 md:mb-16\" />\n      <Skeleton className=\"h-9 w-60 mb-6\" />\n      <div className=\"w-full border border-grayscale-200 rounded-xs p-4 flex flex-wrap gap-8 max-lg:flex-col lg:items-center mb-16\">\n        <div className=\"flex gap-8 flex-1\">\n          <Skeleton className=\"h-6 w-6 mt-2.5\" />\n          <div className=\"flex max-sm:flex-col sm:flex-wrap gap-6 sm:gap-x-16\">\n            <div>\n              <Skeleton className=\"h-4 w-14 mb-1.5\" />\n              <Skeleton className=\"h-6 w-22\" />\n            </div>\n            <div>\n              <Skeleton className=\"h-4 w-14 mb-1.5\" />\n              <Skeleton className=\"h-6 w-22\" />\n            </div>\n          </div>\n        </div>\n        <SkeletonButton className=\"w-full lg:w-27\" />\n      </div>\n      <Skeleton className=\"h-9 w-60 mb-6\" />\n      <div className=\"w-full border border-grayscale-200 rounded-xs p-4 flex flex-wrap gap-y-6 gap-x-8 items-center mb-4\">\n        <Skeleton className=\"h-6 w-6 mt-2.5\" />\n        <div>\n          <Skeleton className=\"h-4 w-14 mb-1.5\" />\n          <Skeleton className=\"h-6 w-40\" />\n        </div>\n      </div>\n    </>\n  )\n}\n\nexport default SkeletonAccountPage\n"
  },
  {
    "path": "storefront/src/modules/skeletons/templates/skeleton-cart-page/index.tsx",
    "content": "import repeat from \"@lib/util/repeat\"\nimport SkeletonCartItem from \"@modules/skeletons/components/skeleton-cart-item\"\nimport SkeletonOrderSummary from \"@modules/skeletons/components/skeleton-order-summary\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { Skeleton } from \"@/components/ui/Skeleton\"\n\nconst SkeletonCartPage = () => {\n  return (\n    <Layout className=\"py-26 md:pb-36 md:pt-39\">\n      <LayoutColumn\n        start={1}\n        end={{ base: 13, lg: 9, xl: 10 }}\n        className=\"mb-14 lg:mb-0\"\n      >\n        <div className=\"pb-8 md:pb-12 border-b border-b-grayscale-100\">\n          <Skeleton className=\"w-54 md:w-108 h-6 md:h-[4.1875rem]\" />\n        </div>\n        <div>\n          {repeat(3).map((index) => (\n            <SkeletonCartItem key={index} />\n          ))}\n        </div>\n      </LayoutColumn>\n      <LayoutColumn\n        start={{ base: 1, lg: 9, xl: 10 }}\n        end={13}\n        className=\"lg:pt-8\"\n      >\n        <SkeletonOrderSummary />\n      </LayoutColumn>\n    </Layout>\n  )\n}\n\nexport default SkeletonCartPage\n"
  },
  {
    "path": "storefront/src/modules/skeletons/templates/skeleton-checkout-summary/index.tsx",
    "content": "import { Skeleton } from \"@/components/ui/Skeleton\"\n\nexport default function SkeletonCheckoutSummary() {\n  return (\n    <>\n      <Skeleton colorScheme=\"white\" className=\"w-full h-6 mb-8 lg:mb-16\" />\n      <div className=\"flex gap-4 lg:gap-6 mb-8\">\n        <Skeleton colorScheme=\"white\" className=\"w-25 lg:w-33 aspect-[3/4]\" />\n        <div className=\"flex flex-col flex-1 justify-between\">\n          <div className=\"flex flex-wrap gap-x-4 gap-y-1 justify-between\">\n            <Skeleton colorScheme=\"white\" className=\"w-full h-6\" />\n          </div>\n          <div className=\"flex flex-col gap-1.5 max-lg:text-xs\">\n            <Skeleton colorScheme=\"white\" className=\"w-full h-6\" />\n            <Skeleton colorScheme=\"white\" className=\"w-full h-6\" />\n          </div>\n        </div>\n      </div>\n      <div className=\"flex max-sm:flex-col gap-x-6 gap-y-4 mb-8\">\n        <Skeleton colorScheme=\"white\" className=\"h-12 lg:h-14 flex-1\" />\n        <Skeleton\n          colorScheme=\"white\"\n          className=\"lg:h-14 flex-1 sm:max-w-23 h-12\"\n        />\n      </div>\n      <div className=\"flex flex-col gap-2 lg:gap-1 mb-8\">\n        <Skeleton colorScheme=\"white\" className=\"w-full h-6\" />\n        <Skeleton colorScheme=\"white\" className=\"w-full h-6\" />\n        <Skeleton colorScheme=\"white\" className=\"w-full h-6\" />\n      </div>\n      <div className=\"flex justify-between text-md\">\n        <Skeleton colorScheme=\"white\" className=\"w-full h-8\" />\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/skeletons/templates/skeleton-order-confirmed/index.tsx",
    "content": "import { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { Skeleton } from \"@/components/ui/Skeleton\"\nimport SkeletonButton from \"@modules/skeletons/components/skeleton-button\"\n\nconst SkeletonOrderConfirmed = () => {\n  return (\n    <Layout className=\"pt-39 pb-36\">\n      <LayoutColumn\n        start={{ base: 1, lg: 3, xl: 4 }}\n        end={{ base: 13, lg: 11, xl: 10 }}\n      >\n        <Skeleton className=\"w-full h-17 mb-6\" />\n        <Skeleton className=\"w-[90%] h-12 mb-4\" />\n        <Skeleton className=\"w-[80%] h-12 mb-4\" />\n        <div className=\"flex flex-col sm:flex-row mt-16 gap-8\">\n          <div className=\"flex-1\">\n            <Skeleton className=\"w-30 h-5 mb-2\" />\n            <Skeleton className=\"w-25 h-5 mb-2\" />\n            <Skeleton className=\"w-20 h-5 mb-2\" />\n            <Skeleton className=\"w-15 h-5\" />\n          </div>\n          <div className=\"flex-1\">\n            <Skeleton className=\"w-30 h-5 mb-2\" />\n            <Skeleton className=\"w-25 h-5 mb-2\" />\n            <Skeleton className=\"w-20 h-5 mb-2\" />\n            <Skeleton className=\"w-15 h-5\" />\n          </div>\n        </div>\n        <SkeletonButton className=\"w-full mt-16\" />\n      </LayoutColumn>\n    </Layout>\n  )\n}\n\nexport default SkeletonOrderConfirmed\n"
  },
  {
    "path": "storefront/src/modules/skeletons/templates/skeleton-product-grid/index.tsx",
    "content": "import { Layout, LayoutColumn } from \"@/components/Layout\"\nimport repeat from \"@lib/util/repeat\"\nimport SkeletonProductPreview from \"@modules/skeletons/components/skeleton-product-preview\"\n\nconst SkeletonProductGrid = () => {\n  return (\n    <Layout className=\"gap-y-10 md:gap-y-16 mb-16 md:mb-20\">\n      {repeat(9).map((index) => (\n        <LayoutColumn className=\"md:!col-span-4 !col-span-6\" key={index}>\n          <SkeletonProductPreview />\n        </LayoutColumn>\n      ))}\n    </Layout>\n  )\n}\n\nexport default SkeletonProductGrid\n"
  },
  {
    "path": "storefront/src/modules/skeletons/templates/skeleton-related-products/index.tsx",
    "content": "import repeat from \"@lib/util/repeat\"\nimport SkeletonProductPreview from \"@modules/skeletons/components/skeleton-product-preview\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\n\nconst SkeletonRelatedProducts = () => {\n  return (\n    <>\n      <Layout>\n        <LayoutColumn className=\"mt-26 md:mt-36\">\n          <h4 className=\"text-md md:text-2xl mb-8 md:mb-16\">\n            Related products\n          </h4>\n        </LayoutColumn>\n      </Layout>\n      <Layout className=\"gap-y-10 md:gap-y-16\">\n        {repeat(3).map((index) => (\n          <LayoutColumn className=\"!col-span-6 md:!col-span-4\" key={index}>\n            <SkeletonProductPreview />\n          </LayoutColumn>\n        ))}\n      </Layout>\n    </>\n  )\n}\n\nexport default SkeletonRelatedProducts\n"
  },
  {
    "path": "storefront/src/modules/store/components/collections-slider/index.tsx",
    "content": "import * as React from \"react\"\nimport Image from \"next/image\"\n\nimport { getCollectionsList } from \"@lib/data/collections\"\nimport { Carousel } from \"@/components/Carousel\"\nimport { LocalizedLink } from \"@/components/LocalizedLink\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport const CollectionsSlider: React.FC<{\n  heading?: React.ReactNode\n  className?: string\n}> = async ({ heading = \"Collections\", className }) => {\n  const collections = await getCollectionsList(0, 20, [\n    \"id\",\n    \"title\",\n    \"handle\",\n    \"metadata\",\n  ])\n\n  if (!collections || !collections.collections.length) {\n    return null\n  }\n\n  return (\n    <Carousel\n      heading={<h3 className=\"text-md md:text-2xl\">{heading}</h3>}\n      className={twMerge(\"mb-26 md:mb-36\", className)}\n    >\n      {collections.collections.map((c) => (\n        <div\n          key={c.id}\n          className=\"w-[70%] sm:w-[60%] lg:w-full max-w-72 flex-shrink-0\"\n        >\n          <LocalizedLink href={`/collections/${c.handle}`}>\n            {typeof c.metadata?.image === \"object\" &&\n              c.metadata.image &&\n              \"url\" in c.metadata.image &&\n              typeof c.metadata.image.url === \"string\" && (\n                <div className=\"relative mb-4 md:mb-6 w-full aspect-[3/4]\">\n                  <Image src={c.metadata.image.url} alt={c.title} fill />\n                </div>\n              )}\n            <h3>{c.title}</h3>\n          </LocalizedLink>\n        </div>\n      ))}\n    </Carousel>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/store/components/no-results.tsx/index.tsx",
    "content": "\"use client\"\n\nimport { LayoutColumn } from \"@/components/Layout\"\nimport { Link } from \"@/components/Link\"\nimport { usePathname } from \"next/navigation\"\n\nexport const NoResults = () => {\n  const pathname = usePathname()\n\n  return (\n    <LayoutColumn className=\"pt-28\">\n      <div className=\"flex justify-center flex-col items-center\">\n        <div>\n          <p className=\"text-md text-center mb-2\">No results match!</p>\n        </div>\n        <Link\n          scroll={false}\n          href={pathname}\n          variant=\"underline\"\n          className=\"inline-flex md:pb-0\"\n        >\n          Clear filters\n        </Link>\n      </div>\n    </LayoutColumn>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/store/components/pagination/index.tsx",
    "content": "\"use client\"\n\nimport { usePathname, useRouter, useSearchParams } from \"next/navigation\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function Pagination({\n  page,\n  totalPages,\n  \"data-testid\": dataTestid,\n}: {\n  page: number\n  totalPages: number\n  \"data-testid\"?: string\n}) {\n  const router = useRouter()\n  const pathname = usePathname()\n  const searchParams = useSearchParams()\n\n  // Helper function to generate an array of numbers within a range\n  const arrayRange = (start: number, stop: number) =>\n    Array.from({ length: stop - start + 1 }, (_, index) => start + index)\n\n  // Function to handle page changes\n  const handlePageChange = (newPage: number) => {\n    const params = new URLSearchParams(searchParams)\n    params.set(\"page\", newPage.toString())\n    router.push(`${pathname}?${params.toString()}`)\n  }\n\n  // Function to render a page button\n  const renderPageButton = (\n    p: number,\n    label: string | number,\n    isCurrent: boolean\n  ) => (\n    <button\n      key={p}\n      className={twMerge(\n        \"txt-xlarge-plus text-fg-muted dark:text-fg-muted-dark px-1\",\n        isCurrent &&\n          \"text-fg-base dark:text-fg-base-dark hover:text-fg-subtle dark:hover:text-fg-subtle-dark\"\n      )}\n      disabled={isCurrent}\n      onClick={() => handlePageChange(p)}\n    >\n      {label}\n    </button>\n  )\n\n  // Function to render ellipsis\n  const renderEllipsis = (key: string) => (\n    <span\n      key={key}\n      className=\"txt-xlarge-plus text-fg-muted dark:text-fg-muted-dark items-center cursor-default\"\n    >\n      ...\n    </span>\n  )\n\n  // Function to render page buttons based on the current page and total pages\n  const renderPageButtons = () => {\n    const buttons = []\n\n    if (totalPages <= 7) {\n      // Show all pages\n      buttons.push(\n        ...arrayRange(1, totalPages).map((p) =>\n          renderPageButton(p, p, p === page)\n        )\n      )\n    } else {\n      // Handle different cases for displaying pages and ellipses\n      if (page <= 4) {\n        // Show 1, 2, 3, 4, 5, ..., lastpage\n        buttons.push(\n          ...arrayRange(1, 5).map((p) => renderPageButton(p, p, p === page))\n        )\n        buttons.push(renderEllipsis(\"ellipsis1\"))\n        buttons.push(\n          renderPageButton(totalPages, totalPages, totalPages === page)\n        )\n      } else if (page >= totalPages - 3) {\n        // Show 1, ..., lastpage - 4, lastpage - 3, lastpage - 2, lastpage - 1, lastpage\n        buttons.push(renderPageButton(1, 1, 1 === page))\n        buttons.push(renderEllipsis(\"ellipsis2\"))\n        buttons.push(\n          ...arrayRange(totalPages - 4, totalPages).map((p) =>\n            renderPageButton(p, p, p === page)\n          )\n        )\n      } else {\n        // Show 1, ..., page - 1, page, page + 1, ..., lastpage\n        buttons.push(renderPageButton(1, 1, 1 === page))\n        buttons.push(renderEllipsis(\"ellipsis3\"))\n        buttons.push(\n          ...arrayRange(page - 1, page + 1).map((p) =>\n            renderPageButton(p, p, p === page)\n          )\n        )\n        buttons.push(renderEllipsis(\"ellipsis4\"))\n        buttons.push(\n          renderPageButton(totalPages, totalPages, totalPages === page)\n        )\n      }\n    }\n\n    return buttons\n  }\n\n  // Render the component\n  return (\n    <div className=\"flex justify-center w-full mt-12\">\n      <div className=\"flex gap-2 items-end\" data-testid={dataTestid}>\n        {renderPageButtons()}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/store/components/refinement-list/category-filter/index.tsx",
    "content": "\"use client\"\n\nimport * as ReactAria from \"react-aria-components\"\nimport {\n  UiSelectButton,\n  UiSelectDialog,\n  UiSelectIcon,\n} from \"@/components/ui/Select\"\nimport {\n  UiCheckbox,\n  UiCheckboxBox,\n  UiCheckboxIcon,\n  UiCheckboxLabel,\n} from \"@/components/ui/Checkbox\"\nimport { UiDialogTrigger } from \"@/components/Dialog\"\n\nexport const CategoryFilter: React.FC<{\n  categories: Record<string, string>\n  category?: string[]\n  setQueryParams: (name: string, value: string[]) => void\n}> = ({ category, categories, setQueryParams }) => (\n  <UiDialogTrigger>\n    <UiSelectButton className=\"w-35\">\n      <span>Category</span>\n      <UiSelectIcon />\n    </UiSelectButton>\n    <ReactAria.Popover className=\"w-64\" placement=\"bottom left\">\n      <UiSelectDialog>\n        <ReactAria.CheckboxGroup\n          value={category ?? []}\n          onChange={(value) => {\n            setQueryParams(\"category\", value)\n          }}\n          className=\"max-h-50 overflow-scroll\"\n        >\n          {Object.entries(categories).map(([key, value]) => (\n            <UiCheckbox value={key} className=\"p-4\" key={key}>\n              <UiCheckboxBox>\n                <UiCheckboxIcon />\n              </UiCheckboxBox>\n              <UiCheckboxLabel>{value}</UiCheckboxLabel>\n            </UiCheckbox>\n          ))}\n        </ReactAria.CheckboxGroup>\n      </UiSelectDialog>\n    </ReactAria.Popover>\n  </UiDialogTrigger>\n)\n"
  },
  {
    "path": "storefront/src/modules/store/components/refinement-list/collection-filter/index.tsx",
    "content": "\"use client\"\n\nimport * as ReactAria from \"react-aria-components\"\nimport {\n  UiSelectButton,\n  UiSelectDialog,\n  UiSelectIcon,\n} from \"@/components/ui/Select\"\nimport {\n  UiCheckbox,\n  UiCheckboxBox,\n  UiCheckboxIcon,\n  UiCheckboxLabel,\n} from \"@/components/ui/Checkbox\"\nimport { UiDialogTrigger } from \"@/components/Dialog\"\n\nexport const CollectionFilter: React.FC<{\n  collections: Record<string, string>\n  collection?: string[]\n  setQueryParams: (name: string, value: string[]) => void\n}> = ({ collection, collections, setQueryParams }) => (\n  <UiDialogTrigger>\n    <UiSelectButton className=\"w-35\">\n      <span>Collection</span>\n      <UiSelectIcon />\n    </UiSelectButton>\n    <ReactAria.Popover className=\"w-64\" placement=\"bottom left\">\n      <UiSelectDialog>\n        <ReactAria.CheckboxGroup\n          value={collection ?? []}\n          onChange={(value) => {\n            setQueryParams(\"collection\", value)\n          }}\n          className=\"max-h-50 overflow-scroll\"\n        >\n          {Object.entries(collections).map(([key, value]) => (\n            <UiCheckbox value={key} className=\"p-4\" key={key}>\n              <UiCheckboxBox>\n                <UiCheckboxIcon />\n              </UiCheckboxBox>\n              <UiCheckboxLabel>{value}</UiCheckboxLabel>\n            </UiCheckbox>\n          ))}\n        </ReactAria.CheckboxGroup>\n      </UiSelectDialog>\n    </ReactAria.Popover>\n  </UiDialogTrigger>\n)\n"
  },
  {
    "path": "storefront/src/modules/store/components/refinement-list/index.tsx",
    "content": "\"use client\"\n\nimport { usePathname, useRouter, useSearchParams } from \"next/navigation\"\nimport { useCallback } from \"react\"\n\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { CategoryFilter } from \"@modules/store/components/refinement-list/category-filter\"\nimport { CollectionFilter } from \"@modules/store/components/refinement-list/collection-filter\"\nimport { MobileFilters } from \"@modules/store/components/refinement-list/mobile-filters\"\nimport { MobileSort } from \"@modules/store/components/refinement-list/mobile-sort\"\nimport SortProducts, {\n  SortOptions,\n} from \"@modules/store/components/refinement-list/sort-products\"\nimport { TypeFilter } from \"@modules/store/components/refinement-list/type-filter\"\n\ntype RefinementListProps = {\n  title?: string\n  collections?: Record<string, string>\n  collection?: string[]\n  categories?: Record<string, string>\n  category?: string[]\n  types?: Record<string, string>\n  type?: string[]\n  sortBy: SortOptions | undefined\n  \"data-testid\"?: string\n}\n\nconst RefinementList = ({\n  title = \"Shop\",\n  collections,\n  collection,\n  categories,\n  category,\n  types,\n  type,\n  sortBy,\n  \"data-testid\": dataTestId,\n}: RefinementListProps) => {\n  const router = useRouter()\n  const pathname = usePathname()\n  const searchParams = useSearchParams()\n\n  const setQueryParams = useCallback(\n    (name: string, value: string | string[]) => {\n      const query = new URLSearchParams(searchParams)\n\n      if (Array.isArray(value)) {\n        query.delete(name)\n        value.forEach((v) => query.append(name, v))\n      } else {\n        query.set(name, value)\n      }\n\n      router.push(`${pathname}?${query.toString()}`, { scroll: false })\n    },\n    [pathname, router, searchParams]\n  )\n\n  const setMultipleQueryParams = useCallback(\n    (params: Record<string, string | string[]>) => {\n      const query = new URLSearchParams(searchParams)\n\n      Object.entries(params).forEach(([name, value]) => {\n        if (Array.isArray(value)) {\n          query.delete(name)\n          value.forEach((v) => query.append(name, v))\n        } else {\n          query.set(name, value)\n        }\n      })\n\n      router.push(`${pathname}?${query.toString()}`, { scroll: false })\n    },\n    [searchParams, pathname, router]\n  )\n\n  return (\n    <Layout className=\"mb-6 md:mb-8\">\n      <LayoutColumn>\n        <h2 className=\"text-md md:text-2xl mb-6 md:mb-7\" id=\"products\">\n          {title}\n        </h2>\n        <div className=\"flex justify-between gap-10\">\n          <MobileFilters\n            collections={collections}\n            collection={collection}\n            categories={categories}\n            category={category}\n            types={types}\n            type={type}\n            setMultipleQueryParams={setMultipleQueryParams}\n          />\n          <MobileSort sortBy={sortBy} setQueryParams={setQueryParams} />\n          <div className=\"flex justify-between gap-4 max-md:hidden\">\n            {typeof collections !== \"undefined\" && (\n              <CollectionFilter\n                collections={collections}\n                collection={collection}\n                setQueryParams={setQueryParams}\n              />\n            )}\n            {typeof categories !== \"undefined\" && (\n              <CategoryFilter\n                categories={categories}\n                category={category}\n                setQueryParams={setQueryParams}\n              />\n            )}\n            {typeof types !== \"undefined\" && (\n              <TypeFilter\n                types={types}\n                type={type}\n                setQueryParams={setQueryParams}\n              />\n            )}\n          </div>\n          <SortProducts\n            sortBy={sortBy}\n            setQueryParams={setQueryParams}\n            data-testid={dataTestId}\n          />\n        </div>\n      </LayoutColumn>\n    </Layout>\n  )\n}\n\nexport default RefinementList\n"
  },
  {
    "path": "storefront/src/modules/store/components/refinement-list/mobile-filters/index.tsx",
    "content": "import * as React from \"react\"\nimport * as ReactAria from \"react-aria-components\"\n\nimport {\n  UiCheckbox,\n  UiCheckboxBox,\n  UiCheckboxIcon,\n  UiCheckboxLabel,\n} from \"@/components/ui/Checkbox\"\nimport { Button } from \"@/components/Button\"\nimport { UiModal, UiModalOverlay } from \"@/components/ui/Modal\"\nimport { UiDialog, UiDialogTrigger } from \"@/components/Dialog\"\n\nexport const MobileFilters: React.FC<{\n  collections?: Record<string, string>\n  collection?: string[]\n  categories?: Record<string, string>\n  category?: string[]\n  types?: Record<string, string>\n  type?: string[]\n  setMultipleQueryParams: (params: Record<string, string | string[]>) => void\n}> = ({\n  collections,\n  collection,\n  categories,\n  category,\n  types,\n  type,\n  setMultipleQueryParams,\n}) => {\n  return (\n    <UiDialogTrigger>\n      <Button\n        size=\"sm\"\n        variant=\"outline\"\n        iconName=\"plus\"\n        iconPosition=\"end\"\n        className=\"md:hidden border-grayscale-200\"\n      >\n        Filter\n      </Button>\n      <UiModalOverlay className=\"p-0\">\n        <UiModal\n          animateFrom=\"bottom\"\n          className=\"top-36 w-full pb-26 max-w-full\"\n        >\n          <UiDialog>\n            {({ close }) => (\n              <form\n                onSubmit={(event) => {\n                  const formData = new FormData(event.currentTarget)\n\n                  const collection = formData\n                    .getAll(\"collection\")\n                    .map((value) => value.toString())\n                  const category = formData\n                    .getAll(\"category\")\n                    .map((value) => value.toString())\n                  const type = formData\n                    .getAll(\"type\")\n                    .map((value) => value.toString())\n\n                  setMultipleQueryParams({\n                    collection,\n                    category,\n                    type,\n                  })\n\n                  close()\n                }}\n              >\n                {collections && Object.keys(collections).length > 0 && (\n                  <ReactAria.CheckboxGroup\n                    className=\"flex flex-col\"\n                    name=\"collection\"\n                    defaultValue={collection ?? []}\n                  >\n                    <ReactAria.Label className=\"block text-md font-semibold mb-3\">\n                      Collections\n                    </ReactAria.Label>\n                    {Object.entries(collections).map(([key, value]) => (\n                      <UiCheckbox\n                        key={key}\n                        value={key}\n                        className=\"justify-between py-3\"\n                      >\n                        <UiCheckboxLabel>{value}</UiCheckboxLabel>\n                        <UiCheckboxBox>\n                          <UiCheckboxIcon />\n                        </UiCheckboxBox>\n                      </UiCheckbox>\n                    ))}\n                  </ReactAria.CheckboxGroup>\n                )}\n                {collections &&\n                  Object.keys(collections).length > 0 &&\n                  ((categories && Object.keys(categories).length > 0) ||\n                    (types && Object.keys(types).length > 0)) && (\n                    <hr className=\"my-3 text-grayscale-200\" />\n                  )}\n                {categories && Object.keys(categories).length > 0 && (\n                  <ReactAria.CheckboxGroup\n                    className=\"flex flex-col\"\n                    name=\"category\"\n                    defaultValue={category ?? []}\n                  >\n                    <ReactAria.Label className=\"block text-md font-semibold mb-3\">\n                      Categories\n                    </ReactAria.Label>\n                    {Object.entries(categories).map(([key, value]) => (\n                      <UiCheckbox\n                        key={key}\n                        value={key}\n                        className=\"justify-between py-3\"\n                      >\n                        <UiCheckboxLabel>{value}</UiCheckboxLabel>\n                        <UiCheckboxBox>\n                          <UiCheckboxIcon />\n                        </UiCheckboxBox>\n                      </UiCheckbox>\n                    ))}\n                  </ReactAria.CheckboxGroup>\n                )}\n                {categories &&\n                  Object.keys(categories).length > 0 &&\n                  types &&\n                  Object.keys(types).length > 0 && (\n                    <hr className=\"my-3 text-grayscale-200\" />\n                  )}\n                {types && Object.keys(types).length > 0 && (\n                  <ReactAria.CheckboxGroup\n                    className=\"flex flex-col\"\n                    name=\"type\"\n                    defaultValue={type ?? []}\n                  >\n                    <ReactAria.Label className=\"block text-md font-semibold mb-3\">\n                      Types\n                    </ReactAria.Label>\n                    {Object.entries(types).map(([key, value]) => (\n                      <UiCheckbox\n                        key={key}\n                        value={key}\n                        className=\"justify-between py-3\"\n                      >\n                        <UiCheckboxLabel>{value}</UiCheckboxLabel>\n                        <UiCheckboxBox>\n                          <UiCheckboxIcon />\n                        </UiCheckboxBox>\n                      </UiCheckbox>\n                    ))}\n                  </ReactAria.CheckboxGroup>\n                )}\n                <footer className=\"flex items-center h-21 fixed bottom-0 left-0 w-full bg-white px-6 border-t border-grayscale-100\">\n                  <Button type=\"submit\" isFullWidth>\n                    Show results\n                  </Button>\n                </footer>\n              </form>\n            )}\n          </UiDialog>\n        </UiModal>\n      </UiModalOverlay>\n    </UiDialogTrigger>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/store/components/refinement-list/mobile-sort/index.tsx",
    "content": "import * as React from \"react\"\nimport * as ReactAria from \"react-aria-components\"\nimport { SortOptions } from \"@modules/store/components/refinement-list/sort-products\"\nimport {\n  UiRadio,\n  UiRadioBox,\n  UiRadioGroup,\n  UiRadioLabel,\n} from \"@/components/ui/Radio\"\nimport { UiModal, UiModalOverlay } from \"@/components/ui/Modal\"\nimport { Button } from \"@/components/Button\"\nimport { UiDialog, UiDialogTrigger } from \"@/components/Dialog\"\n\nexport const MobileSort: React.FC<{\n  sortBy: SortOptions | undefined\n  setQueryParams: (name: string, value: SortOptions) => void\n}> = ({ sortBy, setQueryParams }) => {\n  return (\n    <UiDialogTrigger>\n      <Button\n        size=\"sm\"\n        variant=\"outline\"\n        iconName=\"chevron-down\"\n        iconPosition=\"end\"\n        className=\"md:hidden border-grayscale-200\"\n      >\n        Sort by\n      </Button>\n      <UiModalOverlay className=\"p-0\">\n        <UiModal\n          animateFrom=\"bottom\"\n          className=\"w-full rounded-none max-w-full shadow-none pb-21\"\n        >\n          <UiDialog>\n            {({ close }) => (\n              <form\n                onSubmit={(event) => {\n                  const formData = new FormData(event.currentTarget)\n\n                  const sortBy = formData.get(\"sortBy\")?.toString()\n\n                  setQueryParams(\"sortBy\", sortBy as SortOptions)\n\n                  close()\n                }}\n              >\n                <UiRadioGroup\n                  className=\"flex flex-col mb-5\"\n                  name=\"sortBy\"\n                  defaultValue={sortBy}\n                  aria-label=\"Sort by\"\n                >\n                  <ReactAria.Label className=\"block text-md font-semibold mb-3\">\n                    Sort by\n                  </ReactAria.Label>\n                  <UiRadio value=\"created_at\" className=\"justify-between py-3\">\n                    <UiRadioLabel>Latest Arrivals</UiRadioLabel>\n                    <UiRadioBox />\n                  </UiRadio>\n                  <UiRadio value=\"price_asc\" className=\"justify-between py-3\">\n                    <UiRadioLabel>Lowest price</UiRadioLabel>\n                    <UiRadioBox />\n                  </UiRadio>\n                  <UiRadio value=\"price_desc\" className=\"justify-between py-3\">\n                    <UiRadioLabel>Highest price</UiRadioLabel>\n                    <UiRadioBox />\n                  </UiRadio>\n                </UiRadioGroup>\n                <footer className=\"flex items-center h-21 fixed bottom-0 left-0 w-full bg-white px-6 border-t border-grayscale-100\">\n                  <Button type=\"submit\" isFullWidth>\n                    Show results\n                  </Button>\n                </footer>\n              </form>\n            )}\n          </UiDialog>\n        </UiModal>\n      </UiModalOverlay>\n    </UiDialogTrigger>\n  )\n}\n"
  },
  {
    "path": "storefront/src/modules/store/components/refinement-list/sort-products/index.tsx",
    "content": "\"use client\"\n\nimport * as ReactAria from \"react-aria-components\"\nimport {\n  UiSelectButton,\n  UiSelectIcon,\n  UiSelectListBox,\n  UiSelectListBoxItem,\n  UiSelectValue,\n} from \"@/components/ui/Select\"\n\nexport type SortOptions = \"price_asc\" | \"price_desc\" | \"created_at\"\n\ntype SortProductsProps = {\n  sortBy: SortOptions | undefined\n  setQueryParams: (name: string, value: SortOptions) => void\n}\n\nconst SortProducts = ({ sortBy, setQueryParams }: SortProductsProps) => {\n  const handleChange = (value: SortOptions) => {\n    setQueryParams(\"sortBy\", value)\n  }\n\n  return (\n    <ReactAria.Select\n      placeholder=\"Sort by\"\n      selectedKey={sortBy || \"sortBy\"}\n      onSelectionChange={(key) => {\n        handleChange(key as SortOptions)\n      }}\n      className=\"max-md:hidden\"\n      aria-label=\"Sort by\"\n    >\n      <UiSelectButton>\n        <UiSelectValue />\n        <UiSelectIcon />\n      </UiSelectButton>\n      <ReactAria.Popover className=\"w-60\" placement=\"bottom right\">\n        <UiSelectListBox>\n          <UiSelectListBoxItem id=\"created_at\">\n            Latest Arrivals\n          </UiSelectListBoxItem>\n          <UiSelectListBoxItem id=\"price_asc\">Lowest price</UiSelectListBoxItem>\n          <UiSelectListBoxItem id=\"price_desc\">\n            Highest price\n          </UiSelectListBoxItem>\n        </UiSelectListBox>\n      </ReactAria.Popover>\n    </ReactAria.Select>\n  )\n}\n\nexport default SortProducts\n"
  },
  {
    "path": "storefront/src/modules/store/components/refinement-list/type-filter/index.tsx",
    "content": "\"use client\"\n\nimport * as ReactAria from \"react-aria-components\"\nimport {\n  UiSelectButton,\n  UiSelectDialog,\n  UiSelectIcon,\n} from \"@/components/ui/Select\"\nimport {\n  UiCheckbox,\n  UiCheckboxBox,\n  UiCheckboxIcon,\n  UiCheckboxLabel,\n} from \"@/components/ui/Checkbox\"\nimport { UiDialogTrigger } from \"@/components/Dialog\"\n\nexport const TypeFilter: React.FC<{\n  types: Record<string, string>\n  type?: string[]\n  setQueryParams: (name: string, value: string[]) => void\n}> = ({ type, types, setQueryParams }) => (\n  <UiDialogTrigger>\n    <UiSelectButton className=\"w-35\">\n      <span>Type</span>\n      <UiSelectIcon />\n    </UiSelectButton>\n    <ReactAria.Popover className=\"w-64\" placement=\"bottom left\">\n      <UiSelectDialog>\n        <ReactAria.CheckboxGroup\n          value={type ?? []}\n          onChange={(value) => {\n            setQueryParams(\"type\", value)\n          }}\n          className=\"max-h-50 overflow-scroll\"\n        >\n          {Object.entries(types).map(([key, value]) => (\n            <UiCheckbox value={key} className=\"p-4\" key={key}>\n              <UiCheckboxBox>\n                <UiCheckboxIcon />\n              </UiCheckboxBox>\n              <UiCheckboxLabel>{value}</UiCheckboxLabel>\n            </UiCheckbox>\n          ))}\n        </ReactAria.CheckboxGroup>\n      </UiSelectDialog>\n    </ReactAria.Popover>\n  </UiDialogTrigger>\n)\n"
  },
  {
    "path": "storefront/src/modules/store/templates/index.tsx",
    "content": "import { Suspense } from \"react\"\n\nimport SkeletonProductGrid from \"@modules/skeletons/templates/skeleton-product-grid\"\nimport RefinementList from \"@modules/store/components/refinement-list\"\nimport { SortOptions } from \"@modules/store/components/refinement-list/sort-products\"\nimport { CollectionsSlider } from \"@modules/store/components/collections-slider\"\n\nimport { getCollectionsList } from \"@lib/data/collections\"\nimport { getCategoriesList } from \"@lib/data/categories\"\nimport { getProductTypesList } from \"@lib/data/product-types\"\nimport PaginatedProducts from \"@modules/store/templates/paginated-products\"\nimport { getRegion } from \"@lib/data/regions\"\n\nconst StoreTemplate = async ({\n  sortBy,\n  collection,\n  category,\n  type,\n  page,\n  countryCode,\n}: {\n  sortBy?: SortOptions\n  collection?: string[]\n  category?: string[]\n  type?: string[]\n  page?: string\n  countryCode: string\n}) => {\n  const pageNumber = page ? parseInt(page, 10) : 1\n\n  const [collections, categories, types, region] = await Promise.all([\n    getCollectionsList(0, 100, [\"id\", \"title\", \"handle\"]),\n    getCategoriesList(0, 100, [\"id\", \"name\", \"handle\"]),\n    getProductTypesList(0, 100, [\"id\", \"value\"]),\n    getRegion(countryCode),\n  ])\n\n  return (\n    <div className=\"md:pt-47 py-26 md:pb-36\">\n      <CollectionsSlider />\n      <RefinementList\n        collections={Object.fromEntries(\n          collections.collections.map((c) => [c.handle, c.title])\n        )}\n        collection={collection}\n        categories={Object.fromEntries(\n          categories.product_categories.map((c) => [c.handle, c.name])\n        )}\n        category={category}\n        types={Object.fromEntries(\n          types.productTypes.map((t) => [t.value, t.value])\n        )}\n        type={type}\n        sortBy={sortBy}\n      />\n      <Suspense fallback={<SkeletonProductGrid />}>\n        {region && (\n          <PaginatedProducts\n            sortBy={sortBy}\n            page={pageNumber}\n            countryCode={countryCode}\n            collectionId={\n              !collection\n                ? undefined\n                : collections.collections\n                    .filter((c) => collection.includes(c.handle))\n                    .map((c) => c.id)\n            }\n            categoryId={\n              !category\n                ? undefined\n                : categories.product_categories\n                    .filter((c) => category.includes(c.handle))\n                    .map((c) => c.id)\n            }\n            typeId={\n              !type\n                ? undefined\n                : types.productTypes\n                    .filter((t) => type.includes(t.value))\n                    .map((t) => t.id)\n            }\n          />\n        )}\n      </Suspense>\n    </div>\n  )\n}\n\nexport default StoreTemplate\n"
  },
  {
    "path": "storefront/src/modules/store/templates/paginated-products.tsx",
    "content": "\"use client\"\nimport { HttpTypes, StoreProduct } from \"@medusajs/types\"\nimport ProductPreview from \"@modules/products/components/product-preview\"\nimport { SortOptions } from \"@modules/store/components/refinement-list/sort-products\"\nimport { Layout, LayoutColumn } from \"@/components/Layout\"\nimport { NoResults } from \"@modules/store/components/no-results.tsx\"\nimport { withReactQueryProvider } from \"@lib/util/react-query\"\nimport * as React from \"react\"\nimport { useStoreProducts } from \"hooks/store\"\nimport SkeletonProductGrid from \"@modules/skeletons/templates/skeleton-product-grid\"\n\nconst PRODUCT_LIMIT = 12\nfunction PaginatedProducts({\n  sortBy,\n  page,\n  collectionId,\n  categoryId,\n  typeId,\n  productsIds,\n  countryCode,\n}: {\n  sortBy?: SortOptions\n  page: number\n  collectionId?: string | string[]\n  categoryId?: string | string[]\n  typeId?: string | string[]\n  productsIds?: string[]\n  countryCode: string\n}) {\n  const queryParams: HttpTypes.StoreProductListParams = {\n    limit: PRODUCT_LIMIT,\n  }\n\n  if (collectionId) {\n    queryParams[\"collection_id\"] = Array.isArray(collectionId)\n      ? collectionId\n      : [collectionId]\n  }\n\n  if (categoryId) {\n    queryParams[\"category_id\"] = Array.isArray(categoryId)\n      ? categoryId\n      : [categoryId]\n  }\n\n  if (typeId) {\n    queryParams[\"type_id\"] = Array.isArray(typeId) ? typeId : [typeId]\n  }\n\n  if (productsIds) {\n    queryParams[\"id\"] = productsIds\n  }\n\n  if (sortBy === \"created_at\") {\n    queryParams[\"order\"] = \"created_at\"\n  }\n\n  const productsQuery = useStoreProducts({\n    page,\n    queryParams,\n    sortBy,\n    countryCode,\n  })\n  const loadMoreRef = React.useRef<HTMLDivElement>(null)\n\n  React.useEffect(() => {\n    if (!loadMoreRef.current) return\n    const observer = new IntersectionObserver(\n      (entries) => {\n        if (entries[0].isIntersecting && productsQuery.hasNextPage) {\n          productsQuery.fetchNextPage()\n        }\n      },\n      { rootMargin: \"100px\" }\n    )\n\n    observer.observe(loadMoreRef.current)\n    return () => observer.disconnect()\n  }, [productsQuery, loadMoreRef])\n\n  if (productsQuery.isPending) {\n    return <SkeletonProductGrid />\n  }\n\n  return (\n    <>\n      <Layout className=\"gap-y-10 md:gap-y-16 mb-16\">\n        {productsQuery?.data?.pages[0]?.response?.products?.length &&\n        (!productsIds || productsIds.length > 0) ? (\n          productsQuery?.data?.pages.flatMap((page) => {\n            return page?.response?.products.map((p: StoreProduct) => {\n              return (\n                <LayoutColumn key={p.id} className=\"md:!col-span-4 !col-span-6\">\n                  <ProductPreview product={p} />\n                </LayoutColumn>\n              )\n            })\n          })\n        ) : (\n          <NoResults />\n        )}\n        {productsQuery.hasNextPage && <div ref={loadMoreRef} />}\n      </Layout>\n    </>\n  )\n}\n\nexport default withReactQueryProvider(PaginatedProducts)\n"
  },
  {
    "path": "storefront/src/styles/globals.css",
    "content": "@import \"tailwindcss/base\";\n@import \"tailwindcss/components\";\n@import \"tailwindcss/utilities\";\n\n@layer base {\n  html {\n    @apply text-black font-normal;\n  }\n\n  h1,\n  h2,\n  h3,\n  h4,\n  h5,\n  h6 {\n    @apply font-medium;\n  }\n}\n\n@layer utilities {\n  /* Chrome, Safari and Opera */\n  .no-scrollbar::-webkit-scrollbar {\n    display: none;\n  }\n\n  .no-scrollbar::-webkit-scrollbar-track {\n    background-color: transparent;\n  }\n\n  .no-scrollbar {\n    -ms-overflow-style: none; /* IE and Edge */\n    scrollbar-width: none; /* Firefox */\n  }\n}\n\n@layer components {\n  .content-container {\n    @apply max-w-[1440px] w-full mx-auto px-6;\n  }\n\n  .contrast-btn {\n    @apply px-4 py-2 border border-black rounded-full hover:bg-black hover:text-white transition-colors duration-200 ease-in;\n  }\n\n  .text-xsmall-regular {\n    @apply text-[10px] leading-4 font-normal;\n  }\n\n  .text-small-regular {\n    @apply text-xs leading-5 font-normal;\n  }\n\n  .text-small-semi {\n    @apply text-xs leading-5 font-semibold;\n  }\n\n  .text-base-regular {\n    @apply text-sm leading-6 font-normal;\n  }\n\n  .text-base-semi {\n    @apply text-sm leading-6 font-semibold;\n  }\n\n  .text-large-regular {\n    @apply text-base leading-6 font-normal;\n  }\n\n  .text-large-semi {\n    @apply text-base leading-6 font-semibold;\n  }\n\n  .text-xl-regular {\n    @apply text-2xl leading-[36px] font-normal;\n  }\n\n  .text-xl-semi {\n    @apply text-2xl leading-[36px] font-semibold;\n  }\n\n  .text-2xl-regular {\n    @apply text-[30px] leading-[48px] font-normal;\n  }\n\n  .text-2xl-semi {\n    @apply text-[30px] leading-[48px] font-semibold;\n  }\n\n  .text-3xl-regular {\n    @apply text-[32px] leading-[44px] font-normal;\n  }\n\n  .text-3xl-semi {\n    @apply text-[32px] leading-[44px] font-semibold;\n  }\n\n  .article {\n    h1 {\n      @apply text-2xl;\n    }\n\n    h2,\n    h3,\n    h4,\n    h5,\n    h6 {\n      @apply text-md mt-16 mb-8;\n    }\n\n    p,\n    ul {\n      @apply mb-4;\n    }\n\n    ul,\n    ol {\n      @apply pl-5;\n    }\n\n    ul {\n      @apply list-disc;\n    }\n\n    ol {\n      @apply list-decimal;\n\n      ul {\n        @apply mt-4 pl-0;\n      }\n\n      > li {\n        @apply pl-1;\n      }\n\n      > li + li {\n        @apply mt-5;\n      }\n    }\n  }\n\n  .txt-medium {\n    @apply text-[1.875rem] leading-[1.3125rem] font-normal font-inter;\n  }\n\n  .txt-xlarge-plus {\n    @apply text-[1.125rem] leading-[1.6875rem] font-medium font-inter;\n  }\n}\n"
  },
  {
    "path": "storefront/src/types/icon.ts",
    "content": "export type IconProps = {\n  color?: string\n  size?: string | number\n} & React.SVGAttributes<SVGElement>\n"
  },
  {
    "path": "storefront/tailwind.config.js",
    "content": "module.exports = {\n  darkMode: \"class\",\n  content: [\n    \"./src/app/**/*.{js,ts,jsx,tsx}\",\n    \"./src/pages/**/*.{js,ts,jsx,tsx}\",\n    \"./src/components/**/*.{js,ts,jsx,tsx}\",\n    \"./src/modules/**/*.{js,ts,jsx,tsx}\",\n    \"./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}\",\n  ],\n  theme: {\n    colors: {\n      black: \"#050505\",\n      \"black-10%\": \"rgba(5, 5, 5, 0.1)\",\n      \"black-30%\": \"rgba(5, 5, 5, 0.3)\",\n      white: \"#FDFDFD\",\n      grayscale: {\n        800: \"#1F1F20\",\n        700: \"#3A3A3B\",\n        600: \"#545457\",\n        500: \"#808080\",\n        400: \"#A3A3A3\",\n        300: \"#BBBBBB\",\n        200: \"#D1D1D1\",\n        100: \"#E7E7E7\",\n        50: \"#F4F4F4\",\n        30: \"#F8F8F9\",\n        20: \"#FBFBFB\",\n      },\n      red: {\n        900: \"#BD3207\",\n        primary: \"#DF4718\",\n      },\n      yellow: \"#FFEFB7\",\n      transparent: \"rgba(0,0,0,0)\",\n      current: \"currentColor\",\n      \"fg-subtle\": {\n        DEFAULT: \"rgba(82, 82, 91, 1)\",\n        dark: \"rgba(161, 161, 170, 1)\",\n      },\n      \"fg-base\": {\n        DEFAULT: \"rgba(24, 24, 27, 1)\",\n        dark: \"rgba(244, 244, 245, 1)\",\n      },\n      \"bg-field\": {\n        DEFAULT: \"rgba(250, 250, 250, 1)\",\n        dark: \"rgba(255, 255, 255, 0.04)\",\n      },\n      \"bg-field-hover\": {\n        DEFAULT: \"rgba(244, 244, 245, 1)\",\n        dark: \"rgba(255, 255, 255, 0.08)\",\n      },\n      \"border-base\": {\n        DEFAULT: \"rgba(228, 228, 231, 1)\",\n        dark: \"rgba(255, 255, 255, 0.08)\",\n      },\n      \"fg-muted\": {\n        DEFAULT: \"rgba(161, 161, 170, 1)\",\n        dark: \"rgba(113, 113, 122, 1)\",\n      },\n    },\n    fontSize: {\n      \"3xl\": [\"3.5rem\", 1.4],\n      \"2xl\": [\"3rem\", 1.4],\n      xl: [\"2.5rem\", 1.4],\n      lg: [\"2rem\", 1.4],\n      md: [\"1.5rem\", 1.4],\n      sm: [\"1.125rem\", 1.4],\n      base: [\"1rem\", 1.4],\n      xs: [\"0.75rem\", 1.4],\n      \"2xs\": [\"0.625rem\", 1.4],\n    },\n    borderRadius: {\n      \"2xs\": \"2px\",\n      xs: \"4px\",\n      md: \"24px\",\n      lg: \"30px\",\n      full: \"100%\",\n      none: \"0px\",\n    },\n    screens: {\n      xs: \"400px\",\n      sm: \"640px\",\n      md: \"768px\",\n      lg: \"1024px\",\n      xl: \"1280px\",\n      \"2xl\": \"1400px\",\n    },\n    extend: {\n      spacing: {\n        6.5: \"1.625rem\",\n        11.5: \"2.875rem\",\n        13: \"3.25rem\",\n        13.5: \"3.375rem\",\n        14.5: \"3.625rem\",\n        15: \"3.75rem\",\n        17: \"4.25rem\",\n        18: \"4.5rem\",\n        19: \"4.75rem\",\n        21: \"5.25rem\",\n        22: \"5.5rem\",\n        23: \"5.75rem\",\n        25: \"6.25rem\",\n        26: \"6.5rem\",\n        27: \"6.75rem\",\n        28: \"7rem\",\n        29: \"7.25rem\",\n        30: \"7.5rem\",\n        31: \"7.75rem\",\n        33: \"8.25rem\",\n        34: \"8.5rem\",\n        35: \"8.75rem\",\n        37: \"9.25rem\",\n        39: \"9.75rem\",\n        42: \"10.5rem\",\n        45: \"11.25rem\",\n        46: \"11.5rem\",\n        47: \"11.75rem\",\n        50: \"12.5rem\",\n        54: \"13.5rem\",\n        61: \"15.25rem\",\n        65: \"16.25rem\",\n        66: \"16.5rem\",\n        75: \"18.75rem\",\n        90: \"22.5rem\",\n        91: \"22.75rem\",\n        93: \"23.25rem\",\n        95: \"23.75rem\",\n        98: \"24.5rem\",\n        100: \"25rem\",\n        108: \"27rem\",\n        120: \"30rem\",\n        123: \"30.75rem\",\n        124: \"31rem\",\n        125: \"31.25rem\",\n        135: \"33.75rem\",\n        139: \"34.75rem\",\n        150: \"37.5rem\",\n        154: \"38.5rem\",\n        159: \"39.75rem\",\n        200: \"50rem\",\n      },\n      borderWidth: {\n        6: \"6px\",\n      },\n      fontFamily: {\n        inter: [\n          \"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji\",\n        ],\n      },\n      transitionProperty: {\n        fontWeight: \"font-weight\",\n        padding: \"padding\",\n        width: \"width\",\n      },\n      zIndex: {\n        header: \"9999\",\n      },\n      keyframes: {\n        shimmer: {\n          \"100%\": {\n            transform: \"translateX(100%)\",\n          },\n        },\n      },\n      boxShadow: {\n        modal: \"0px 0px 40px -16px rgba(0, 0, 0, 0.20)\",\n        \"borders-interactive-with-active\":\n          \"0px 0px 0px 1px rgba(59, 130, 246, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.2)\",\n        \"borders-interactive-with-active-dark\":\n          \"0px 0px 0px 1px rgba(96, 165, 250, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.25)\",\n      },\n    },\n  },\n  safelist: [\n    {\n      pattern: /col-(start|end)-(1|2|3|4|5|6|7|8|9|10|11|12|13)/,\n      variants: [\"xs\", \"sm\", \"md\", \"lg\", \"xl\"],\n    },\n  ],\n  plugins: [require(\"tailwindcss-radix\"), require(\"tailwindcss-animate\")],\n}\n"
  },
  {
    "path": "storefront/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"baseUrl\": \"./src\",\n    \"paths\": {\n      \"@lib/*\": [\"lib/*\"],\n      \"@modules/*\": [\"modules/*\"],\n      \"@pages/*\": [\"pages/*\"],\n      \"@/components/*\": [\"components/*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\n    \"node_modules\",\n    \".next\",\n    \".nyc_output\",\n    \"coverage\",\n    \"jest-coverage\"\n  ]\n}\n"
  }
]