├── .env.local.example ├── .github ├── actions │ ├── build-test-frontend │ │ └── action.yml │ ├── playwright-setup │ │ └── action.yml │ └── pnpm-setup │ │ └── action.yml └── workflows │ ├── codesee-arch-diagram.yml │ ├── datadog.yml │ ├── deploy-strapi.yml │ ├── format.yml │ ├── jest.yml │ ├── lint.yml │ ├── main.yml │ ├── nextjs_bundle_analysis.yml │ ├── stress_test.yml │ └── tsc.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .npmpackagejsonlintrc.json ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── app_architecture_diagram.excalidraw ├── backend ├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitignore ├── .strapi │ └── client │ │ ├── app.js │ │ └── index.html ├── Dockerfile ├── README.md ├── build-docker-and-push ├── config │ ├── admin.ts │ ├── api.ts │ ├── database.ts │ ├── middlewares.ts │ ├── plugins.ts │ └── server.ts ├── database │ └── migrations │ │ └── .gitkeep ├── docker-compose.yaml ├── favicon.png ├── package.json ├── public │ ├── robots.txt │ └── uploads │ │ └── .gitkeep ├── src │ ├── admin │ │ ├── app.example.tsx │ │ └── tsconfig.json │ ├── api │ │ ├── .gitkeep │ │ ├── banner │ │ │ ├── content-types │ │ │ │ └── banner │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── banner.ts │ │ │ ├── routes │ │ │ │ └── banner.ts │ │ │ └── services │ │ │ │ └── banner.ts │ │ ├── company │ │ │ ├── content-types │ │ │ │ └── company │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── company.ts │ │ │ ├── routes │ │ │ │ └── company.ts │ │ │ └── services │ │ │ │ └── company.ts │ │ ├── faq │ │ │ ├── content-types │ │ │ │ └── faq │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── faq.ts │ │ │ ├── routes │ │ │ │ └── faq.ts │ │ │ └── services │ │ │ │ └── faq.ts │ │ ├── global │ │ │ ├── content-types │ │ │ │ └── global │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── global.ts │ │ │ ├── routes │ │ │ │ └── global.ts │ │ │ └── services │ │ │ │ └── global.ts │ │ ├── item-type │ │ │ ├── content-types │ │ │ │ └── item-type │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── item-type.ts │ │ │ ├── routes │ │ │ │ └── item-type.ts │ │ │ └── services │ │ │ │ └── item-type.ts │ │ ├── menu │ │ │ ├── content-types │ │ │ │ └── menu │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── menu.ts │ │ │ ├── routes │ │ │ │ └── menu.ts │ │ │ └── services │ │ │ │ └── menu.ts │ │ ├── page │ │ │ ├── content-types │ │ │ │ └── page │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── page.ts │ │ │ ├── routes │ │ │ │ └── page.ts │ │ │ └── services │ │ │ │ └── page.ts │ │ ├── product-list │ │ │ ├── content-types │ │ │ │ └── product-list │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── product-list.ts │ │ │ ├── routes │ │ │ │ └── product-list.ts │ │ │ └── services │ │ │ │ └── product-list.ts │ │ ├── product │ │ │ ├── content-types │ │ │ │ └── product │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── product.ts │ │ │ ├── routes │ │ │ │ └── product.ts │ │ │ └── services │ │ │ │ └── product.ts │ │ ├── reusable-section │ │ │ ├── content-types │ │ │ │ └── reusable-section │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── reusable-section.ts │ │ │ ├── routes │ │ │ │ └── reusable-section.ts │ │ │ └── services │ │ │ │ └── reusable-section.ts │ │ ├── screwdriver-bit-type │ │ │ ├── content-types │ │ │ │ └── screwdriver-bit-type │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── screwdriver-bit-type.ts │ │ │ ├── routes │ │ │ │ └── screwdriver-bit-type.ts │ │ │ └── services │ │ │ │ └── screwdriver-bit-type.ts │ │ ├── screwdriver-bit │ │ │ ├── content-types │ │ │ │ └── screwdriver-bit │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── screwdriver-bit.ts │ │ │ ├── routes │ │ │ │ └── screwdriver-bit.ts │ │ │ └── services │ │ │ │ └── screwdriver-bit.ts │ │ ├── social-post │ │ │ ├── content-types │ │ │ │ └── social-post │ │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ │ └── social-post.ts │ │ │ ├── routes │ │ │ │ └── social-post.ts │ │ │ └── services │ │ │ │ └── social-post.ts │ │ └── store │ │ │ ├── content-types │ │ │ └── store │ │ │ │ └── schema.json │ │ │ ├── controllers │ │ │ └── store.ts │ │ │ ├── routes │ │ │ └── store.ts │ │ │ └── services │ │ │ └── store.ts │ ├── bootstrap │ │ ├── index.ts │ │ └── permissions │ │ │ ├── config.json │ │ │ └── index.ts │ ├── components │ │ ├── global │ │ │ ├── newsletter-form.json │ │ │ └── person.json │ │ ├── menu │ │ │ ├── link-with-image.json │ │ │ ├── link.json │ │ │ ├── product-list-link.json │ │ │ └── submenu.json │ │ ├── misc │ │ │ └── placement.json │ │ ├── page │ │ │ ├── browse.json │ │ │ ├── call-to-action.json │ │ │ ├── category.json │ │ │ ├── hero.json │ │ │ ├── press-quote.json │ │ │ ├── press.json │ │ │ ├── split-with-image.json │ │ │ ├── stat-item.json │ │ │ └── stats.json │ │ ├── product-list │ │ │ ├── banner.json │ │ │ ├── item-type-override.json │ │ │ ├── linked-product-list-set.json │ │ │ └── related-posts.json │ │ ├── product │ │ │ ├── bit-table.json │ │ │ ├── cross-sell.json │ │ │ ├── device-compatibility.json │ │ │ ├── product-customer-reviews.json │ │ │ ├── product.json │ │ │ └── replacement-guides.json │ │ ├── section │ │ │ ├── banner.json │ │ │ ├── faqs.json │ │ │ ├── featured-products.json │ │ │ ├── lifetime-warranty.json │ │ │ ├── quote-card.json │ │ │ ├── quote-gallery.json │ │ │ ├── quote.json │ │ │ ├── service-value-propositions.json │ │ │ ├── social-gallery.json │ │ │ ├── stories.json │ │ │ └── tools.json │ │ └── store │ │ │ ├── footer.json │ │ │ ├── header.json │ │ │ ├── shopify-settings.json │ │ │ └── social-media-accounts.json │ ├── extensions │ │ └── .gitkeep │ ├── index.ts │ └── plugins │ │ └── addons │ │ ├── README.md │ │ ├── admin │ │ └── src │ │ │ ├── api │ │ │ ├── request.ts │ │ │ └── seed.ts │ │ │ ├── components │ │ │ ├── Illo.tsx │ │ │ ├── Initializer │ │ │ │ └── index.tsx │ │ │ ├── NavBar.tsx │ │ │ ├── PluginIcon │ │ │ │ └── index.tsx │ │ │ ├── SelectCollectionType.tsx │ │ │ └── bulk-operations │ │ │ │ ├── ExportSection.tsx │ │ │ │ └── ImportSection.tsx │ │ │ ├── index.tsx │ │ │ ├── pages │ │ │ ├── App │ │ │ │ └── index.tsx │ │ │ ├── Backup │ │ │ │ └── index.tsx │ │ │ └── BulkOperations │ │ │ │ └── index.tsx │ │ │ ├── pluginId.ts │ │ │ ├── translations │ │ │ ├── en.json │ │ │ └── fr.json │ │ │ └── utils │ │ │ ├── addons-api.ts │ │ │ ├── axiosInstance.ts │ │ │ ├── getTrad.ts │ │ │ ├── path-helpers.ts │ │ │ └── react-query-client.ts │ │ ├── global.d.ts │ │ ├── helpers │ │ └── generic-helpers.ts │ │ ├── package.json │ │ ├── server │ │ ├── bootstrap.ts │ │ ├── config │ │ │ └── index.ts │ │ ├── content-types │ │ │ └── index.ts │ │ ├── controllers │ │ │ ├── bulk-operations.ts │ │ │ ├── content-types.ts │ │ │ ├── index.ts │ │ │ └── seed.ts │ │ ├── destroy.ts │ │ ├── helpers │ │ │ └── server-helpers.ts │ │ ├── index.ts │ │ ├── middlewares │ │ │ └── index.ts │ │ ├── policies │ │ │ └── index.ts │ │ ├── register.ts │ │ ├── routes │ │ │ └── index.ts │ │ └── services │ │ │ ├── bulk-operations │ │ │ ├── export-csv.ts │ │ │ ├── import-csv.ts │ │ │ └── index.ts │ │ │ ├── content-types │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── seed │ │ │ ├── backup │ │ │ ├── download.ts │ │ │ ├── export.ts │ │ │ ├── import.ts │ │ │ └── types.ts │ │ │ ├── custom │ │ │ ├── content-fetcher.ts │ │ │ ├── content-record-parser.ts │ │ │ ├── content-record.ts │ │ │ ├── content-store.ts │ │ │ ├── import.ts │ │ │ ├── index.ts │ │ │ ├── media-store.ts │ │ │ └── populate-param-builder.ts │ │ │ ├── errors │ │ │ └── ContentTypeNotFoundError.ts │ │ │ └── index.ts │ │ ├── strapi-admin.js │ │ ├── strapi-server.js │ │ ├── tsconfig.json │ │ └── yarn.lock ├── strapi-deploy-actions ├── tsconfig.json ├── types │ └── generated │ │ ├── components.d.ts │ │ └── contentTypes.d.ts └── yarn.lock ├── frontend ├── .env ├── .env.development ├── .env.local.example ├── .env.production ├── .env.test ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .prettierignore ├── @types │ ├── index.d.ts │ └── lite-youtube-embed.d.ts ├── app │ ├── (defaultLayout) │ │ ├── @storeSelect │ │ │ ├── default.tsx │ │ │ ├── products │ │ │ │ └── [handle] │ │ │ │ │ └── page.tsx │ │ │ └── store-select.tsx │ │ ├── Store │ │ │ └── [[...slug]] │ │ │ │ ├── data.ts │ │ │ │ └── page.tsx │ │ ├── Troubleshooting │ │ │ └── [device] │ │ │ │ ├── components │ │ │ │ ├── AnswerCard.tsx │ │ │ │ ├── Answers.tsx │ │ │ │ ├── NavBar.tsx │ │ │ │ ├── ProblemCard.tsx │ │ │ │ └── troubleshootingProblems.tsx │ │ │ │ ├── hooks │ │ │ │ └── useTroubleshootingProblemsProps.ts │ │ │ │ └── page.tsx │ │ ├── app-router │ │ │ └── Troubleshooting │ │ │ │ └── [device] │ │ │ │ └── [problem] │ │ │ │ └── [wikiid] │ │ │ │ └── page.tsx │ │ ├── components │ │ │ ├── analytics │ │ │ │ ├── google-tag-manager.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── piwik-pro.tsx │ │ │ ├── not-found-page.tsx │ │ │ └── page-frame │ │ │ │ ├── footer.tsx │ │ │ │ ├── header.tsx │ │ │ │ └── index.tsx │ │ ├── error.tsx │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ └── products │ │ │ └── [handle] │ │ │ ├── json-ld-scripts.tsx │ │ │ └── page.tsx │ ├── _data │ │ └── product.ts │ ├── _helpers │ │ ├── app-helpers.ts │ │ └── product-helpers.ts │ ├── global-error.tsx │ └── layout.tsx ├── assets │ ├── images │ │ └── no-image-fixie.jpeg │ └── svg │ │ └── files │ │ ├── index.tsx │ │ ├── partners │ │ ├── crucial.svg │ │ ├── google.svg │ │ ├── hp.svg │ │ ├── index.tsx │ │ ├── lenovo.svg │ │ ├── logitech.svg │ │ ├── micron.svg │ │ ├── microsoft.svg │ │ ├── motorola.svg │ │ ├── nokia.svg │ │ ├── polaroid.svg │ │ ├── samsung.svg │ │ ├── steam.svg │ │ ├── teenage-engineering.svg │ │ ├── valve.svg │ │ └── vive.svg │ │ ├── product-list-empty-state-illustration.svg │ │ ├── quality-guarantee.svg │ │ └── search-empty-state-illustration.svg ├── codegen │ ├── config.ts │ └── strapi-schema-config.ts ├── components │ ├── admin │ │ ├── PageEditMenu.tsx │ │ └── index.ts │ ├── analytics │ │ ├── GoogleAnalytics.tsx │ │ ├── PiwikiPro.tsx │ │ ├── PixelPing.tsx │ │ └── index.ts │ ├── common │ │ ├── AppProviders.tsx │ │ ├── CompatibleDevice.tsx │ │ ├── FlexScrollGradient.tsx │ │ ├── InstantSearchProvider.tsx │ │ ├── NavTabs.tsx │ │ ├── PageBreadcrumb.tsx │ │ ├── PrerenderedHTML.tsx │ │ ├── ProductCard.tsx │ │ ├── ProductGrid.tsx │ │ ├── ProductGridItem.tsx │ │ ├── ProductRating.tsx │ │ ├── SecondaryNavbar.tsx │ │ ├── TextWithHighlight.tsx │ │ ├── ViewStats.tsx │ │ ├── index.ts │ │ └── useSearchCache.tsx │ ├── community │ │ ├── activity.tsx │ │ ├── info.tsx │ │ ├── navigation.tsx │ │ ├── options.tsx │ │ └── video.tsx │ ├── page │ │ └── guidePage.tsx │ ├── product-list │ │ └── ProductListCard.tsx │ ├── sections │ │ ├── BannersSection │ │ │ ├── MultipleBanners.tsx │ │ │ ├── SingleBanner.tsx │ │ │ └── index.tsx │ │ ├── BitTableSection.tsx │ │ ├── FAQsSection.tsx │ │ ├── FeaturedProductsSection.tsx │ │ ├── IFixitStatsSection.tsx │ │ ├── LifetimeWarrantySection.tsx │ │ ├── QuoteGallerySection.tsx │ │ ├── QuoteSection.tsx │ │ ├── ReplacementGuidesSection.tsx │ │ ├── SectionDescription.tsx │ │ ├── SectionHeaderWrapper.tsx │ │ ├── SectionHeading.tsx │ │ ├── ServiceValuePropositionSection.tsx │ │ ├── SocialGallerySection.tsx │ │ ├── SplitWithImageSection.tsx │ │ └── ToolsSection │ │ │ ├── index.tsx │ │ │ ├── tool-shapes │ │ │ ├── 64-bit-driver.svg │ │ │ ├── angles-tweezers.svg │ │ │ ├── anti-static-wrist-strap.svg │ │ │ ├── blunt-tweezers.svg │ │ │ ├── flex-extension.svg │ │ │ ├── hallberd-spudger.svg │ │ │ ├── index.ts │ │ │ ├── jimmy.svg │ │ │ ├── metal-spudger.svg │ │ │ ├── opening-pick.svg │ │ │ ├── opening-tool.svg │ │ │ ├── reverse-tweezers.svg │ │ │ ├── spudger.svg │ │ │ └── suction-handle.svg │ │ │ └── toolsData.ts │ └── ui │ │ ├── Card.tsx │ │ ├── IntlDate.tsx │ │ ├── LinkButton.tsx │ │ ├── Rating.tsx │ │ ├── ScreenOnlyLabel.tsx │ │ ├── SmartLink.tsx │ │ ├── Thumbnail.tsx │ │ ├── Tooltip.tsx │ │ └── index.ts ├── config │ ├── constants.ts │ ├── env.ts │ └── flags.ts ├── helpers │ ├── algolia-helpers.ts │ ├── application-helpers.ts │ ├── cache-control-helpers.ts │ ├── metadata-helpers.ts │ ├── next-helpers.ts │ ├── path-helpers.ts │ ├── product-list-helpers.ts │ ├── product-preview-helpers.ts │ ├── storefront-helpers.ts │ ├── strapi-helpers.ts │ ├── ui-helpers.ts │ ├── vercel-helpers.ts │ └── zod-helpers.ts ├── jest.config.js ├── layouts │ └── default │ │ ├── Footer.tsx │ │ ├── LayoutErrorBoundary.tsx │ │ ├── index.tsx │ │ └── server.ts ├── lib │ ├── cache.ts │ ├── duration.ts │ ├── ifixit-api │ │ ├── devices.ts │ │ ├── international-buy-box.ts │ │ └── productData.ts │ ├── images.tsx │ ├── links.tsx │ ├── next-middleware.ts │ ├── redis │ │ └── index.ts │ ├── server-side-props.tsx │ ├── shopify-storefront-sdk │ │ ├── generated │ │ │ └── sdk.ts │ │ ├── index.ts │ │ └── operations │ │ │ ├── ImageFields.fragment.graphql │ │ │ ├── ProductVariantFields.fragment.graphql │ │ │ ├── findProduct.graphql │ │ │ └── productPreviewFields.fragment.graphql │ ├── site.tsx │ ├── strapi-sdk │ │ ├── generated │ │ │ ├── sdk.ts │ │ │ └── validation.ts │ │ ├── index.ts │ │ └── operations │ │ │ ├── BannersFields.fragment.graphql │ │ │ ├── CallToActionFields.fragment.graphql │ │ │ ├── CompanyFields.fragment.graphql │ │ │ ├── components │ │ │ ├── FAQFields.fragment.graphql │ │ │ ├── ImageFields.fragment.graphql │ │ │ ├── PersonFields.fragment.graphql │ │ │ ├── ProductListPreviewFields.fragment.graphql │ │ │ ├── QuoteCardFields.fragment.graphql │ │ │ ├── ScrewdriverBit.fragment.graphql │ │ │ └── ScrewdriverBitType.fragment.graphql │ │ │ ├── findPage.graphql │ │ │ ├── findProduct.graphql │ │ │ ├── findProductList.graphql │ │ │ ├── findReusableSections.graphql │ │ │ ├── findStore.graphql │ │ │ ├── getGlobalSettings.graphql │ │ │ ├── getStoreList.graphql │ │ │ └── sections │ │ │ ├── BannersSectionFields.fragment.graphql │ │ │ ├── BitTableSectionFields.fragment.graphql │ │ │ ├── BrowseSectionFields.fragment.graphql │ │ │ ├── CategoryFields.fragment.graphql │ │ │ ├── DeviceCompatibilitySectionFields.fragment.graphql │ │ │ ├── FAQsSectionFields.fragment.graphql │ │ │ ├── FeaturedProductsSectionFields.fragment.graphql │ │ │ ├── HeroSectionFields.fragment.graphql │ │ │ ├── LifetimeWarrantySectionFields.fragment.graphql │ │ │ ├── PressQuotesSectionFields.fragment.graphql │ │ │ ├── ProductCrossSellSectionFields.fragment.graphql │ │ │ ├── ProductCustomerReviewsSectionFields.fragment.graphql │ │ │ ├── ProductListBannerFields.fragment.graphql │ │ │ ├── ProductListLinkedProductListSetSectionFields.fragment.graphql │ │ │ ├── ProductListRelatedPostsFields.fragment.graphql │ │ │ ├── ProductOverviewSectionFields.fragment.graphql │ │ │ ├── ProductReplacementGuidesSectionFields.fragment.graphql │ │ │ ├── QuoteGallerySectionFields.fragment.graphql │ │ │ ├── QuoteSectionFields.fragment.graphql │ │ │ ├── ServiceValuePropositionSectionFields.fragment.graphql │ │ │ ├── SocialGallerySectionFields.fragment.graphql │ │ │ ├── SplitWithImageSectionFields.fragment.graphql │ │ │ ├── StatsSectionFields.fragment.graphql │ │ │ └── ToolsSectionFields.fragment.graphql │ └── swr-cache │ │ ├── adapters │ │ ├── index.ts │ │ ├── null-adapter.ts │ │ └── redis-adapter.ts │ │ ├── index.ts │ │ └── utils.ts ├── models │ ├── components │ │ ├── algolia-product-hit.ts │ │ ├── banner.ts │ │ ├── breadcrumb.ts │ │ ├── call-to-action.ts │ │ ├── company.ts │ │ ├── faq.ts │ │ ├── image.ts │ │ ├── money.ts │ │ ├── person.ts │ │ ├── press-quote.ts │ │ ├── pro-price-tiers.ts │ │ ├── product-preview.ts │ │ ├── product-reviews.ts │ │ ├── quote-card.ts │ │ ├── replacement-guide-preview.ts │ │ ├── screwdriver-bit-type.ts │ │ ├── screwdriver-bit.ts │ │ └── social-post.ts │ ├── global-settings.ts │ ├── page │ │ ├── components │ │ │ └── browse-category.ts │ │ ├── index.ts │ │ ├── sections │ │ │ ├── browse-section.ts │ │ │ └── hero-section.ts │ │ └── server.ts │ ├── posts.ts │ ├── product-list │ │ ├── component │ │ │ ├── product-list-ancestor.ts │ │ │ ├── product-list-child.ts │ │ │ ├── product-list-preview.ts │ │ │ ├── product-list-redirect-to-type.server.ts │ │ │ ├── product-list-redirect-to-type.ts │ │ │ ├── product-list-type.server.ts │ │ │ └── product-list-type.ts │ │ ├── index.ts │ │ ├── sections │ │ │ ├── featured-product-lists-section.ts │ │ │ ├── filterable-products-section.ts │ │ │ ├── hero-section.ts │ │ │ ├── index.ts │ │ │ ├── product-list-children-section.ts │ │ │ └── reusable-sections.ts │ │ ├── server.ts │ │ └── types.ts │ ├── product │ │ ├── components │ │ │ ├── product-device-compatibility.ts │ │ │ ├── product-enabled-domains.ts │ │ │ ├── product-oem-partnership.ts │ │ │ ├── product-option.ts │ │ │ ├── product-variant.ts │ │ │ └── product-video.ts │ │ ├── index.ts │ │ ├── reviews.ts │ │ ├── sections │ │ │ ├── compatibility-section.ts │ │ │ ├── index.ts │ │ │ ├── product-overview-section.ts │ │ │ └── product-reviews-section.ts │ │ └── server.ts │ ├── reusable-section │ │ ├── components │ │ │ └── placement.ts │ │ ├── index.ts │ │ └── server.ts │ ├── sections │ │ ├── banners-section.ts │ │ ├── bit-table-section.ts │ │ ├── faqs-section.ts │ │ ├── featured-products-section.ts │ │ ├── ifixit-stats-section.ts │ │ ├── lifetime-warranty-section.ts │ │ ├── press-quotes-section.ts │ │ ├── quote-gallery-section.ts │ │ ├── quote-section.ts │ │ ├── related-posts-section.ts │ │ ├── replacement-guides-section.ts │ │ ├── service-value-proposition-section.ts │ │ ├── social-gallery-section.ts │ │ ├── split-with-image-section.ts │ │ └── tools-section.ts │ └── store.ts ├── next-config │ ├── helpers │ │ └── redirects.js │ └── redirects │ │ ├── legacy-filter-slugs.json │ │ └── tool-collections.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── 404.tsx │ ├── CommunityNext │ │ └── index.tsx │ ├── Parts │ │ ├── [...deviceHandleItemType].tsx │ │ └── index.tsx │ ├── Shop │ │ └── [handle].tsx │ ├── Tools │ │ ├── [handle].tsx │ │ └── index.tsx │ ├── Troubleshooting │ │ └── [device] │ │ │ └── [problem] │ │ │ └── [wikiid].tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.tsx │ └── api │ │ └── nextjs │ │ ├── cache │ │ ├── product-list.ts │ │ └── product.ts │ │ └── index.ts ├── playwright.config.ts ├── public │ ├── favicon.ico │ ├── images │ │ ├── lifetime-guarantee-background.jpg │ │ └── newsletter-icon.png │ ├── mockServiceWorker.js │ ├── robots.txt │ └── vercel.svg ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── sentry.properties ├── sentry.server.config.ts ├── seo │ └── product-list │ │ ├── hreflangs.ts │ │ ├── index.ts │ │ └── noIndexExemptions.ts ├── templates │ ├── page │ │ └── sections │ │ │ ├── BrowseSection.tsx │ │ │ ├── HeroSection.tsx │ │ │ └── PressQuotesSection.tsx │ ├── product-list │ │ ├── MetaTags.tsx │ │ ├── ProductListDeviceNavigation.tsx │ │ ├── ProductListView.tsx │ │ ├── SecondaryNavigation.tsx │ │ ├── hooks │ │ │ ├── useAvailableItemTypes.ts │ │ │ ├── useCurrentProductList.tsx │ │ │ ├── useDevicePartsItemType.ts │ │ │ ├── useItemTypeProductList.ts │ │ │ ├── useProductListBreadcrumbs.ts │ │ │ ├── useProductListTemplateProps.ts │ │ │ ├── useSearchQuery.tsx │ │ │ └── useVariantProductList.ts │ │ ├── index.tsx │ │ ├── sections │ │ │ ├── FeaturedProductListsSection.tsx │ │ │ ├── FilterableProductsSection │ │ │ │ ├── CurrentRefinements.tsx │ │ │ │ ├── Pagination.tsx │ │ │ │ ├── ProductList.tsx │ │ │ │ ├── SearchInput.tsx │ │ │ │ ├── Toolbar.tsx │ │ │ │ ├── facets │ │ │ │ │ ├── MenuFacet.tsx │ │ │ │ │ ├── RefinementListFacet.tsx │ │ │ │ │ ├── ShowMoreButton.tsx │ │ │ │ │ ├── accordion │ │ │ │ │ │ ├── FacetAccordionItem.tsx │ │ │ │ │ │ ├── FacetMenuAccordionItem.tsx │ │ │ │ │ │ ├── FacetRefinementListAccordionItem.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── useFacetAccordionItemState.tsx │ │ │ │ │ ├── drawer │ │ │ │ │ │ ├── FacetListItem.tsx │ │ │ │ │ │ ├── FacetPanel.tsx │ │ │ │ │ │ ├── ListItem.tsx │ │ │ │ │ │ ├── MenuFacetListItem.tsx │ │ │ │ │ │ ├── MenuFacetPanel.tsx │ │ │ │ │ │ ├── Panel.tsx │ │ │ │ │ │ ├── RefinementListFacetListItem.tsx │ │ │ │ │ │ ├── RefinementListFacetPanel.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── useCountRefinements.tsx │ │ │ │ │ ├── useCreateItemTypeURL.tsx │ │ │ │ │ ├── useFacets.tsx │ │ │ │ │ ├── useMenuFacet.tsx │ │ │ │ │ ├── useRefinementListFacet.tsx │ │ │ │ │ └── useSortBy.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── useHasAnyVisibleFacet.ts │ │ │ │ └── useProductSearchHitPricing.ts │ │ │ ├── HeroSection.tsx │ │ │ ├── ProductListChildrenSection.tsx │ │ │ ├── RelatedPostsSection │ │ │ │ ├── PostCard.tsx │ │ │ │ └── index.tsx │ │ │ └── index.ts │ │ └── server.tsx │ ├── product │ │ ├── components │ │ │ ├── ImagePlaceholder.tsx │ │ │ ├── PixelPing.tsx │ │ │ └── SecondaryNavigation.tsx │ │ ├── hooks │ │ │ ├── useInternationalBuyBox.tsx │ │ │ ├── useIsProductForSale.ts │ │ │ ├── useProductPageAdminLinks.tsx │ │ │ ├── useProductReviews.ts │ │ │ └── useSelectedVariant.ts │ │ ├── index.tsx │ │ └── sections │ │ │ ├── CompatibilityNotesSection │ │ │ └── index.tsx │ │ │ ├── CompatibilitySection │ │ │ └── index.tsx │ │ │ ├── ProductOverviewSection │ │ │ ├── AddToCart │ │ │ │ ├── AddToCartBar.tsx │ │ │ │ ├── InventoryMessage.tsx │ │ │ │ ├── NotifyMeForm.tsx │ │ │ │ ├── ShippingRestrictions.tsx │ │ │ │ └── index.tsx │ │ │ ├── CompatibilityNotes.tsx │ │ │ ├── CompatibleDevices.tsx │ │ │ ├── CrossSell │ │ │ │ ├── index.tsx │ │ │ │ └── useAvailableForSaleVariants.tsx │ │ │ ├── GenuinePartBanner.tsx │ │ │ ├── InternationalBuyBox.tsx │ │ │ ├── ProductDescription.tsx │ │ │ ├── ProductGallery.tsx │ │ │ ├── ProductOptions.tsx │ │ │ ├── ProductRating.tsx │ │ │ ├── ProductVideos.tsx │ │ │ ├── Prop65Warning.tsx │ │ │ ├── ValuePropositionList.tsx │ │ │ ├── constants.tsx │ │ │ └── index.tsx │ │ │ └── ProductReviewsSection │ │ │ └── index.tsx │ └── troubleshooting │ │ ├── DifficultyBadge.tsx │ │ ├── Problem.tsx │ │ ├── Resource.tsx │ │ ├── components │ │ ├── Causes.tsx │ │ ├── HeadingSelfLink.tsx │ │ ├── NavBar.tsx │ │ └── TagManager.tsx │ │ ├── hooks │ │ ├── GuideModel.ts │ │ └── useTroubleshootingProps.tsx │ │ ├── index.tsx │ │ ├── server.tsx │ │ ├── solution.tsx │ │ ├── toc.tsx │ │ └── tocContext.tsx ├── tests │ ├── jest │ │ ├── README.md │ │ ├── __mocks__ │ │ │ ├── product-list.ts │ │ │ ├── products │ │ │ │ ├── battery.ts │ │ │ │ ├── generic.ts │ │ │ │ ├── index.ts │ │ │ │ ├── part.ts │ │ │ │ └── tool.ts │ │ │ ├── reviews.ts │ │ │ └── svg.tsx │ │ ├── jest-setup.ts │ │ ├── tests │ │ │ ├── CompatibleDevice.test.tsx │ │ │ ├── ProductListItem.test.tsx │ │ │ ├── ProductReplacementGuide.test.tsx │ │ │ ├── ProductReviews.test.tsx │ │ │ ├── ProductSection.test.tsx │ │ │ ├── __snapshots__ │ │ │ │ ├── CompatibleDevice.test.tsx.snap │ │ │ │ └── ProductListItem.test.tsx.snap │ │ │ ├── sentry.test.tsx │ │ │ ├── shopify-storefront-sdk.test.tsx │ │ │ └── withCache.test.tsx │ │ └── utils.tsx │ └── playwright │ │ ├── .gitignore │ │ ├── README.md │ │ ├── fixtures │ │ ├── cart-drawer.ts │ │ ├── custom-nextjs-server.ts │ │ ├── index.ts │ │ ├── parts-page.ts │ │ ├── product-page.ts │ │ ├── shopify-mocked-queries.ts │ │ ├── strapi-mocked-queries.ts │ │ └── trace-override.ts │ │ ├── login-logout.spec.ts │ │ ├── msw │ │ ├── browser.ts │ │ ├── handlers.ts │ │ ├── index.ts │ │ ├── request-handler.ts │ │ └── server.ts │ │ ├── product-list │ │ ├── collections-display-modes.spec.ts │ │ ├── collections-page.spec.ts │ │ ├── filters.spec.ts │ │ ├── newsletter-subscription.spec.ts │ │ ├── parts-page-navigation.spec.ts │ │ ├── parts-page-product-listing.spec.ts │ │ └── parts-page-search.spec.ts │ │ ├── product │ │ ├── cross-sell.spec.ts │ │ ├── disabled-product.spec.ts │ │ ├── pro-user.spec.ts │ │ ├── product-breadcrumb.spec.ts │ │ ├── product-images.spec.ts │ │ ├── product-information.spec.ts │ │ ├── product-list.spec.ts │ │ ├── product-page-and-cart.spec.ts │ │ ├── product-reviews.spec.ts │ │ └── product-variant.spec.ts │ │ ├── sitemap-redirection.spec.ts │ │ ├── test-fixtures.ts │ │ ├── troubleshooting │ │ └── loading.spec.ts │ │ └── utils.ts └── tsconfig.json ├── package.json ├── packages ├── analytics │ ├── google │ │ └── index.tsx │ ├── index.tsx │ ├── package.json │ ├── piwik │ │ ├── index.tsx │ │ ├── piwikPush.ts │ │ └── track-event.ts │ └── tsconfig.json ├── app │ ├── __mocks__ │ │ └── index.tsx │ ├── index.tsx │ ├── package.json │ └── tsconfig.json ├── auth-sdk │ ├── index.tsx │ ├── package.json │ ├── tsconfig.json │ └── user.tsx ├── bot │ ├── cli.ts │ ├── commands │ │ ├── index.ts │ │ ├── shop │ │ │ ├── create.ts │ │ │ └── create │ │ │ │ └── delegate-token.ts │ │ └── shopify.ts │ ├── package.json │ ├── tsconfig.json │ └── utils │ │ ├── misc.ts │ │ └── shopify.ts ├── breadcrumbs │ ├── Breadcrumbs.tsx │ ├── FlexHiddenWrap.tsx │ ├── package.json │ └── tsconfig.json ├── cart-sdk │ ├── hooks │ │ ├── use-add-to-cart.ts │ │ ├── use-cart-line-item.ts │ │ ├── use-cart.ts │ │ ├── use-checkout.ts │ │ ├── use-remove-line-item.ts │ │ └── use-update-line-item-quantity.ts │ ├── index.tsx │ ├── package.json │ ├── tsconfig.json │ ├── types.ts │ └── utils.ts ├── eslint │ ├── index.js │ ├── package.json │ └── rules │ │ ├── no-new-error.js │ │ └── no-throw-in-sentry-scope.js ├── feature_flags │ ├── README.md │ ├── flag_schema.ts │ ├── flags.json │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── footer │ ├── components │ │ ├── Legal.tsx │ │ ├── Navigation.tsx │ │ ├── Newsletter.tsx │ │ ├── Partners.tsx │ │ ├── Settings.tsx │ │ ├── Shared.tsx │ │ ├── SocialMedia.tsx │ │ └── StoreMenu.tsx │ ├── homepage-kpis │ │ └── index.tsx │ ├── index.tsx │ ├── package.json │ └── tsconfig.json ├── helpers │ ├── commerce-helpers.ts │ ├── generic-helpers.ts │ ├── index.ts │ ├── logger.ts │ ├── nextjs.tsx │ ├── package.json │ ├── product-helpers.ts │ ├── shopify-helpers.ts │ └── tsconfig.json ├── icons │ ├── flags │ │ ├── AuFlag.tsx │ │ ├── CaFlag.tsx │ │ ├── DeFlag.tsx │ │ ├── EuFlag.tsx │ │ ├── Flag.tsx │ │ ├── FrFlag.tsx │ │ ├── GbFlag.tsx │ │ └── UsFlag.tsx │ ├── index.tsx │ ├── misc │ │ ├── FaIcon.tsx │ │ ├── Globe.tsx │ │ └── Language.tsx │ ├── package.json │ ├── social │ │ ├── FacebookLogo.tsx │ │ ├── InstagramLogo.tsx │ │ ├── RepairEULogo.tsx │ │ ├── RepairOrgLogo.tsx │ │ ├── TiktokLogo.tsx │ │ ├── TwitterLogo.tsx │ │ └── YoutubeLogo.tsx │ └── tsconfig.json ├── ifixit-api-client │ ├── client.tsx │ ├── index.tsx │ ├── package.json │ └── tsconfig.json ├── local-storage │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── menu │ ├── index.ts │ ├── menu.ts │ ├── package.json │ └── tsconfig.json ├── newsletter-sdk │ ├── index.tsx │ ├── package.json │ └── tsconfig.json ├── react-feature-flags │ ├── README.md │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── sentry │ ├── SentryErrorIntegration.tsx │ ├── index.tsx │ ├── package.json │ └── tsconfig.json ├── shopify-storefront-client │ ├── index.tsx │ ├── package.json │ └── tsconfig.json ├── tracking-hooks │ ├── hooks │ │ ├── TrackingContext.ts │ │ └── useTrackedOnClick.tsx │ ├── index.tsx │ ├── package.json │ └── tsconfig.json ├── tsconfig │ ├── README.md │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── animations │ ├── AnimatedList.tsx │ ├── Collapse.tsx │ ├── Fade.tsx │ ├── Slide.tsx │ ├── index.ts │ ├── keyframes.tsx │ ├── useAnimatedList.tsx │ ├── useCSSTransition.tsx │ └── useSize.tsx │ ├── cart │ ├── drawer │ │ ├── CartDrawer.tsx │ │ ├── CartDrawerTrigger.tsx │ │ ├── CartEmptyState.tsx │ │ ├── CartLineItem.tsx │ │ ├── CartLineItemImage.tsx │ │ ├── CrossSell.tsx │ │ ├── CrossSellItem.tsx │ │ └── hooks │ │ │ └── useCartDrawer.tsx │ └── index.ts │ ├── commerce │ ├── ProductPrice.tsx │ ├── hooks │ │ └── useUserPrice.tsx │ └── index.ts │ ├── header │ ├── Header.tsx │ ├── NavigationDrawer.tsx │ ├── NavigationMenu.tsx │ ├── Search.tsx │ ├── UserMenu.tsx │ ├── context.tsx │ └── index.tsx │ ├── hooks │ └── index.ts │ ├── index.tsx │ ├── misc │ ├── BlueDot.tsx │ ├── ConditionalWrapper.tsx │ ├── Globe.tsx │ ├── IconBadge.tsx │ ├── ResponsiveImage │ │ ├── iFixitUtils.ts │ │ ├── index.tsx │ │ └── shopifyUtils.ts │ ├── Wordmark.tsx │ ├── Wrapper.tsx │ └── index.ts │ ├── package.json │ ├── pagination │ ├── index.tsx │ └── usePagination.ts │ ├── slider │ └── index.tsx │ ├── theme │ ├── components │ │ ├── alert.ts │ │ ├── badge.ts │ │ ├── button.ts │ │ ├── icon-badge.ts │ │ ├── pagination.ts │ │ └── product-price.ts │ ├── foundations │ │ ├── breakpoints.ts │ │ ├── colors.ts │ │ ├── fonts.ts │ │ ├── radii.ts │ │ ├── shadow.ts │ │ ├── sizes.ts │ │ ├── space.ts │ │ ├── typography.ts │ │ └── zIndices.ts │ ├── index.ts │ └── styles.ts │ └── tsconfig.json ├── patches └── react-lite-yt-embed@1.2.7.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── startReactCommerce.sh /.env.local.example: -------------------------------------------------------------------------------- 1 | VERDACCIO_AUTH_TOKEN= -------------------------------------------------------------------------------- /.github/actions/playwright-setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup playwright 2 | description: Installs playwright dependencies, and caches the binaries 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - name: Get installed Playwright version 7 | id: playwright-version 8 | run: echo "PLAYWRIGHT_VERSION=$(pnpm list -r "@playwright/test" | grep "@playwright/test" | tr -dc 1-9.)" >> $GITHUB_ENV 9 | shell: bash 10 | 11 | - name: Cache playwright binaries 12 | uses: actions/cache@v3 13 | id: playwright-cache 14 | with: 15 | path: ~/.cache/ms-playwright 16 | key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} 17 | 18 | - name: Install Playwright Dependencies 19 | if: steps.playwright-cache.outputs.cache-hit != 'true' 20 | run: cd frontend && npx playwright install 21 | shell: bash 22 | -------------------------------------------------------------------------------- /.github/actions/pnpm-setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup pnpm 2 | description: Installs pnpm, and caches the data store 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - name: Setup pnpm 7 | uses: pnpm/action-setup@v2.2.4 8 | with: 9 | version: 8.8.0 10 | run_install: false 11 | 12 | - name: Get pnpm store directory 13 | id: pnpm-cache 14 | shell: bash 15 | run: | 16 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 17 | env: 18 | VERDACCIO_AUTH_TOKEN: doesntmatter 19 | 20 | - uses: actions/cache@v3 21 | name: Setup pnpm cache 22 | with: 23 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 24 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 25 | restore-keys: | 26 | ${{ runner.os }}-pnpm-store- 27 | -------------------------------------------------------------------------------- /.github/workflows/codesee-arch-diagram.yml: -------------------------------------------------------------------------------- 1 | # This workflow was added by CodeSee. Learn more at https://codesee.io/ 2 | # This is v2.0 of this workflow file 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request_target: 8 | types: [opened, synchronize, reopened] 9 | 10 | name: CodeSee 11 | 12 | permissions: read-all 13 | 14 | jobs: 15 | codesee: 16 | runs-on: ubuntu-latest 17 | continue-on-error: true 18 | name: Analyze the repo with CodeSee 19 | steps: 20 | - uses: Codesee-io/codesee-action@v2 21 | with: 22 | codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} 23 | codesee-url: https://app.codesee.io 24 | -------------------------------------------------------------------------------- /.github/workflows/datadog.yml: -------------------------------------------------------------------------------- 1 | name: Datadog 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - '**' 7 | types: 8 | - completed 9 | 10 | jobs: 11 | send: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 10 14 | steps: 15 | - uses: iFixit/datadog-actions-metrics@v1 16 | with: 17 | datadog-api-key: ${{ secrets.DATADOG_API_KEY }} 18 | send-pull-request-labels: true 19 | collect-job-metrics: true 20 | -------------------------------------------------------------------------------- /.github/workflows/deploy-strapi.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Strapi 2 | 3 | concurrency: 4 | group: strapi-deploy 5 | cancel-in-progress: false 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: 11 | - main 12 | paths: 13 | - 'backend/**' 14 | 15 | jobs: 16 | build-strapi: 17 | runs-on: 18 | group: react-commerce-production-deploy 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Deploy Strapi 24 | run: backend/strapi-deploy-actions 25 | -------------------------------------------------------------------------------- /.github/workflows/jest.yml: -------------------------------------------------------------------------------- 1 | name: Jest tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | jest: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 18 15 | 16 | - name: Setup and cache pnpm 17 | uses: ./.github/actions/pnpm-setup 18 | - name: Install workspaces 19 | env: 20 | VERDACCIO_AUTH_TOKEN: ${{ secrets.VERDACCIO_AUTH_TOKEN }} 21 | run: pnpm install:all 22 | - name: Run Jest tests 23 | run: pnpm test 24 | env: 25 | ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} 26 | -------------------------------------------------------------------------------- /.github/workflows/tsc.yml: -------------------------------------------------------------------------------- 1 | name: Type Check 2 | 3 | on: push 4 | 5 | jobs: 6 | tsc: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | - name: Install Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | 17 | - name: Setup and cache pnpm 18 | uses: ./.github/actions/pnpm-setup 19 | - name: Install workspaces 20 | env: 21 | VERDACCIO_AUTH_TOKEN: ${{ secrets.VERDACCIO_AUTH_TOKEN }} 22 | run: pnpm install:all 23 | - name: Run Typescript Compiler 24 | run: pnpm type-check 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # OS 4 | .DS_Store 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | tmp 31 | # Rush 32 | **/.rush/temp 33 | 34 | tsconfig.tsbuildinfo 35 | 36 | .env.local 37 | .env 38 | .vercel 39 | 40 | .next -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged && pnpm package-json:lint 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | shamefully-hoist=true 3 | always-auth=false 4 | strict-peer-dependencies=false 5 | registry=https://verdaccio.ubreakit.com 6 | //verdaccio.ubreakit.com/:_authToken=${VERDACCIO_AUTH_TOKEN} -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pnpm-lock.yaml 3 | frontend/.next 4 | frontend/public 5 | backend/.cache 6 | backend/.tmp 7 | backend/build 8 | backend/dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 3, 4 | "singleQuote": true, 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | .tmp/ 2 | .cache/ 3 | .git/ 4 | build/ 5 | node_modules/ 6 | data/ -------------------------------------------------------------------------------- /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # This file is used by govinor to generate preview deployment for the branch 2 | # Every env variable in this file will be passed to the govinor preview deployment. 3 | # Env variable that are only meant for production (e.g. S3_BUCKET) should be commented out. 4 | JWT_SECRET=staging-secret 5 | SEED_DB=true 6 | STRAPI_ADMIN_ENABLE_ADDONS_DANGEROUS_ACTIONS=true 7 | API_TOKEN_SALT='Not_A-s3Cr3t-/Qr5iGP0g==' 8 | SENTRY_DSN=https://95ab6917c0234f80b99bb0be8e720355@o186239.ingest.sentry.io/6475315 9 | # AWS_REGION="us-west-1" 10 | # S3_BUCKET="some-bucket-name" 11 | # APP_KEYS="toBeModified1,toBeModified2" 12 | # SENDGRID_API_KEY='someSecretKey' -------------------------------------------------------------------------------- /backend/.strapi/client/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was automatically generated by Strapi. 3 | * Any modifications made will be discarded. 4 | */ 5 | import graphql from '@strapi/plugin-graphql/strapi-admin'; 6 | import i18N from '@strapi/plugin-i18n/strapi-admin'; 7 | import sentry from '@strapi/plugin-sentry/strapi-admin'; 8 | import usersPermissions from '@strapi/plugin-users-permissions/strapi-admin'; 9 | import publisher from 'strapi-plugin-publisher/strapi-admin'; 10 | import addons from '../../src/plugins/addons/strapi-admin'; 11 | import { renderAdmin } from '@strapi/strapi/admin'; 12 | 13 | renderAdmin(document.getElementById('strapi'), { 14 | plugins: { 15 | graphql: graphql, 16 | i18n: i18N, 17 | sentry: sentry, 18 | 'users-permissions': usersPermissions, 19 | publisher: publisher, 20 | addons: addons, 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine3.18 2 | RUN apk update && apk add python3 build-base 3 | ARG NODE_ENV=production 4 | ENV NODE_ENV=${NODE_ENV} 5 | 6 | WORKDIR /opt/ 7 | COPY --chown=node:node ./package.json ./ 8 | COPY --chown=node:node ./yarn.lock ./ 9 | COPY --chown=node:node ./src/plugins/addons/package.json ./src/plugins/addons/ 10 | COPY --chown=node:node ./src/plugins/addons/yarn.lock ./src/plugins/addons/ 11 | 12 | ENV PATH /opt/node_modules/.bin:$PATH 13 | 14 | RUN yarn config set network-timeout 600000 -g 15 | RUN yarn install:all && yarn cache clean --all 16 | 17 | COPY --chown=node:node ./ . 18 | 19 | RUN NODE_ENV=production yarn build 20 | 21 | EXPOSE 1337 22 | USER node 23 | CMD ["yarn", "start"] 24 | -------------------------------------------------------------------------------- /backend/build-docker-and-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | GIT_REF=$(git rev-parse HEAD) 6 | DOCKER_TAG="strapi:$GIT_REF" 7 | # iFixit AWS ECR for strapi 8 | REGISTRY="884681002735.dkr.ecr.us-east-1.amazonaws.com" 9 | 10 | echo "Building docker image" 11 | docker build -t "$DOCKER_TAG" . 12 | docker tag "$DOCKER_TAG" "$REGISTRY/$DOCKER_TAG" 13 | docker tag "$DOCKER_TAG" "$REGISTRY/strapi:latest" 14 | 15 | echo "Logging into ECR..." 16 | $(aws ecr get-login --no-include-email --region us-east-1) 17 | 18 | echo "Pushing image..." 19 | docker push "$REGISTRY/$DOCKER_TAG" 20 | docker push "$REGISTRY/strapi:latest" 21 | -------------------------------------------------------------------------------- /backend/config/admin.ts: -------------------------------------------------------------------------------- 1 | export default ({ env }) => ({ 2 | auth: { 3 | secret: env('JWT_SECRET'), 4 | }, 5 | apiToken: { 6 | salt: env('API_TOKEN_SALT'), 7 | }, 8 | transfer: { 9 | token: { 10 | salt: env('TRANSFER_TOKEN_SALT'), 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /backend/config/api.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | rest: { 3 | defaultLimit: 25, 4 | maxLimit: 100, 5 | withCount: true, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /backend/config/server.ts: -------------------------------------------------------------------------------- 1 | export default ({ env }) => ({ 2 | host: env('HOST', '0.0.0.0'), 3 | port: env.int('PORT', 1337), 4 | // app: { 5 | // keys: env.array('APP_KEYS'), 6 | // }, 7 | cron: { 8 | enabled: true, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /backend/database/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iFixit/react-commerce/a469d7ce584637e21e7f9157ecf78b55879adc75/backend/database/migrations/.gitkeep -------------------------------------------------------------------------------- /backend/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iFixit/react-commerce/a469d7ce584637e21e7f9157ecf78b55879adc75/backend/favicon.png -------------------------------------------------------------------------------- /backend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 2 | # User-Agent: * 3 | # Disallow: / 4 | -------------------------------------------------------------------------------- /backend/public/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iFixit/react-commerce/a469d7ce584637e21e7f9157ecf78b55879adc75/backend/public/uploads/.gitkeep -------------------------------------------------------------------------------- /backend/src/admin/app.example.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | locales: [ 4 | // 'ar', 5 | // 'fr', 6 | // 'cs', 7 | // 'de', 8 | // 'dk', 9 | // 'es', 10 | // 'he', 11 | // 'id', 12 | // 'it', 13 | // 'ja', 14 | // 'ko', 15 | // 'ms', 16 | // 'nl', 17 | // 'no', 18 | // 'pl', 19 | // 'pt-BR', 20 | // 'pt', 21 | // 'ru', 22 | // 'sk', 23 | // 'sv', 24 | // 'th', 25 | // 'tr', 26 | // 'uk', 27 | // 'vi', 28 | // 'zh-Hans', 29 | // 'zh', 30 | ], 31 | }, 32 | bootstrap(app: any) {}, 33 | }; 34 | -------------------------------------------------------------------------------- /backend/src/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@strapi/typescript-utils/tsconfigs/admin", 3 | "include": ["../plugins/**/admin/src/**/*", "./"], 4 | "exclude": ["node_modules/", "build/", "dist/", "**/*.test.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/api/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iFixit/react-commerce/a469d7ce584637e21e7f9157ecf78b55879adc75/backend/src/api/.gitkeep -------------------------------------------------------------------------------- /backend/src/api/banner/controllers/banner.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * banner controller 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreController('api::banner.banner'); 8 | -------------------------------------------------------------------------------- /backend/src/api/banner/routes/banner.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * banner router 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreRouter('api::banner.banner'); 8 | -------------------------------------------------------------------------------- /backend/src/api/banner/services/banner.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * banner service 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreService('api::banner.banner'); 8 | -------------------------------------------------------------------------------- /backend/src/api/company/content-types/company/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "companies", 4 | "info": { 5 | "singularName": "company", 6 | "pluralName": "companies", 7 | "displayName": "Company" 8 | }, 9 | "options": { 10 | "draftAndPublish": true 11 | }, 12 | "pluginOptions": {}, 13 | "attributes": { 14 | "name": { 15 | "type": "string", 16 | "required": true 17 | }, 18 | "logo": { 19 | "allowedTypes": ["images"], 20 | "type": "media", 21 | "multiple": false 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/api/company/controllers/company.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * company controller 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreController('api::company.company'); 8 | -------------------------------------------------------------------------------- /backend/src/api/company/routes/company.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * company router 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreRouter('api::company.company'); 8 | -------------------------------------------------------------------------------- /backend/src/api/company/services/company.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * company service 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreService('api::company.company'); 8 | -------------------------------------------------------------------------------- /backend/src/api/faq/controllers/faq.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * faq controller 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreController('api::faq.faq'); 8 | -------------------------------------------------------------------------------- /backend/src/api/faq/routes/faq.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * faq router 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreRouter('api::faq.faq'); 8 | -------------------------------------------------------------------------------- /backend/src/api/faq/services/faq.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * faq service 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreService('api::faq.faq'); 8 | -------------------------------------------------------------------------------- /backend/src/api/global/content-types/global/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "singleType", 3 | "collectionName": "globals", 4 | "info": { 5 | "singularName": "global", 6 | "pluralName": "globals", 7 | "displayName": "Global" 8 | }, 9 | "options": { 10 | "draftAndPublish": true 11 | }, 12 | "pluginOptions": { 13 | "i18n": { 14 | "localized": true 15 | } 16 | }, 17 | "attributes": { 18 | "newsletterForm": { 19 | "type": "component", 20 | "repeatable": false, 21 | "pluginOptions": { 22 | "i18n": { 23 | "localized": true 24 | } 25 | }, 26 | "component": "global.newsletter-form", 27 | "required": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/api/global/controllers/global.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * global controller 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreController('api::global.global'); 10 | -------------------------------------------------------------------------------- /backend/src/api/global/routes/global.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * global router. 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreRouter('api::global.global'); 10 | -------------------------------------------------------------------------------- /backend/src/api/global/services/global.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * global service. 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreService('api::global.global'); 10 | -------------------------------------------------------------------------------- /backend/src/api/item-type/content-types/item-type/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "item_types", 4 | "info": { 5 | "singularName": "item-type", 6 | "pluralName": "item-types", 7 | "displayName": "Item Type" 8 | }, 9 | "options": { 10 | "draftAndPublish": true 11 | }, 12 | "pluginOptions": {}, 13 | "attributes": { 14 | "akeneo_code": { 15 | "type": "string", 16 | "required": true, 17 | "unique": true, 18 | "regex": "[a-z0-9_]+" 19 | }, 20 | "fallback_image": { 21 | "allowedTypes": ["images"], 22 | "type": "media", 23 | "multiple": false, 24 | "required": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/api/item-type/controllers/item-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * item-type controller 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreController('api::item-type.item-type'); 8 | -------------------------------------------------------------------------------- /backend/src/api/item-type/routes/item-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * item-type router 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreRouter('api::item-type.item-type'); 8 | -------------------------------------------------------------------------------- /backend/src/api/item-type/services/item-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * item-type service 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreService('api::item-type.item-type'); 8 | -------------------------------------------------------------------------------- /backend/src/api/menu/controllers/menu.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * menu controller 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreController('api::menu.menu'); 10 | -------------------------------------------------------------------------------- /backend/src/api/menu/routes/menu.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * menu router. 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreRouter('api::menu.menu'); 10 | -------------------------------------------------------------------------------- /backend/src/api/menu/services/menu.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * menu service. 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreService('api::menu.menu'); 10 | -------------------------------------------------------------------------------- /backend/src/api/page/controllers/page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * page controller 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreController('api::page.page'); 8 | -------------------------------------------------------------------------------- /backend/src/api/page/routes/page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * page router. 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreRouter('api::page.page'); 8 | -------------------------------------------------------------------------------- /backend/src/api/page/services/page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * page service. 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreService('api::page.page'); 8 | -------------------------------------------------------------------------------- /backend/src/api/product-list/controllers/product-list.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * product-list controller 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreController('api::product-list.product-list'); 10 | -------------------------------------------------------------------------------- /backend/src/api/product-list/routes/product-list.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * product-list router. 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreRouter('api::product-list.product-list'); 10 | -------------------------------------------------------------------------------- /backend/src/api/product-list/services/product-list.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * product-list service. 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreService('api::product-list.product-list'); 10 | -------------------------------------------------------------------------------- /backend/src/api/product/controllers/product.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * product controller 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreController('api::product.product'); 8 | -------------------------------------------------------------------------------- /backend/src/api/product/routes/product.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * product router 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreRouter('api::product.product'); 8 | -------------------------------------------------------------------------------- /backend/src/api/product/services/product.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * product service 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreService('api::product.product'); 8 | -------------------------------------------------------------------------------- /backend/src/api/reusable-section/controllers/reusable-section.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * reusable-section controller 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreController( 8 | 'api::reusable-section.reusable-section' 9 | ); 10 | -------------------------------------------------------------------------------- /backend/src/api/reusable-section/routes/reusable-section.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * reusable-section router 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreRouter( 8 | 'api::reusable-section.reusable-section' 9 | ); 10 | -------------------------------------------------------------------------------- /backend/src/api/reusable-section/services/reusable-section.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * reusable-section service 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreService( 8 | 'api::reusable-section.reusable-section' 9 | ); 10 | -------------------------------------------------------------------------------- /backend/src/api/screwdriver-bit-type/content-types/screwdriver-bit-type/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "screwdriver_bit_types", 4 | "info": { 5 | "singularName": "screwdriver-bit-type", 6 | "pluralName": "screwdriver-bit-types", 7 | "displayName": "ScrewdriverBitType", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "name": { 16 | "type": "string", 17 | "required": true 18 | }, 19 | "driverSize": { 20 | "type": "string", 21 | "required": true 22 | }, 23 | "icon": { 24 | "type": "media", 25 | "multiple": false, 26 | "required": true, 27 | "allowedTypes": ["images"] 28 | }, 29 | "slug": { 30 | "type": "uid" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/api/screwdriver-bit-type/controllers/screwdriver-bit-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * screwdriver-bit-type controller 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreController( 8 | 'api::screwdriver-bit-type.screwdriver-bit-type' 9 | ); 10 | -------------------------------------------------------------------------------- /backend/src/api/screwdriver-bit-type/routes/screwdriver-bit-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * screwdriver-bit-type router 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreRouter( 8 | 'api::screwdriver-bit-type.screwdriver-bit-type' 9 | ); 10 | -------------------------------------------------------------------------------- /backend/src/api/screwdriver-bit-type/services/screwdriver-bit-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * screwdriver-bit-type service 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreService( 8 | 'api::screwdriver-bit-type.screwdriver-bit-type' 9 | ); 10 | -------------------------------------------------------------------------------- /backend/src/api/screwdriver-bit/content-types/screwdriver-bit/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "screwdriver_bits", 4 | "info": { 5 | "singularName": "screwdriver-bit", 6 | "pluralName": "screwdriver-bits", 7 | "displayName": "ScrewdriverBit", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "type": { 16 | "type": "relation", 17 | "relation": "oneToOne", 18 | "target": "api::screwdriver-bit-type.screwdriver-bit-type" 19 | }, 20 | "size": { 21 | "type": "string" 22 | }, 23 | "slug": { 24 | "type": "uid" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/api/screwdriver-bit/controllers/screwdriver-bit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * screwdriver-bit controller 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreController( 8 | 'api::screwdriver-bit.screwdriver-bit' 9 | ); 10 | -------------------------------------------------------------------------------- /backend/src/api/screwdriver-bit/routes/screwdriver-bit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * screwdriver-bit router 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreRouter( 8 | 'api::screwdriver-bit.screwdriver-bit' 9 | ); 10 | -------------------------------------------------------------------------------- /backend/src/api/screwdriver-bit/services/screwdriver-bit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * screwdriver-bit service 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreService( 8 | 'api::screwdriver-bit.screwdriver-bit' 9 | ); 10 | -------------------------------------------------------------------------------- /backend/src/api/social-post/content-types/social-post/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "social_posts", 4 | "info": { 5 | "singularName": "social-post", 6 | "pluralName": "social-posts", 7 | "displayName": "SocialPost", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": true 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "title": { 16 | "type": "string", 17 | "unique": true, 18 | "private": true 19 | }, 20 | "image": { 21 | "type": "media", 22 | "multiple": false, 23 | "required": true, 24 | "allowedTypes": ["images"] 25 | }, 26 | "author": { 27 | "type": "string", 28 | "required": true 29 | }, 30 | "url": { 31 | "type": "string" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/api/social-post/controllers/social-post.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * social-post controller 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreController('api::social-post.social-post'); 8 | -------------------------------------------------------------------------------- /backend/src/api/social-post/routes/social-post.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * social-post router 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreRouter('api::social-post.social-post'); 8 | -------------------------------------------------------------------------------- /backend/src/api/social-post/services/social-post.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * social-post service 3 | */ 4 | 5 | import { factories } from '@strapi/strapi'; 6 | 7 | export default factories.createCoreService('api::social-post.social-post'); 8 | -------------------------------------------------------------------------------- /backend/src/api/store/controllers/store.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * store controller 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreController('api::store.store'); 10 | -------------------------------------------------------------------------------- /backend/src/api/store/routes/store.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * store router. 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreRouter('api::store.store'); 10 | -------------------------------------------------------------------------------- /backend/src/api/store/services/store.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * store service. 5 | */ 6 | 7 | import { factories } from '@strapi/strapi'; 8 | 9 | export default factories.createCoreService('api::store.store'); 10 | -------------------------------------------------------------------------------- /backend/src/bootstrap/index.ts: -------------------------------------------------------------------------------- 1 | import setDefaultPermissions from './permissions'; 2 | 3 | export default async function bootstrap({ strapi }) { 4 | try { 5 | await setDefaultPermissions(strapi); 6 | } catch (err) { 7 | strapi.log.error('💥 Error during bootstrap:', err); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/components/global/newsletter-form.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_global_newsletter_forms", 3 | "info": { 4 | "displayName": "newsletterForm", 5 | "icon": "at" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "title": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "subtitle": { 14 | "type": "string", 15 | "required": true 16 | }, 17 | "inputPlaceholder": { 18 | "type": "string", 19 | "required": true, 20 | "default": "Enter your email" 21 | }, 22 | "callToActionButtonTitle": { 23 | "type": "string", 24 | "required": true, 25 | "default": "Subscribe" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/components/global/person.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_global_people", 3 | "info": { 4 | "displayName": "Person" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "name": { 9 | "type": "string" 10 | }, 11 | "role": { 12 | "type": "string" 13 | }, 14 | "avatar": { 15 | "allowedTypes": ["images"], 16 | "type": "media", 17 | "multiple": false 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/components/menu/link-with-image.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_menu_link_with_images", 3 | "info": { 4 | "displayName": "linkWithImage", 5 | "icon": "image" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "name": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "url": { 14 | "type": "string", 15 | "required": true 16 | }, 17 | "image": { 18 | "allowedTypes": ["images"], 19 | "type": "media", 20 | "multiple": false, 21 | "required": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/components/menu/link.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_menu_links", 3 | "info": { 4 | "displayName": "link", 5 | "icon": "external-link-alt" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "name": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "url": { 14 | "type": "string", 15 | "required": true 16 | }, 17 | "description": { 18 | "type": "richtext" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/components/menu/product-list-link.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_menu_product_list_links", 3 | "info": { 4 | "displayName": "productListLink", 5 | "icon": "anchor" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "name": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "productList": { 14 | "type": "relation", 15 | "relation": "oneToOne", 16 | "target": "api::product-list.product-list" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/components/menu/submenu.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_menu_submenus", 3 | "info": { 4 | "displayName": "submenu", 5 | "icon": "stream" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "name": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "submenu": { 14 | "type": "relation", 15 | "relation": "oneToOne", 16 | "target": "api::menu.menu" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/components/misc/placement.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_misc_placements", 3 | "info": { 4 | "displayName": "Placement", 5 | "description": "" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "productLists": { 10 | "type": "relation", 11 | "relation": "oneToMany", 12 | "target": "api::product-list.product-list" 13 | }, 14 | "showInProductListPages": { 15 | "type": "enumeration", 16 | "enum": [ 17 | "only selected", 18 | "selected and descendants", 19 | "only descendants", 20 | "none" 21 | ], 22 | "required": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/components/page/browse.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_page_browses", 3 | "info": { 4 | "displayName": "browse", 5 | "icon": "search", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "title": { 11 | "type": "string" 12 | }, 13 | "description": { 14 | "type": "richtext" 15 | }, 16 | "image": { 17 | "type": "media", 18 | "multiple": false, 19 | "required": false, 20 | "allowedTypes": ["images"] 21 | }, 22 | "categories": { 23 | "displayName": "category", 24 | "type": "component", 25 | "repeatable": true, 26 | "component": "page.category" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/components/page/call-to-action.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_page_call_to_actions", 3 | "info": { 4 | "displayName": "callToAction", 5 | "icon": "bolt" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "title": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "url": { 14 | "type": "string", 15 | "required": true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/components/page/category.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_page_categories", 3 | "info": { 4 | "displayName": "category", 5 | "icon": "border-all", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "productList": { 11 | "type": "relation", 12 | "relation": "oneToOne", 13 | "target": "api::product-list.product-list" 14 | }, 15 | "description": { 16 | "type": "text" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/components/page/hero.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_page_heroes", 3 | "info": { 4 | "displayName": "hero", 5 | "icon": "pager", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "title": { 11 | "type": "string" 12 | }, 13 | "description": { 14 | "type": "richtext" 15 | }, 16 | "image": { 17 | "type": "media", 18 | "multiple": false, 19 | "required": false, 20 | "allowedTypes": ["images"] 21 | }, 22 | "callToAction": { 23 | "type": "component", 24 | "repeatable": false, 25 | "component": "page.call-to-action" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/components/page/press-quote.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_page_press_quotes", 3 | "info": { 4 | "displayName": "Press Quote" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "company": { 9 | "type": "relation", 10 | "relation": "oneToOne", 11 | "target": "api::company.company" 12 | }, 13 | "text": { 14 | "type": "richtext", 15 | "required": true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/components/page/press.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_page_presses", 3 | "info": { 4 | "displayName": "press quotes" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "title": { 9 | "type": "string" 10 | }, 11 | "description": { 12 | "type": "richtext" 13 | }, 14 | "quotes": { 15 | "type": "component", 16 | "repeatable": true, 17 | "component": "page.press-quote" 18 | }, 19 | "callToAction": { 20 | "type": "component", 21 | "repeatable": false, 22 | "component": "page.call-to-action" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/components/page/split-with-image.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_page_split_with_images", 3 | "info": { 4 | "displayName": "split with image", 5 | "description": "" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "title": { 10 | "type": "string" 11 | }, 12 | "description": { 13 | "type": "richtext" 14 | }, 15 | "callToAction": { 16 | "type": "component", 17 | "repeatable": false, 18 | "component": "page.call-to-action" 19 | }, 20 | "imagePosition": { 21 | "type": "enumeration", 22 | "enum": ["Left", "Right"], 23 | "default": "Right" 24 | }, 25 | "image": { 26 | "type": "media", 27 | "multiple": false, 28 | "required": false, 29 | "allowedTypes": ["images"] 30 | }, 31 | "label": { 32 | "type": "string" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/components/page/stat-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_page_stat_items", 3 | "info": { 4 | "displayName": "stat item" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "label": { 9 | "type": "string", 10 | "required": true 11 | }, 12 | "value": { 13 | "type": "string", 14 | "required": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/components/page/stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_page_stats", 3 | "info": { 4 | "displayName": "stats", 5 | "icon": "chart-line" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "stats": { 10 | "displayName": "stat item", 11 | "type": "component", 12 | "repeatable": true, 13 | "component": "page.stat-item", 14 | "required": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/components/product-list/banner.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_product_list_banners", 3 | "info": { 4 | "displayName": "banner", 5 | "icon": "bullhorn" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "title": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "description": { 14 | "type": "richtext", 15 | "required": true 16 | }, 17 | "callToActionLabel": { 18 | "type": "string", 19 | "required": true 20 | }, 21 | "url": { 22 | "type": "string", 23 | "required": true 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/components/product-list/item-type-override.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_product_list_item_type_overrides", 3 | "info": { 4 | "displayName": "ItemTypeOverride", 5 | "description": "" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "itemType": { 10 | "type": "string", 11 | "required": false 12 | }, 13 | "title": { 14 | "type": "string" 15 | }, 16 | "metaTitle": { 17 | "type": "string" 18 | }, 19 | "description": { 20 | "type": "richtext" 21 | }, 22 | "metaDescription": { 23 | "type": "string" 24 | }, 25 | "tagline": { 26 | "type": "string" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/components/product-list/linked-product-list-set.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_pl_linked_pl_sets", 3 | "info": { 4 | "displayName": "linkedProductListSet", 5 | "icon": "sitemap" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "title": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "productLists": { 14 | "type": "relation", 15 | "relation": "oneToMany", 16 | "target": "api::product-list.product-list" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/components/product-list/related-posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_product_list_related_posts", 3 | "info": { 4 | "displayName": "relatedPosts", 5 | "icon": "newspaper" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "tags": { 10 | "type": "string", 11 | "required": false 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/components/product/bit-table.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_product_bit_tables", 3 | "info": { 4 | "displayName": "bit table", 5 | "description": "" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "title": { 10 | "type": "string", 11 | "required": false 12 | }, 13 | "description": { 14 | "type": "richtext" 15 | }, 16 | "bits": { 17 | "type": "relation", 18 | "relation": "oneToMany", 19 | "target": "api::screwdriver-bit.screwdriver-bit" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/components/product/cross-sell.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_product_cross_sells", 3 | "info": { 4 | "displayName": "cross sell" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "title": { 9 | "type": "string" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/components/product/device-compatibility.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_product_device_compatibilities", 3 | "info": { 4 | "displayName": "device compatibility" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "title": { 9 | "type": "string" 10 | }, 11 | "description": { 12 | "type": "richtext" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/components/product/product-customer-reviews.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_product_product_customer_reviews", 3 | "info": { 4 | "displayName": "product customer reviews" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "title": { 9 | "type": "string" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/components/product/product.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_product_products", 3 | "info": { 4 | "displayName": "product overview" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "addToCartBar": { 9 | "type": "boolean", 10 | "default": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/components/product/replacement-guides.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_product_replacement_guides", 3 | "info": { 4 | "displayName": "replacement guides", 5 | "description": "" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "title": { 10 | "type": "string", 11 | "required": false 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/components/section/banner.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_banners", 3 | "info": { 4 | "displayName": "banner" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "banners": { 9 | "type": "relation", 10 | "relation": "oneToMany", 11 | "target": "api::banner.banner" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/components/section/faqs.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_faqs", 3 | "info": { 4 | "displayName": "faqs" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "title": { 9 | "type": "string" 10 | }, 11 | "description": { 12 | "type": "richtext" 13 | }, 14 | "faqs": { 15 | "type": "relation", 16 | "relation": "oneToMany", 17 | "target": "api::faq.faq" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/components/section/featured-products.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_featured_products", 3 | "info": { 4 | "displayName": "featured products", 5 | "description": "" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "title": { 10 | "type": "string" 11 | }, 12 | "description": { 13 | "type": "richtext" 14 | }, 15 | "productList": { 16 | "type": "relation", 17 | "relation": "oneToOne", 18 | "target": "api::product-list.product-list" 19 | }, 20 | "background": { 21 | "type": "enumeration", 22 | "enum": ["white", "transparent"], 23 | "default": "transparent" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/components/section/lifetime-warranty.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_lifetime_warranties", 3 | "info": { 4 | "displayName": "lifetime warranty" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "title": { 9 | "type": "string" 10 | }, 11 | "description": { 12 | "type": "richtext" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/components/section/quote-card.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_quote_cards", 3 | "info": { 4 | "displayName": "quote card" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "text": { 9 | "type": "richtext", 10 | "required": true 11 | }, 12 | "author": { 13 | "displayName": "Person", 14 | "type": "component", 15 | "repeatable": false, 16 | "component": "global.person" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/components/section/quote-gallery.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_quote_galleries", 3 | "info": { 4 | "displayName": "quote gallery" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "title": { 9 | "type": "string" 10 | }, 11 | "description": { 12 | "type": "richtext" 13 | }, 14 | "quotes": { 15 | "displayName": "quote card", 16 | "type": "component", 17 | "repeatable": true, 18 | "component": "section.quote-card" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/components/section/quote.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_quotes", 3 | "info": { 4 | "displayName": "quote", 5 | "description": "" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "text": { 10 | "type": "text", 11 | "required": true 12 | }, 13 | "author": { 14 | "type": "string" 15 | }, 16 | "image": { 17 | "allowedTypes": ["images"], 18 | "type": "media", 19 | "multiple": false 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/components/section/service-value-propositions.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_service_value_propositions", 3 | "info": { 4 | "displayName": "service value propositions" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "title": { 9 | "type": "string" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/components/section/social-gallery.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_social_galleries", 3 | "info": { 4 | "displayName": "SocialGallery" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "title": { 9 | "type": "string" 10 | }, 11 | "description": { 12 | "type": "richtext" 13 | }, 14 | "posts": { 15 | "type": "relation", 16 | "relation": "oneToMany", 17 | "target": "api::social-post.social-post" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/components/section/stories.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_stories", 3 | "info": { 4 | "displayName": "stories" 5 | }, 6 | "options": {}, 7 | "attributes": { 8 | "title": { 9 | "type": "string" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/components/section/tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_section_tools", 3 | "info": { 4 | "displayName": "Tools", 5 | "description": "" 6 | }, 7 | "options": {}, 8 | "attributes": {} 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/components/store/header.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_store_headers", 3 | "info": { 4 | "displayName": "header", 5 | "icon": "window-maximize" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "menu": { 10 | "type": "relation", 11 | "relation": "oneToOne", 12 | "target": "api::menu.menu" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/components/store/shopify-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_store_shopify_settings", 3 | "info": { 4 | "displayName": "shopifySettings", 5 | "icon": "shopping-bag", 6 | "description": "" 7 | }, 8 | "options": {}, 9 | "attributes": { 10 | "storefrontDomain": { 11 | "type": "string", 12 | "required": true 13 | }, 14 | "storefrontAccessToken": { 15 | "type": "string", 16 | "required": true 17 | }, 18 | "delegateAccessToken": { 19 | "type": "string" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/components/store/social-media-accounts.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectionName": "components_store_social_media_accounts", 3 | "info": { 4 | "displayName": "socialMediaAccounts", 5 | "icon": "address-card" 6 | }, 7 | "options": {}, 8 | "attributes": { 9 | "twitter": { 10 | "type": "string" 11 | }, 12 | "tiktok": { 13 | "type": "string" 14 | }, 15 | "facebook": { 16 | "type": "string" 17 | }, 18 | "instagram": { 19 | "type": "string" 20 | }, 21 | "youtube": { 22 | "type": "string" 23 | }, 24 | "repairOrg": { 25 | "type": "string" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/extensions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iFixit/react-commerce/a469d7ce584637e21e7f9157ecf78b55879adc75/backend/src/extensions/.gitkeep -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import bootstrap from './bootstrap'; 2 | 3 | export default { 4 | /** 5 | * An asynchronous register function that runs before 6 | * your application is initialized. 7 | * 8 | * This gives you an opportunity to extend code. 9 | */ 10 | register(/*{ strapi }*/) {}, 11 | 12 | /** 13 | * An asynchronous bootstrap function that runs before 14 | * your application gets started. 15 | * 16 | * This gives you an opportunity to set up your data model, 17 | * run jobs, or perform some special logic. 18 | */ 19 | // bootstrap(/*{ strapi }*/) {}, 20 | bootstrap: bootstrap, 21 | }; 22 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/README.md: -------------------------------------------------------------------------------- 1 | # Strapi plugin addons 2 | 3 | A quick description of addons. 4 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/admin/src/components/Initializer/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Initializer 4 | * 5 | */ 6 | 7 | import React, { useEffect, useRef } from 'react'; 8 | import pluginId from '../../pluginId'; 9 | 10 | type InitializerProps = { 11 | setPlugin: (id: string) => void; 12 | }; 13 | 14 | const Initializer: React.FC = ({ setPlugin }) => { 15 | const ref = useRef<((id: string) => void) | null>(null); 16 | ref.current = setPlugin; 17 | 18 | useEffect(() => { 19 | if (ref.current) { 20 | ref.current(pluginId); 21 | } 22 | }, []); 23 | 24 | return null; 25 | }; 26 | 27 | export default Initializer; 28 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/admin/src/pluginId.ts: -------------------------------------------------------------------------------- 1 | import pluginPkg from '../../package.json'; 2 | 3 | const pluginId = pluginPkg.name.replace( 4 | /^(@[^-,.][\w,-]+\/|strapi-)plugin-/i, 5 | '' 6 | ); 7 | 8 | export default pluginId; 9 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/admin/src/translations/en.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/admin/src/translations/fr.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/admin/src/utils/getTrad.ts: -------------------------------------------------------------------------------- 1 | import pluginId from '../pluginId'; 2 | 3 | const getTrad = (id: string) => `${pluginId}.${id}`; 4 | 5 | export default getTrad; 6 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/admin/src/utils/path-helpers.ts: -------------------------------------------------------------------------------- 1 | import pluginId from '../pluginId'; 2 | 3 | export function appBasePath() { 4 | return `/plugins/${pluginId}`; 5 | } 6 | 7 | export function homePath() { 8 | return `${appBasePath()}/`; 9 | } 10 | 11 | export function backupPath() { 12 | return `${appBasePath()}/backup`; 13 | } 14 | 15 | export function bulkOperationsPath() { 16 | return `${appBasePath()}/bulk-operations`; 17 | } 18 | 19 | export function joinPaths(...paths: string[]): string { 20 | const cleanedPaths = paths.map((path) => path.replace(/^\/|\/$/g, '')); 21 | 22 | return cleanedPaths.join('/'); 23 | } 24 | 25 | export function convertToRelativePath(path: string) { 26 | return path.replace(/^\//gm, ''); 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/admin/src/utils/react-query-client.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query'; 2 | import { addonsAPI } from './addons-api'; 3 | 4 | const defaultQueryFn = async ({ queryKey }) => { 5 | return addonsAPI.get(queryKey[0]); 6 | }; 7 | 8 | export const queryClient = new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | queryFn: defaultQueryFn, 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@strapi/design-system'; 2 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/helpers/generic-helpers.ts: -------------------------------------------------------------------------------- 1 | const URL_REGEX = /^https?:\/\//i; 2 | 3 | export function parseValidUrl(value: unknown): URL | null { 4 | if (typeof value !== 'string' || value.trim().length === 0) { 5 | return null; 6 | } 7 | 8 | const domain = value.trim(); 9 | const urlString = URL_REGEX.test(domain) ? domain : `https://${domain}`; 10 | 11 | return new URL(urlString); 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import type { Strapi } from '@strapi/strapi'; 2 | import { getAddonsService } from './services'; 3 | 4 | export default async ({ strapi }: { strapi: Strapi }) => { 5 | const isSeedingEnabled = process.env.SEED_DB === 'true'; 6 | const shouldSeed = isSeedingEnabled; 7 | 8 | if (shouldSeed) { 9 | try { 10 | const seedService = getAddonsService(strapi, 'seed'); 11 | await seedService.createAdminUser(); 12 | await seedService.importContentTypes({ 13 | overrideExistingContent: false, 14 | }); 15 | } catch (err: any) { 16 | strapi.log.error('💥 Error while seeding database'); 17 | strapi.log.error(err.message); 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/config/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | default: {}, 3 | validator() {}, 4 | }; 5 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/content-types/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/controllers/content-types.ts: -------------------------------------------------------------------------------- 1 | import type { Strapi } from '@strapi/strapi'; 2 | import type { Context } from 'koa'; 3 | import z from 'zod'; 4 | import { getAddonsService } from '../services'; 5 | 6 | const FindManyContentTypesArgsSchema = z.object({ 7 | kind: z.enum(['collectionType', 'singleType']), 8 | }); 9 | 10 | export default ({ strapi }: { strapi: Strapi }) => ({ 11 | async findManyContentTypes(ctx: Context) { 12 | const contentTypesService = getAddonsService(strapi, 'contentTypes'); 13 | let result = contentTypesService.findManyContentTypes(); 14 | const validatedQuery = FindManyContentTypesArgsSchema.safeParse( 15 | ctx.query 16 | ); 17 | if (validatedQuery.success) { 18 | result = result.filter( 19 | (schema) => schema.kind === validatedQuery.data.kind 20 | ); 21 | } 22 | ctx.body = result; 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import seedController from './seed'; 2 | import bulkOperationsController from './bulk-operations'; 3 | import contentTypesController from './content-types'; 4 | 5 | export default { 6 | contentTypesController, 7 | seedController, 8 | bulkOperationsController, 9 | }; 10 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/destroy.ts: -------------------------------------------------------------------------------- 1 | import type { Strapi } from '@strapi/strapi'; 2 | 3 | export default ({ strapi }: { strapi: Strapi }) => { 4 | // destroy phase 5 | }; 6 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/helpers/server-helpers.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | 3 | export function isStderrAnError(stderr: string) { 4 | const errorKeywords = ['error', 'failed', 'cannot', 'unable to']; 5 | return errorKeywords.some((keyword) => 6 | stderr.toLowerCase().includes(keyword) 7 | ); 8 | } 9 | 10 | export async function ensureDirectoryExists(filePath: string): Promise { 11 | await fs.mkdir(filePath, { recursive: true }); 12 | } 13 | 14 | export function isBlank(value: unknown): boolean { 15 | return value == null || (typeof value === 'string' && value.trim() === ''); 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/index.ts: -------------------------------------------------------------------------------- 1 | import register from './register'; 2 | import bootstrap from './bootstrap'; 3 | import destroy from './destroy'; 4 | import config from './config'; 5 | import contentTypes from './content-types'; 6 | import controllers from './controllers'; 7 | import routes from './routes'; 8 | import middlewares from './middlewares'; 9 | import policies from './policies'; 10 | import services from './services'; 11 | 12 | export default { 13 | register, 14 | bootstrap, 15 | destroy, 16 | config, 17 | controllers, 18 | routes, 19 | services, 20 | contentTypes, 21 | policies, 22 | middlewares, 23 | }; 24 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/policies/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/register.ts: -------------------------------------------------------------------------------- 1 | import type { Strapi } from '@strapi/strapi'; 2 | 3 | export default ({ strapi }: { strapi: Strapi }) => { 4 | // registeration phase 5 | }; 6 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/services/bulk-operations/index.ts: -------------------------------------------------------------------------------- 1 | import type { Strapi } from '@strapi/strapi'; 2 | import { getExportCSV } from './export-csv'; 3 | import { getImportCSV } from './import-csv'; 4 | 5 | export default ({ strapi }: { strapi: Strapi }) => ({ 6 | exportCSV: getExportCSV(strapi), 7 | importCSV: getImportCSV(strapi), 8 | }); 9 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/services/index.ts: -------------------------------------------------------------------------------- 1 | import type { Strapi } from '@strapi/strapi'; 2 | import seed from './seed'; 3 | import bulkOperations from './bulk-operations'; 4 | import contentTypes from './content-types'; 5 | 6 | const services = { 7 | seed, 8 | bulkOperations, 9 | contentTypes, 10 | }; 11 | 12 | type Services = typeof services; 13 | 14 | export const getAddonsService = ( 15 | strapi: Strapi, 16 | serviceName: K 17 | ): ReturnType => { 18 | return strapi.plugin('addons').service(serviceName); 19 | }; 20 | 21 | export default services; 22 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/services/seed/backup/types.ts: -------------------------------------------------------------------------------- 1 | export interface Backup { 2 | filePath: string; 3 | } 4 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/services/seed/custom/index.ts: -------------------------------------------------------------------------------- 1 | import { DataImporter } from './import'; 2 | 3 | export interface SeedResult { 4 | contentTypes: Record; 5 | media: { count: number }; 6 | } 7 | 8 | const FALLBACK_STRAPI_ORIGIN = 'https://main.govinor.com'; 9 | 10 | type ImportContentTypesOptions = { 11 | strapiOrigin?: string; 12 | overrideExistingContent?: boolean; 13 | }; 14 | 15 | export async function importContentTypes({ 16 | strapiOrigin = FALLBACK_STRAPI_ORIGIN, 17 | overrideExistingContent = false, 18 | }: ImportContentTypesOptions): Promise { 19 | const importer = new DataImporter(strapi); 20 | await importer.import(strapiOrigin, { 21 | maxDepth: 3, 22 | overrideExistingContent: overrideExistingContent, 23 | }); 24 | 25 | return { 26 | contentTypes: importer.countsByContentType, 27 | media: { count: importer.mediaCount }, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/server/services/seed/errors/ContentTypeNotFoundError.ts: -------------------------------------------------------------------------------- 1 | export class ContentTypeNotFoundError extends Error { 2 | code: string; 3 | 4 | constructor(contentType: string) { 5 | super(`Content type "${contentType}" not found`); 6 | this.code = 'CONTENT_TYPE_NOT_FOUND'; 7 | } 8 | get [Symbol.toStringTag]() { 9 | return 'ContentTypeNotFoundError'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/strapi-admin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./admin/src').default; 4 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/strapi-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./dist/server'); 4 | -------------------------------------------------------------------------------- /backend/src/plugins/addons/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@strapi/typescript-utils/tsconfigs/server", 3 | 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "rootDir": ".", 7 | "strict": true 8 | }, 9 | 10 | "include": [ 11 | // Include the root directory 12 | "server", 13 | // Force the JSON files in the src folder to be included 14 | "server/**/*.json", 15 | "global.d.ts", 16 | "server/helpers/server-helpers.ts" 17 | ], 18 | 19 | "exclude": [ 20 | "node_modules/", 21 | "dist/", 22 | 23 | // Do not include admin files in the server compilation 24 | "admin/", 25 | // Do not include test files 26 | "**/*.test.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@strapi/typescript-utils/tsconfigs/server", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": ".", 6 | "jsx": "react-jsx", 7 | "lib": ["ES2020", "DOM"] 8 | }, 9 | "include": ["./", "src/**/*.json"], 10 | "exclude": [ 11 | "node_modules/", 12 | "build/", 13 | "dist/", 14 | ".cache/", 15 | ".tmp/", 16 | "src/admin/", 17 | "**/*.test.ts", 18 | "src/plugins/**" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SHOPIFY_STOREFRONT_VERSION=2024-01 2 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ALGOLIA_APP_ID=XQEP3AD9ZT 2 | NEXT_PUBLIC_IFIXIT_ORIGIN=https://www.cominor.com 3 | NEXT_PUBLIC_APP_ORIGIN=http://localhost:3000 4 | NEXT_PUBLIC_STRAPI_ORIGIN=http://localhost:1337 5 | STRAPI_IMAGE_DOMAIN=ifixit-dev-strapi-uploads.s3.us-west-1.amazonaws.com 6 | NEXT_PUBLIC_SENTRY_DSN=https://95ab6917c0234f80b99bb0be8e720355@o186239.ingest.sentry.io/6475315 7 | SENTRY_SAMPLING_ENABLED=true 8 | NEXT_PUBLIC_PIWIK_ENV=none 9 | # If we want to test Google Analytics in dev DEBUG should be true, GTAG_ID should be G-5ZXNWJ73GK, and URL should be 10 | # https://www.google-analytics.com/analytics_debug.js 11 | NEXT_PUBLIC_GA_DEBUG= 12 | NEXT_PUBLIC_GTAG_ID= 13 | NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX_NAME=dev_product_group_en 14 | NEXT_PUBLIC_DEFAULT_STORE_CODE=test 15 | 16 | # Flags 17 | NEXT_PUBLIC_FLAG__APP_ROUTER_TROUBLESHOOTING_PAGE_ENABLED=true 18 | -------------------------------------------------------------------------------- /frontend/.env.local.example: -------------------------------------------------------------------------------- 1 | ALGOLIA_API_KEY= 2 | SENTRY_AUTH_TOKEN= 3 | REDIS_URL= -------------------------------------------------------------------------------- /frontend/.env.test: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ALGOLIA_APP_ID=XQEP3AD9ZT 2 | NEXT_PUBLIC_IFIXIT_ORIGIN=https://www.cominor.com 3 | NEXT_PUBLIC_APP_ORIGIN=http://localhost:3000 4 | NEXT_PUBLIC_STRAPI_ORIGIN=http://localhost:1337 5 | NEXT_PUBLIC_SHOPIFY_STOREFRONT_VERSION=2022-10 6 | STRAPI_IMAGE_DOMAIN=ifixit-dev-strapi-uploads.s3.us-west-1.amazonaws.com 7 | NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX_NAME=dev_product_group_en 8 | NEXT_PUBLIC_DEFAULT_STORE_CODE=test 9 | NODE_OPTIONS="--dns-result-order ipv4first" 10 | NEXT_PUBLIC_DEV_API_AUTH_TOKEN= 11 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/out/* 3 | **/.next/* 4 | **/generated/* 5 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next* 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | !.env 34 | 35 | # vercel 36 | .vercel 37 | 38 | # Sentry 39 | .sentryclirc 40 | tests/playwright/test-results/ 41 | /playwright/.cache/ 42 | 43 | # codegen 44 | lib/strapi-sdk/generated/schema.graphql 45 | -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | always-auth=false 3 | registry=https://verdaccio.ubreakit.com 4 | //verdaccio.ubreakit.com/:_authToken=${VERDACCIO_AUTH_TOKEN} -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | yarn.lock 4 | package-lock.json 5 | public -------------------------------------------------------------------------------- /frontend/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare type NextPageWithLayout

= import('next').NextPage< 7 | P, 8 | IP 9 | > & { 10 | getLayout?: ( 11 | page: import('react').ReactElement, 12 | pageProps: P 13 | ) => import('react').ReactNode; 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/@types/lite-youtube-embed.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'lite-youtube-embed'; 2 | -------------------------------------------------------------------------------- /frontend/app/(defaultLayout)/@storeSelect/default.tsx: -------------------------------------------------------------------------------- 1 | import { getStoreList } from '@models/store'; 2 | import { StoreSelect } from './store-select'; 3 | 4 | export default async function DefaultStoreSelect() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/app/(defaultLayout)/Store/[[...slug]]/data.ts: -------------------------------------------------------------------------------- 1 | import { findPage } from '@models/page/server'; 2 | import { cache } from 'react'; 3 | 4 | export const findPageByPath = cache(async (path: string) => { 5 | return findPage({ path }); 6 | }); 7 | -------------------------------------------------------------------------------- /frontend/app/(defaultLayout)/app-router/Troubleshooting/[device]/[problem]/[wikiid]/page.tsx: -------------------------------------------------------------------------------- 1 | import { flags } from '@config/flags'; 2 | import { notFound } from 'next/navigation'; 3 | 4 | interface WikiPageProps { 5 | params: { 6 | device: string; 7 | problem: string; 8 | wikiid: string; 9 | }; 10 | searchParams: { 11 | disableCacheGets?: string | string[] | undefined; 12 | }; 13 | } 14 | 15 | export default async function WikiPage({ params }: WikiPageProps) { 16 | if (!flags.APP_ROUTER_TROUBLESHOOTING_PAGE_ENABLED) notFound(); 17 | 18 | return ( 19 |

20 | WikiPage: {JSON.stringify(params, null, 2)} 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /frontend/app/(defaultLayout)/components/analytics/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { GTAG_ID, PIWIK_ID } from '@config/env'; 4 | import { Suspense } from 'react'; 5 | import { GoogleTagManager } from './google-tag-manager'; 6 | import { PiwikPro } from './piwik-pro'; 7 | 8 | export function AnalyticsScripts() { 9 | return ( 10 | 11 | {GTAG_ID && } 12 | {PIWIK_ID && } 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/app/(defaultLayout)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DEFAULT_STORE_CODE } from '@config/env'; 2 | import { getLayoutServerSideProps } from '@layouts/default/server'; 3 | import { ReactNode } from 'react'; 4 | import { PageFrame } from './components/page-frame'; 5 | 6 | export default async function DefaultLayout({ 7 | children, 8 | storeSelect, 9 | }: { 10 | children: ReactNode; 11 | storeSelect: React.ReactNode; 12 | }) { 13 | const layoutData = await getLayoutServerSideProps({ 14 | storeCode: DEFAULT_STORE_CODE, 15 | }); 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /frontend/app/(defaultLayout)/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { DEFAULT_STORE_CODE } from '@config/env'; 2 | import { getLayoutServerSideProps } from '@layouts/default/server'; 3 | import { StoreSelect } from './@storeSelect/store-select'; 4 | import { NotFoundPage } from './components/not-found-page'; 5 | import { PageFrame } from './components/page-frame'; 6 | 7 | export default async function NotFound() { 8 | const layoutData = await getLayoutServerSideProps({ 9 | storeCode: DEFAULT_STORE_CODE, 10 | }); 11 | 12 | return ( 13 | } 16 | > 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /frontend/app/_helpers/app-helpers.ts: -------------------------------------------------------------------------------- 1 | import { CACHE_DISABLED, IFIXIT_ORIGIN } from '@config/env'; 2 | import { headers } from 'next/headers'; 3 | 4 | export function shouldSkipCache( 5 | searchParams: Record 6 | ) { 7 | return searchParams.disableCacheGets != null || CACHE_DISABLED; 8 | } 9 | 10 | export function ifixitOrigin(): string { 11 | return devSandboxOrigin() ?? IFIXIT_ORIGIN; 12 | } 13 | 14 | function devSandboxOrigin(): string | null { 15 | const host = 16 | headers().get('x-ifixit-forwarded-host') || 17 | headers().get('x-forwarded-host'); 18 | const isDevProxy = !!host?.match( 19 | /^(?!react-commerce).*(\.cominor\.com|\.ubreakit\.com)$/ 20 | ); 21 | return isDevProxy ? `https://${host}` : null; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/app/_helpers/product-helpers.ts: -------------------------------------------------------------------------------- 1 | import type { Image } from '@models/components/image'; 2 | import type { Product } from '@models/product'; 3 | 4 | export function imagesFor( 5 | product: Product, 6 | selectedVariantId: string 7 | ): Image[] { 8 | const genericImages = product.images.filter((image) => { 9 | return image.variantId === null; 10 | }); 11 | 12 | const selectedVariantImages = product.images.filter( 13 | (image) => image.variantId === selectedVariantId 14 | ); 15 | 16 | const relevantImages = genericImages.concat(selectedVariantImages); 17 | 18 | return relevantImages.length > 0 ? relevantImages : product.fallbackImages; 19 | } 20 | 21 | export function defaultVariantFor(product: Product) { 22 | return ( 23 | product.variants.find( 24 | (variant) => variant.quantityAvailable && variant.quantityAvailable > 0 25 | ) ?? product.variants[0] 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/assets/images/no-image-fixie.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iFixit/react-commerce/a469d7ce584637e21e7f9157ecf78b55879adc75/frontend/assets/images/no-image-fixie.jpeg -------------------------------------------------------------------------------- /frontend/assets/svg/files/index.tsx: -------------------------------------------------------------------------------- 1 | import { chakra } from '@chakra-ui/react'; 2 | import SvgProductListEmptyStateIllustration from './product-list-empty-state-illustration.svg'; 3 | import SvgQualityGuarantee from './quality-guarantee.svg'; 4 | import SvgSearchEmptyStateIllustration from './search-empty-state-illustration.svg'; 5 | 6 | export const ProductListEmptyStateIllustration = chakra( 7 | SvgProductListEmptyStateIllustration 8 | ); 9 | export const QualityGuarantee = chakra(SvgQualityGuarantee); 10 | export const SearchEmptyStateIllustration = chakra( 11 | SvgSearchEmptyStateIllustration 12 | ); 13 | -------------------------------------------------------------------------------- /frontend/assets/svg/files/partners/hp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/assets/svg/files/partners/teenage-engineering.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/assets/svg/files/partners/valve.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/codegen/strapi-schema-config.ts: -------------------------------------------------------------------------------- 1 | import { CodegenConfig } from '@graphql-codegen/cli'; 2 | 3 | const schemaConfig: CodegenConfig = { 4 | overwrite: true, 5 | generates: { 6 | 'lib/strapi-sdk/generated/schema.graphql': { 7 | schema: 'http://localhost:1337/graphql', 8 | plugins: ['schema-ast'], 9 | }, 10 | }, 11 | }; 12 | 13 | export default schemaConfig; 14 | -------------------------------------------------------------------------------- /frontend/components/admin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PageEditMenu'; 2 | -------------------------------------------------------------------------------- /frontend/components/analytics/PixelPing.tsx: -------------------------------------------------------------------------------- 1 | import { PIXEL_PING_URL } from '@config/env'; 2 | 3 | export const PixelPing = ({ 4 | type, 5 | id, 6 | site = 'ifixit', 7 | lang = 'en', 8 | }: { 9 | type: string; 10 | id: number; 11 | site?: string; 12 | lang?: string; 13 | }) => { 14 | if (!PIXEL_PING_URL) { 15 | return null; 16 | } 17 | 18 | const intId = Math.trunc(id); 19 | const key = `${site}/${type}/${intId}/${lang}`; 20 | const url = `${PIXEL_PING_URL}?key=${encodeURIComponent(key)}`; 21 | return ( 22 | // eslint-disable-next-line @next/next/no-img-element 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/components/analytics/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GoogleAnalytics'; 2 | export * from './PiwikiPro'; 3 | -------------------------------------------------------------------------------- /frontend/components/common/ProductRating.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, StackProps, Text } from '@chakra-ui/react'; 2 | import { Rating } from '@components/ui'; 3 | 4 | export type ProductRatingProps = StackProps & { 5 | rating: number; 6 | count: number; 7 | }; 8 | 9 | export const ProductRating = ({ 10 | rating, 11 | count, 12 | ...stackProps 13 | }: ProductRatingProps) => { 14 | if (rating < 4 && count <= 10) { 15 | return null; 16 | } 17 | return ( 18 | 19 | 20 | {count} 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/components/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AppProviders'; 2 | export * from './ProductCard'; 3 | export * from './SecondaryNavbar'; 4 | export * from './PageBreadcrumb'; 5 | export * from './useSearchCache'; 6 | export * from './ProductRating'; 7 | export * from './CompatibleDevice'; 8 | export * from './PrerenderedHTML'; 9 | -------------------------------------------------------------------------------- /frontend/components/community/info.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import OptionsDisplay from './options'; 3 | import VideoDisplay from './video'; 4 | 5 | export default function InfoDisplay({ userLang }: { userLang: string }) { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/components/page/guidePage.tsx: -------------------------------------------------------------------------------- 1 | import { chakra } from '@chakra-ui/react'; 2 | import Head from 'next/head'; 3 | import * as React from 'react'; 4 | import { space } from '@core-ds/primitives'; 5 | 6 | export default function GuidePage({ 7 | title, 8 | children, 9 | }: { 10 | title: string; 11 | children: React.ReactNode; 12 | }) { 13 | const Main = chakra('main'); 14 | 15 | return ( 16 | 17 | 18 | {title + ' - iFixit'} 19 | 20 |
24 | {children} 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/components/sections/BannersSection/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { Banner as SingleBannerSection } from '@models/components/banner'; 4 | import { MultipleBanners } from './MultipleBanners'; 5 | import { SingleBanner } from './SingleBanner'; 6 | 7 | export interface BannersSectionProps { 8 | id: string; 9 | banners: SingleBannerSection[]; 10 | } 11 | 12 | export function BannersSection({ id, banners }: BannersSectionProps) { 13 | if (banners.length === 0) return null; 14 | 15 | if (banners.length === 1) { 16 | return ; 17 | } 18 | 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/components/sections/SectionDescription.tsx: -------------------------------------------------------------------------------- 1 | import { BoxProps } from '@chakra-ui/react'; 2 | import { PrerenderedHTML } from '@components/common'; 3 | 4 | export type SectionDescriptionProps = Omit & { 5 | richText: string; 6 | }; 7 | export function SectionDescription({ 8 | richText, 9 | ...otherProps 10 | }: SectionDescriptionProps) { 11 | return ( 12 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/components/sections/SectionHeaderWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps } from '@chakra-ui/react'; 2 | 3 | export type SectionHeaderWrapper = BoxProps; 4 | 5 | export function SectionHeaderWrapper({ ...props }: SectionHeaderWrapper) { 6 | return ( 7 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/components/sections/SectionHeading.tsx: -------------------------------------------------------------------------------- 1 | import { Heading, HeadingProps } from '@chakra-ui/react'; 2 | 3 | export function SectionHeading(props: HeadingProps) { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /frontend/components/ui/Card.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, FlexProps, forwardRef } from '@chakra-ui/react'; 2 | 3 | export type CardProps = FlexProps & { 4 | className?: string; 5 | }; 6 | 7 | export const Card = forwardRef( 8 | ({ children, className, ...otherProps }, ref) => { 9 | return ( 10 | 19 | {children} 20 | 21 | ); 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /frontend/components/ui/LinkButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps, forwardRef } from '@chakra-ui/react'; 2 | 3 | export const LinkButton = forwardRef((props, ref) => ( 4 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/templates/product-list/sections/FilterableProductsSection/facets/drawer/Panel.tsx: -------------------------------------------------------------------------------- 1 | import { Box, VStack } from '@chakra-ui/react'; 2 | import type { PropsWithChildren } from 'react'; 3 | 4 | type PanelProps = PropsWithChildren<{ 5 | isOpen: boolean; 6 | }>; 7 | 8 | export function Panel({ isOpen, children, ...otherProps }: PanelProps) { 9 | return ( 10 | 24 | 25 | {children} 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /frontend/templates/product-list/sections/FilterableProductsSection/facets/useCountRefinements.tsx: -------------------------------------------------------------------------------- 1 | import { useCurrentRefinements } from 'react-instantsearch'; 2 | 3 | export function useCountRefinements() { 4 | const currentRefinements = useCurrentRefinements(); 5 | return (attributes: string[]) => { 6 | return currentRefinements.items.reduce((acc, item) => { 7 | if (attributes.includes(item.attribute)) { 8 | return acc + item.refinements.length; 9 | } 10 | return acc; 11 | }, 0); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/templates/product-list/sections/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RelatedPostsSection'; 2 | export * from './FilterableProductsSection'; 3 | export * from './HeroSection'; 4 | export * from './ProductListChildrenSection'; 5 | export * from './FeaturedProductListsSection'; 6 | -------------------------------------------------------------------------------- /frontend/templates/product/components/ImagePlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps, Circle, forwardRef } from '@chakra-ui/react'; 2 | import { faImage } from '@fortawesome/pro-duotone-svg-icons'; 3 | import { FaIcon } from '@ifixit/icons'; 4 | 5 | export const ImagePlaceholder = forwardRef( 6 | ({ children, ...otherProps }, ref) => { 7 | return ( 8 | 9 | 10 | 16 | 17 | 18 | ); 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /frontend/templates/product/components/PixelPing.tsx: -------------------------------------------------------------------------------- 1 | import { PixelPing } from '@components/analytics/PixelPing'; 2 | 3 | export const ProductPixelPing = ({ productcode }: { productcode: number }) => { 4 | return ; 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/templates/product/components/SecondaryNavigation.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps } from '@chakra-ui/react'; 2 | import { Wrapper } from '@ifixit/ui'; 3 | 4 | export function SecondaryNavigation({ children, ...other }: BoxProps) { 5 | return ( 6 | 18 | {children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/templates/product/hooks/useIsProductForSale.ts: -------------------------------------------------------------------------------- 1 | import { useAuthenticatedUser } from '@ifixit/auth-sdk'; 2 | import type { Product } from '@pages/api/nextjs/cache/product'; 3 | 4 | export function useIsProductForSale(product: Product): boolean { 5 | const user = useAuthenticatedUser(); 6 | const isProOnlyProduct = product.tags.includes('Pro Only'); 7 | const isProUser = user.data?.is_pro ?? false; 8 | const isForSale = !isProOnlyProduct || (isProOnlyProduct && isProUser); 9 | return product.isEnabled && isForSale; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/templates/product/hooks/useProductReviews.ts: -------------------------------------------------------------------------------- 1 | import { useIFixitApiClient } from '@ifixit/ifixit-api-client'; 2 | import { fetchProductReviews } from '@models/product/reviews'; 3 | import type { Product } from '@pages/api/nextjs/cache/product'; 4 | import { useQuery } from '@tanstack/react-query'; 5 | 6 | const productReviewsKeys = { 7 | reviews(productId: string) { 8 | return ['product-reviews', productId]; 9 | }, 10 | }; 11 | 12 | export function useProductReviews(product: Product) { 13 | const apiClient = useIFixitApiClient(); 14 | const query = useQuery( 15 | productReviewsKeys.reviews(product.iFixitProductId), 16 | () => fetchProductReviews(apiClient, product.iFixitProductId), 17 | { staleTime: Infinity } 18 | ); 19 | return query; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/templates/product/sections/ProductOverviewSection/constants.tsx: -------------------------------------------------------------------------------- 1 | export const PRODUCT_OVERVIEW_SECTION_ID = 'product-overview'; 2 | -------------------------------------------------------------------------------- /frontend/tests/jest/__mocks__/svg.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from 'react'; 2 | 3 | const SvgrMock = React.forwardRef>( 4 | (props, ref) => 5 | ); 6 | 7 | export default SvgrMock; 8 | -------------------------------------------------------------------------------- /frontend/tests/jest/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import dotenv from 'dotenv'; 3 | 4 | dotenv.config({ path: '.env.test' }); 5 | dotenv.config({ path: '.env.local' }); 6 | -------------------------------------------------------------------------------- /frontend/tests/playwright/.gitignore: -------------------------------------------------------------------------------- 1 | .auth -------------------------------------------------------------------------------- /frontend/tests/playwright/fixtures/parts-page.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test'; 2 | 3 | export class PartsPage { 4 | readonly page: Page; 5 | private baseURL: string; 6 | 7 | constructor(page: Page, baseURL: string) { 8 | this.page = page; 9 | this.baseURL = baseURL; 10 | } 11 | 12 | updateBaseURL(baseURL: string) { 13 | this.baseURL = baseURL; 14 | } 15 | 16 | /** 17 | * @description Navigates to the parts page for a product 18 | */ 19 | async goToProductParts(deviceTitle: string) { 20 | await this.page.goto(`${this.baseURL}/Parts/${deviceTitle}`); 21 | } 22 | 23 | /** 24 | * @description Navigates to the parts page for a product's item type 25 | */ 26 | async goToProductPartsItemType(deviceTitle: string, itemType: String) { 27 | await this.page.goto(`${this.baseURL}/Parts/${deviceTitle}/${itemType}`); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/tests/playwright/msw/browser.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw'; 2 | 3 | import { handlers } from './handlers'; 4 | 5 | function bootstrapMockWorker() { 6 | return setupWorker(...handlers); 7 | } 8 | 9 | export default bootstrapMockWorker; 10 | -------------------------------------------------------------------------------- /frontend/tests/playwright/msw/handlers.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'msw'; 2 | import { createRestHandler } from './request-handler'; 3 | 4 | /** 5 | * @see https://mswjs.io/docs/basics/request-handler for 6 | * more information on how to create request handlers for 7 | * rest and graphql requests. 8 | */ 9 | export const handlers: RequestHandler[] = [ 10 | /** 11 | * Prepending the wildcard to the path would capture requests regardless of 12 | * origin. Therefore, API requests to Cominor will also be matched against. 13 | */ 14 | createRestHandler({ 15 | request: { 16 | endpoint: '*/api/2.0/internal/international_store_promotion/buybox', 17 | method: 'get', 18 | }, 19 | response: { 20 | status: 200, 21 | }, 22 | }), 23 | ]; 24 | -------------------------------------------------------------------------------- /frontend/tests/playwright/msw/index.ts: -------------------------------------------------------------------------------- 1 | import bootstrapMockServer from './server'; 2 | import bootstrapMockWorker from './browser'; 3 | 4 | export const setupMocks = async () => { 5 | if (typeof window === 'undefined') { 6 | const mswServer = bootstrapMockServer(); 7 | 8 | mswServer.listen({ 9 | onUnhandledRequest: process.env.CI ? 'bypass' : 'warn', 10 | }); 11 | } else { 12 | const mswWorker = bootstrapMockWorker(); 13 | 14 | await mswWorker.start({ 15 | onUnhandledRequest: process.env.CI ? 'bypass' : 'warn', 16 | }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/tests/playwright/msw/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | 3 | import { handlers } from './handlers'; 4 | 5 | function bootstrapMockServer() { 6 | return setupServer(...handlers); 7 | } 8 | 9 | export default bootstrapMockServer; 10 | -------------------------------------------------------------------------------- /frontend/tests/playwright/product/cross-sell.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '../test-fixtures'; 2 | 3 | test.describe('Product Page Cross-Sell', () => { 4 | test('should add item to cart', async ({ productPage, page }) => { 5 | await productPage.gotoProduct('iphone-6s-plus-replacement-battery'); 6 | 7 | const productCrossSell = page.getByTestId('product-cross-sell'); 8 | const items = productCrossSell.getByTestId('product-cross-sell-item'); 9 | const firstItem = items.first(); 10 | 11 | await firstItem.getByRole('button', { name: 'add to cart' }).click(); 12 | 13 | const firstItemTitle = await firstItem 14 | .getByTestId('product-cross-sell-item-title') 15 | .textContent(); 16 | 17 | const cartDrawerText = await page 18 | .getByTestId('cart-drawer-line-items') 19 | .textContent(); 20 | 21 | expect(cartDrawerText).toContain(firstItemTitle); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/tests/playwright/product/pro-user.spec.ts: -------------------------------------------------------------------------------- 1 | import { interceptLogin } from '../utils'; 2 | import { test, expect } from '../test-fixtures'; 3 | 4 | test.describe('Pro User Test', () => { 5 | test('Pro Discount Applied to Product Price', async ({ productPage }) => { 6 | await productPage.gotoProduct('ipad-2-screw-set'); 7 | 8 | // Get price from page 9 | const originalPrice = await productPage.getCurrentPrice(); 10 | 11 | // Login as pro and reload page 12 | await interceptLogin(productPage.page, { 13 | discount_tier: 'pro_4', 14 | }); 15 | await productPage.page.reload(); 16 | 17 | // Wait until the pro icon is shown. 18 | await productPage.page.waitForSelector('.fa-rectangle-pro'); 19 | 20 | // Assert price on page is lower than step 1 21 | const proPrice = await productPage.getCurrentPrice(); 22 | 23 | expect(proPrice).toBeLessThan(originalPrice); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/analytics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/analytics", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@ifixit/cart-sdk": "workspace:*", 9 | "@ifixit/helpers": "workspace:*", 10 | "@ifixit/auth-sdk": "workspace:*" 11 | }, 12 | "devDependencies": { 13 | "@ifixit/tsconfig": "workspace:*", 14 | "typescript": "5.2.2" 15 | }, 16 | "scripts": { 17 | "build": "", 18 | "clean": "", 19 | "type-check": "tsc --pretty --noEmit" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/analytics/piwik/piwikPush.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | _paq?: { push: (args: any[]) => void }; 4 | } 5 | } 6 | 7 | export function piwikPush(data: any[]) { 8 | if (typeof window === 'undefined') { 9 | return; 10 | } 11 | 12 | const _paq = (window._paq = window._paq || new Array()); 13 | _paq.push(data); 14 | } 15 | -------------------------------------------------------------------------------- /packages/analytics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/app/__mocks__/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the actual module instead of a mock 3 | * and only mocks useAppContext(). 4 | */ 5 | 6 | const app = jest.requireActual('../index.tsx'); 7 | 8 | function useAppContext() { 9 | return { ifixitOrigin: 'https://www.cominor.com' }; 10 | } 11 | 12 | app.useAppContext = useAppContext; 13 | module.exports = app; 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/app", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "dependencies": {}, 8 | "devDependencies": { 9 | "@types/react": "18.2.0", 10 | "@types/react-dom": "18.2.7", 11 | "@ifixit/tsconfig": "workspace:*", 12 | "typescript": "5.2.2", 13 | "react": ">=18.2.0" 14 | }, 15 | "scripts": { 16 | "build": "", 17 | "clean": "", 18 | "type-check": "tsc --pretty --noEmit" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/auth-sdk/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './user'; 2 | -------------------------------------------------------------------------------- /packages/auth-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/auth-sdk", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@ifixit/ui": "workspace:*", 9 | "@ifixit/app": "workspace:*", 10 | "@ifixit/ifixit-api-client": "workspace:*", 11 | "zod": "3.22.4" 12 | }, 13 | "peerDependencies": { 14 | "@tanstack/react-query": "4.x", 15 | "next": "*", 16 | "cookie": "0.5.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "18.2.0", 20 | "@types/react-dom": "18.2.7", 21 | "@ifixit/tsconfig": "workspace:*", 22 | "typescript": "5.2.2" 23 | }, 24 | "scripts": { 25 | "build": "", 26 | "clean": "", 27 | "type-check": "tsc --pretty --noEmit" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/auth-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/bot/commands/index.ts: -------------------------------------------------------------------------------- 1 | import * as shopify from './shopify'; 2 | 3 | export const commands = [shopify]; 4 | -------------------------------------------------------------------------------- /packages/bot/commands/shop/create.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import type { BuilderCallback } from 'yargs'; 3 | import * as delegateToken from './create/delegate-token'; 4 | 5 | const commands = [delegateToken]; 6 | 7 | export const command = 'create '; 8 | 9 | export const describe = chalk.dim('Shopify create commands'); 10 | 11 | export const builder: BuilderCallback = (yargs) => { 12 | return yargs.command(commands as any); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/bot/commands/shopify.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import type { BuilderCallback } from 'yargs'; 3 | import * as create from './shop/create'; 4 | 5 | const commands = [create]; 6 | 7 | export const command = 'shopify '; 8 | 9 | export const aliases = ['shop']; 10 | 11 | export const description = chalk.dim('Shopify related commands'); 12 | 13 | export const builder: BuilderCallback = function (yargs) { 14 | return yargs.command(commands as any); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/bot", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "devDependencies": { 6 | "@ifixit/tsconfig": "workspace:*", 7 | "@types/node": "*", 8 | "@types/prompts": "2.4.1", 9 | "@types/yargs": "17.0.13", 10 | "ts-node": "10.9.1", 11 | "typescript": "5.2.2" 12 | }, 13 | "dependencies": { 14 | "@shopify/shopify-api": "5.2.0", 15 | "chalk": "4.1.2", 16 | "prompts": "2.4.2", 17 | "yargs": "17.6.2" 18 | }, 19 | "scripts": { 20 | "build": "", 21 | "clean": "", 22 | "type-check": "tsc --pretty --noEmit", 23 | "bot": "./cli.ts" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/bot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This is an alias to @tsconfig/node16: https://github.com/tsconfig/bases 3 | "extends": "ts-node/node16/tsconfig.json", 4 | 5 | // Most ts-node options can be specified here using their programmatic names. 6 | "ts-node": { 7 | // It is faster to skip typechecking. 8 | // Remove if you want ts-node to do typechecking. 9 | "transpileOnly": true, 10 | 11 | "files": true, 12 | 13 | "compilerOptions": { 14 | // compilerOptions specified here will override those declared below, 15 | // but *only* in ts-node. Useful if you want ts-node and tsc to use 16 | // different options with a single tsconfig.json. 17 | } 18 | }, 19 | "compilerOptions": { 20 | "isolatedModules": false, 21 | "target": "ESNext", 22 | "moduleResolution": "Node16" 23 | }, 24 | "include": ["."], 25 | "exclude": ["dist", "build", "node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/bot/utils/misc.ts: -------------------------------------------------------------------------------- 1 | export function clearLastLine() { 2 | process.stdout.moveCursor(0, -1); 3 | process.stdout.clearLine(0); 4 | } 5 | 6 | export function clearNpmScriptLogs() { 7 | clearLastLine(); 8 | clearLastLine(); 9 | clearLastLine(); 10 | } 11 | -------------------------------------------------------------------------------- /packages/bot/utils/shopify.ts: -------------------------------------------------------------------------------- 1 | import Shopify from '@shopify/shopify-api'; 2 | 3 | export function getShopClient(shopName: string, accessToken: string) { 4 | return new Shopify.Clients.Rest(getShopDomain(shopName), accessToken); 5 | } 6 | 7 | export function getShopDomain(shopName: string) { 8 | return `${shopName.replace('.myshopify.com', '')}.myshopify.com`; 9 | } 10 | -------------------------------------------------------------------------------- /packages/breadcrumbs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/cart-sdk/hooks/use-cart-line-item.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CartLineItem } from '../types'; 3 | import { useCart } from './use-cart'; 4 | 5 | type UseCartLineItem = { 6 | data: CartLineItem | null; 7 | isLoading: boolean; 8 | isError: boolean; 9 | }; 10 | 11 | export function useCartLineItem(itemcode: string): UseCartLineItem { 12 | const cart = useCart(); 13 | 14 | const isLoading = cart.isLoading; 15 | const isError = cart.isError; 16 | 17 | const data = React.useMemo(() => { 18 | return ( 19 | cart.data?.lineItems.find((item) => item.itemcode === itemcode) ?? null 20 | ); 21 | }, [cart.data, itemcode]); 22 | 23 | return { data, isLoading, isError }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/cart-sdk/index.tsx: -------------------------------------------------------------------------------- 1 | export { useCart } from './hooks/use-cart'; 2 | export { useUpdateLineItemQuantity } from './hooks/use-update-line-item-quantity'; 3 | export { useAddToCart } from './hooks/use-add-to-cart'; 4 | export type { AddToCartInput } from './hooks/use-add-to-cart'; 5 | export { useRemoveLineItem } from './hooks/use-remove-line-item'; 6 | export { useCheckout } from './hooks/use-checkout'; 7 | export { useCartLineItem } from './hooks/use-cart-line-item'; 8 | export type { Cart, CartLineItem } from './types'; 9 | export { CartError } from './utils'; 10 | -------------------------------------------------------------------------------- /packages/cart-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/cart-sdk/utils.ts: -------------------------------------------------------------------------------- 1 | export const cartKeys = { 2 | cart: ['cart'], 3 | checkoutUrl: ['cart', 'checkoutUrl'], 4 | }; 5 | 6 | export enum CartError { 7 | EmptyCart = 'empty_cart', 8 | UnknownError = 'unknown_error', 9 | } 10 | -------------------------------------------------------------------------------- /packages/eslint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-new-error': require('./rules/no-new-error'), 4 | 'no-throw-in-sentry-scope': require('./rules/no-throw-in-sentry-scope'), 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/eslint-plugin", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "scripts": {}, 6 | "keywords": [ 7 | "eslint-plugin" 8 | ], 9 | "license": "MIT" 10 | } 11 | -------------------------------------------------------------------------------- /packages/feature_flags/flag_schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The list of feature flags which can be enabled/disabled. 3 | */ 4 | export type FlagList = Record; 5 | 6 | /** 7 | * If true/false, the flag is enabled or disabled globally. If a 8 | * FlagSettingsObject, the flag is enabled or disabled according to the 9 | * configuration in the object. 10 | */ 11 | export type FlagSettings = boolean | FlagSettingsObject; 12 | 13 | export interface FlagSettingsObject { 14 | userToggle: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /packages/feature_flags/flags.json: -------------------------------------------------------------------------------- 1 | { 2 | "extended-related-problems": true 3 | } 4 | -------------------------------------------------------------------------------- /packages/feature_flags/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/feature_flags", 3 | "version": "0.0.0", 4 | "main": "./index.ts", 5 | "types": "./index.ts", 6 | "license": "MIT", 7 | "exports": { 8 | ".": "./index.ts" 9 | }, 10 | "dependencies": { 11 | "@ifixit/local-storage": "workspace:*" 12 | }, 13 | "devDependencies": { 14 | "@ifixit/tsconfig": "workspace:*", 15 | "typescript": "5.2.2" 16 | }, 17 | "scripts": { 18 | "build": "", 19 | "clean": "", 20 | "type-check": "tsc --pretty --noEmit" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/feature_flags/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"], 5 | "compilerOptions": { 6 | "resolveJsonModule": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/footer/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './components/Legal'; 2 | export * from './components/Navigation'; 3 | export * from './components/Partners'; 4 | export * from './components/Settings'; 5 | export * from './components/Shared'; 6 | export * from './components/SocialMedia'; 7 | export * from './components/StoreMenu'; 8 | export * from './homepage-kpis'; 9 | -------------------------------------------------------------------------------- /packages/footer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generic-helpers'; 2 | export * from './commerce-helpers'; 3 | export * from './product-helpers'; 4 | export * from './shopify-helpers'; 5 | export * from './logger'; 6 | -------------------------------------------------------------------------------- /packages/helpers/nextjs.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSidePropsContext } from 'next'; 2 | 3 | export function urlFromContext(context: GetServerSidePropsContext): string { 4 | const protocol = context.req.headers.referer?.split('://')[0] || 'https'; 5 | return `${protocol}://${context.req.headers.host}${context.resolvedUrl}`; 6 | } 7 | -------------------------------------------------------------------------------- /packages/helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/helpers", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "exports": { 8 | ".": "./index.ts", 9 | "./logger": "./logger.ts", 10 | "./nextjs": "./nextjs.tsx" 11 | }, 12 | "devDependencies": { 13 | "@ifixit/tsconfig": "workspace:*", 14 | "typescript": "5.2.2" 15 | }, 16 | "scripts": { 17 | "build": "", 18 | "clean": "", 19 | "type-check": "tsc --pretty --noEmit" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/helpers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/icons/flags/CaFlag.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function SvgCaFlag(props: React.SVGProps) { 4 | return ( 5 | 11 | 12 | 13 | 17 | 18 | ); 19 | } 20 | 21 | export default SvgCaFlag; 22 | -------------------------------------------------------------------------------- /packages/icons/flags/DeFlag.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function SvgDeFlag(props: React.SVGProps) { 4 | return ( 5 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | export default SvgDeFlag; 19 | -------------------------------------------------------------------------------- /packages/icons/flags/FrFlag.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function SvgFrFlag(props: React.SVGProps) { 4 | return ( 5 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | export default SvgFrFlag; 19 | -------------------------------------------------------------------------------- /packages/icons/flags/UsFlag.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function SvgUsFlag(props: React.SVGProps) { 4 | return ( 5 | 11 | 12 | 13 | 17 | 21 | 22 | ); 23 | } 24 | 25 | export default SvgUsFlag; 26 | -------------------------------------------------------------------------------- /packages/icons/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './misc/FaIcon'; 2 | export { default as Globe } from './misc/Globe'; 3 | export { default as Language } from './misc/Language'; 4 | export { default as FacebookLogo } from './social/FacebookLogo'; 5 | export { default as RepairOrgLogo } from './social/RepairOrgLogo'; 6 | export { default as RepairEULogo } from './social/RepairEULogo'; 7 | export { default as TwitterLogo } from './social/TwitterLogo'; 8 | export { default as TiktokLogo } from './social/TiktokLogo'; 9 | export { default as YoutubeLogo } from './social/YoutubeLogo'; 10 | export { default as InstagramLogo } from './social/InstagramLogo'; 11 | export { default as Flag, FlagCountryCode } from './flags/Flag'; 12 | -------------------------------------------------------------------------------- /packages/icons/misc/FaIcon.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from 'react'; 2 | import { ChakraComponent, ChakraProps, chakra } from '@chakra-ui/react'; 3 | import { 4 | FontAwesomeIcon, 5 | FontAwesomeIconProps, 6 | } from '@fortawesome/react-fontawesome'; 7 | 8 | export const FaIcon = chakra(FontAwesomeIcon, { 9 | baseStyle: { 10 | display: 'flex', 11 | alignItems: 'center', 12 | }, 13 | }); 14 | 15 | export type FaIconProps = React.ComponentProps; 16 | -------------------------------------------------------------------------------- /packages/icons/social/FacebookLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function SvgFacebookLogo(props: React.SVGProps) { 4 | return ( 5 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default SvgFacebookLogo; 17 | -------------------------------------------------------------------------------- /packages/icons/social/RepairOrgLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function SvgRepairOrgLogo(props: React.SVGProps) { 4 | return ( 5 | 11 | 16 | 20 | 21 | ); 22 | } 23 | 24 | export default SvgRepairOrgLogo; 25 | -------------------------------------------------------------------------------- /packages/icons/social/TiktokLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function SvgTiktokLogo(props: React.SVGProps) { 4 | return ( 5 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default SvgTiktokLogo; 17 | -------------------------------------------------------------------------------- /packages/icons/social/TwitterLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function SvgTwitterLogo(props: React.SVGProps) { 4 | return ( 5 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default SvgTwitterLogo; 17 | -------------------------------------------------------------------------------- /packages/icons/social/YoutubeLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function SvgYoutubeLogo(props: React.SVGProps) { 4 | return ( 5 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default SvgYoutubeLogo; 17 | -------------------------------------------------------------------------------- /packages/icons/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/ifixit-api-client/client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useAppContext } from '@ifixit/app'; 4 | import * as React from 'react'; 5 | import { IFixitAPIClient } from '.'; 6 | 7 | /** 8 | * Get the iFixit API client. 9 | */ 10 | export function useIFixitApiClient() { 11 | const appContext = useAppContext(); 12 | 13 | const client = React.useMemo(() => { 14 | return new IFixitAPIClient({ 15 | origin: appContext.ifixitOrigin, 16 | }); 17 | }, [appContext.ifixitOrigin]); 18 | 19 | return client; 20 | } 21 | -------------------------------------------------------------------------------- /packages/ifixit-api-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/ifixit-api-client", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@ifixit/app": "workspace:*", 9 | "@ifixit/helpers": "workspace:*" 10 | }, 11 | "devDependencies": { 12 | "@ifixit/tsconfig": "workspace:*", 13 | "@types/react": "18.2.0", 14 | "@types/react-dom": "18.2.7", 15 | "react": "18.2.0", 16 | "typescript": "5.2.2" 17 | }, 18 | "peerDependencies": { 19 | "react": "18.2.0", 20 | "@ifixit/sentry": "workspace:*" 21 | }, 22 | "scripts": { 23 | "build": "", 24 | "clean": "", 25 | "type-check": "tsc --pretty --noEmit" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/ifixit-api-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/local-storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/local-storage", 3 | "version": "0.0.0", 4 | "main": "./index.ts", 5 | "types": "./index.ts", 6 | "license": "MIT", 7 | "exports": { 8 | ".": "./index.ts" 9 | }, 10 | "devDependencies": { 11 | "@ifixit/tsconfig": "workspace:*", 12 | "typescript": "5.2.2" 13 | }, 14 | "scripts": { 15 | "build": "", 16 | "clean": "", 17 | "type-check": "tsc --pretty --noEmit" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/local-storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './menu'; 2 | -------------------------------------------------------------------------------- /packages/menu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/menu", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "exports": { 8 | ".": "./index.ts" 9 | }, 10 | "scripts": { 11 | "build": "", 12 | "clean": "", 13 | "type-check": "tsc --pretty --noEmit" 14 | }, 15 | "devDependencies": { 16 | "@ifixit/tsconfig": "workspace:*", 17 | "typescript": "5.2.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/menu/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/newsletter-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/newsletter-sdk", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@ifixit/tracking-hooks": "workspace:*", 9 | "@ifixit/helpers": "workspace:*", 10 | "@ifixit/ifixit-api-client": "workspace:*" 11 | }, 12 | "peerDependencies": { 13 | "@tanstack/react-query": "4.x", 14 | "react": "18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "18.2.0", 18 | "@types/react-dom": "18.2.7", 19 | "@ifixit/tsconfig": "workspace:*", 20 | "typescript": "5.2.2", 21 | "react": "18.2.0" 22 | }, 23 | "scripts": { 24 | "build": "", 25 | "clean": "", 26 | "type-check": "tsc --pretty --noEmit" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/newsletter-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-feature-flags/README.md: -------------------------------------------------------------------------------- 1 | This is a React wrapper for the feature flag system in @ifixit/feature_flags. 2 | It's designed to ensure that the server and client rendering line up as much as 3 | possible. 4 | 5 | ## Usage 6 | 7 | Import `useFlag` from this package. Use it in a component like so: 8 | 9 | ```ts 10 | const flag = useFlag('flagname'); 11 | ``` 12 | 13 | Only valid flag names will typecheck; to add a flag, see the docs for @ifixit/feature_flags 14 | -------------------------------------------------------------------------------- /packages/react-feature-flags/index.ts: -------------------------------------------------------------------------------- 1 | import { FlagKey, checkFlag } from '@ifixit/feature_flags'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | export function useFlag(flagName: FlagKey) { 5 | const [flag, setFlag] = useState(false); 6 | useEffect(() => { 7 | setFlag(checkFlag(flagName)); 8 | }); 9 | return flag; 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-feature-flags/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/react-feature-flags", 3 | "version": "0.0.0", 4 | "main": "./index.ts", 5 | "types": "./index.ts", 6 | "license": "MIT", 7 | "exports": { 8 | ".": "./index.ts" 9 | }, 10 | "devDependencies": { 11 | "@ifixit/tsconfig": "workspace:*", 12 | "typescript": "5.2.2" 13 | }, 14 | "peerDependencies": { 15 | "react": "^18.0.0" 16 | }, 17 | "dependencies": { 18 | "@ifixit/feature_flags": "workspace:*" 19 | }, 20 | "scripts": { 21 | "build": "", 22 | "clean": "", 23 | "type-check": "tsc --pretty --noEmit" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-feature-flags/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"], 5 | "compilerOptions": { 6 | "resolveJsonModule": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/sentry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/sentry", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@ifixit/helpers": "workspace:*", 9 | "@sentry/nextjs": "7.93.0" 10 | }, 11 | "peerDependencies": { 12 | "next": "*", 13 | "react": "18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": ">=7.0.0 <8.0.0", 17 | "@ifixit/tsconfig": "workspace:*", 18 | "next": "^10.0.8 || ^11.0 || ^12.0 || ^13.0", 19 | "react": ">=18.2.0", 20 | "react-dom": ">=18.2.0", 21 | "typescript": "5.2.2" 22 | }, 23 | "scripts": { 24 | "build": "", 25 | "clean": "", 26 | "type-check": "tsc --pretty --noEmit" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/sentry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/shopify-storefront-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/shopify-storefront-client", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "@types/react": "18.2.0", 9 | "@types/react-dom": "18.2.7", 10 | "@ifixit/tsconfig": "workspace:*", 11 | "typescript": "5.2.2" 12 | }, 13 | "scripts": { 14 | "build": "", 15 | "clean": "", 16 | "type-check": "tsc --pretty --noEmit" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/shopify-storefront-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/tracking-hooks/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './hooks/useTrackedOnClick'; 2 | export * from './hooks/TrackingContext'; 3 | -------------------------------------------------------------------------------- /packages/tracking-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/tracking-hooks", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "exports": { 8 | ".": "./index.tsx" 9 | }, 10 | "scripts": { 11 | "build": "", 12 | "clean": "", 13 | "type-check": "tsc --pretty --noEmit" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "18.2.0", 17 | "@ifixit/tsconfig": "workspace:*", 18 | "typescript": "5.2.2", 19 | "react": "18.2.0" 20 | }, 21 | "peerDependencies": { 22 | "react": "18.2.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/tracking-hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # `tsconfig` 2 | 3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. 4 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noEmit": true, 12 | "incremental": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve" 18 | }, 19 | "include": [ 20 | "src", 21 | "next-env.d.ts", 22 | "**/*.ts", 23 | "**/*.tsx", 24 | "**/*.js", 25 | "@types/index.d.ts", 26 | "./test/jest-setup.ts" 27 | ], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ifixit/tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "files": [ 7 | "base.json", 8 | "nextjs.json", 9 | "react-library.json" 10 | ], 11 | "scripts": { 12 | "build": "", 13 | "clean": "" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "lib": ["ES2015", "dom", "dom.iterable"], 7 | "module": "ESNext", 8 | "target": "ES6", 9 | "jsx": "react-jsx" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/animations/Slide.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | 4 | interface SlideProps { 5 | show: boolean | undefined | null; 6 | } 7 | 8 | export function Slide({ 9 | show = false, 10 | children, 11 | }: React.PropsWithChildren) { 12 | return ( 13 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/ui/animations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AnimatedList'; 2 | export * from './useAnimatedList'; 3 | export * from './Collapse'; 4 | export * from './Fade'; 5 | export * from './Slide'; 6 | export * from './useSize'; 7 | -------------------------------------------------------------------------------- /packages/ui/animations/useSize.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | export function useSize() { 4 | const ref = useRef(null); 5 | const [size, setSize] = useState(null); 6 | 7 | useEffect(() => { 8 | const element = ref.current; 9 | 10 | if (element) { 11 | setSize(element.getBoundingClientRect()); 12 | const resizeObserver = new ResizeObserver(() => { 13 | setSize(element.getBoundingClientRect()); 14 | }); 15 | 16 | resizeObserver.observe(element); 17 | 18 | return () => resizeObserver.disconnect(); 19 | } 20 | }, []); 21 | 22 | return [ref, size] as const; 23 | } 24 | -------------------------------------------------------------------------------- /packages/ui/cart/drawer/CartEmptyState.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Circle, Text, VStack } from '@chakra-ui/react'; 2 | import { faCartCircleExclamation } from '@fortawesome/pro-duotone-svg-icons'; 3 | import { FaIcon } from '@ifixit/icons'; 4 | 5 | export interface CartEmptyStateProps { 6 | onClose: () => void; 7 | } 8 | 9 | export function CartEmptyState({ onClose }: CartEmptyStateProps) { 10 | return ( 11 | 12 | 13 | 14 | 15 | Your cart is empty 16 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/ui/cart/index.ts: -------------------------------------------------------------------------------- 1 | export * from './drawer/CartDrawer'; 2 | export * from './drawer/hooks/useCartDrawer'; 3 | -------------------------------------------------------------------------------- /packages/ui/commerce/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProductPrice'; 2 | export * from './hooks/useUserPrice'; 3 | -------------------------------------------------------------------------------- /packages/ui/header/context.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type Context = { 4 | hiddenBar: { 5 | isOpen: boolean; 6 | open: () => void; 7 | close: () => void; 8 | }; 9 | navigation: { 10 | isOpen: boolean; 11 | toggleButtonRef: React.RefObject; 12 | toggle: () => void; 13 | close: () => void; 14 | }; 15 | }; 16 | 17 | export const HeaderContext = React.createContext(null); 18 | 19 | export const useHeaderContext = () => { 20 | const context = React.useContext(HeaderContext); 21 | if (!context) { 22 | throw new Error('useHeaderContext must be used within a Header'); 23 | } 24 | return context; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/ui/header/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Header'; 2 | export * from './NavigationMenu'; 3 | export * from './NavigationDrawer'; 4 | export * from './Search'; 5 | export * from './UserMenu'; 6 | export { useHeaderContext } from './context'; 7 | -------------------------------------------------------------------------------- /packages/ui/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './header'; 2 | export * from './pagination'; 3 | export * from './hooks'; 4 | export * from './misc'; 5 | export * from './theme'; 6 | export * from './cart'; 7 | export * from './commerce'; 8 | export * from './slider'; 9 | export * from './animations'; 10 | -------------------------------------------------------------------------------- /packages/ui/misc/BlueDot.tsx: -------------------------------------------------------------------------------- 1 | import { Circle, SquareProps } from '@chakra-ui/react'; 2 | 3 | export function BlueDot(props: SquareProps) { 4 | return ( 5 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/misc/ConditionalWrapper.tsx: -------------------------------------------------------------------------------- 1 | type ConditionalWrapperProps = React.PropsWithChildren<{ 2 | condition: boolean; 3 | wrapper: (children: React.ReactNode) => React.ReactNode; 4 | }>; 5 | 6 | export function ConditionalWrapper({ 7 | condition, 8 | wrapper, 9 | children, 10 | }: ConditionalWrapperProps) { 11 | return condition ? <>{wrapper(children)} : <>{children}; 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/misc/ResponsiveImage/shopifyUtils.ts: -------------------------------------------------------------------------------- 1 | import { ImageLoaderProps } from 'next/image'; 2 | 3 | const BASE_IMAGE_SIZE = 352; 4 | 5 | const shopifyImageRegExp = new RegExp( 6 | /cdn\.shopify\.com|cdn\.shopifycdn\.net|shopify-assets\.shopifycdn\.com|shopify-assets\.shopifycdn\.net/ 7 | ); 8 | export function isShopifyImage(src: string) { 9 | return shopifyImageRegExp.test(src); 10 | } 11 | 12 | export function getShopifyImageLoader() { 13 | return ({ src, width }: ImageLoaderProps) => { 14 | const newUrl = new URL(src); 15 | 16 | if (width) { 17 | let finalWidth: string; 18 | 19 | if (typeof width === 'string') { 20 | finalWidth = BASE_IMAGE_SIZE.toString(); 21 | } else { 22 | finalWidth = width.toString(); 23 | } 24 | 25 | newUrl.searchParams.append('width', finalWidth); 26 | } 27 | 28 | return newUrl.toString(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/ui/misc/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps } from '@chakra-ui/react'; 2 | 3 | export type WrapperProps = BoxProps; 4 | 5 | export function Wrapper({ ...props }: WrapperProps) { 6 | return ( 7 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui/misc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConditionalWrapper'; 2 | export * from './Wordmark'; 3 | export * from './Wrapper'; 4 | export * from './ResponsiveImage'; 5 | export * from './IconBadge'; 6 | export * from './BlueDot'; 7 | -------------------------------------------------------------------------------- /packages/ui/theme/components/badge.ts: -------------------------------------------------------------------------------- 1 | import { ComponentStyleConfig } from '@chakra-ui/react'; 2 | 3 | const Badge: ComponentStyleConfig = { 4 | baseStyle: { 5 | px: 1.5, 6 | py: 1, 7 | fontWeight: 'semibold', 8 | fontSize: 'sm', 9 | lineHeight: '1em', 10 | borderRadius: 'base', 11 | textTransform: 'none', 12 | }, 13 | variants: { 14 | outline: (props) => { 15 | const schemeColors = props.theme.colors[props.colorScheme]; 16 | return { 17 | bg: schemeColors[100], 18 | color: schemeColors[700], 19 | boxShadow: `inset 0 0 0px 1px ${schemeColors[300]}`, 20 | }; 21 | }, 22 | }, 23 | defaultProps: { 24 | variant: 'outline', 25 | }, 26 | }; 27 | 28 | export default Badge; 29 | -------------------------------------------------------------------------------- /packages/ui/theme/components/button.ts: -------------------------------------------------------------------------------- 1 | import { ComponentStyleConfig } from '@chakra-ui/react'; 2 | 3 | const Button: ComponentStyleConfig = { 4 | baseStyle: { 5 | borderRadius: 'base', 6 | flexShrink: 0, 7 | }, 8 | }; 9 | 10 | export default Button; 11 | -------------------------------------------------------------------------------- /packages/ui/theme/foundations/breakpoints.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOverride } from '@chakra-ui/react'; 2 | import { breakpoint as primitiveBreakpoint } from '@core-ds/primitives'; 3 | 4 | export const breakpoints: ThemeOverride['breakpoints'] = { 5 | sm: primitiveBreakpoint.sm, 6 | md: primitiveBreakpoint.md, 7 | mdPlus: `769px`, // chakra uses min-width @media queries, so we need 'md' + 1px 8 | lg: primitiveBreakpoint.lg, 9 | xl: primitiveBreakpoint.xl, 10 | '2xl': primitiveBreakpoint['2xl'], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/ui/theme/foundations/fonts.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOverride } from '@chakra-ui/react'; 2 | import { fontFamily as primitiveFontFamily } from '@core-ds/primitives'; 3 | 4 | export const fonts: ThemeOverride['fonts'] = { 5 | body: primitiveFontFamily.sansSystem, 6 | heading: primitiveFontFamily.sansSystem, 7 | mono: primitiveFontFamily.monoSystem, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/ui/theme/foundations/radii.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOverride } from '@chakra-ui/react'; 2 | import { borderRadius as primitiveBorderRadius } from '@core-ds/primitives'; 3 | 4 | export const radii: ThemeOverride['radii'] = { 5 | sm: primitiveBorderRadius.sm, 6 | md: primitiveBorderRadius.md, 7 | lg: primitiveBorderRadius.lg, 8 | xl: primitiveBorderRadius.xl, 9 | base: primitiveBorderRadius.md, 10 | full: primitiveBorderRadius.pill, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/ui/theme/foundations/shadow.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOverride } from '@chakra-ui/react'; 2 | import { shadow as primitiveShadow } from '@core-ds/primitives'; 3 | 4 | export const shadow: ThemeOverride['shadow'] = { 5 | sm: primitiveShadow[0], 6 | md: primitiveShadow[1], 7 | lg: primitiveShadow[2], 8 | xl: primitiveShadow[3], 9 | '2xl': primitiveShadow[4], 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ui/theme/foundations/sizes.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOverride } from '@chakra-ui/react'; 2 | 3 | export const sizes: ThemeOverride['sizes'] = { 4 | header: '68px', 5 | }; 6 | -------------------------------------------------------------------------------- /packages/ui/theme/foundations/space.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOverride } from '@chakra-ui/react'; 2 | 3 | export const space: ThemeOverride['space'] = { 4 | px: '1px', 5 | 0.5: '2px', 6 | 1: '4px', 7 | 1.5: '6px', 8 | 2: '8px', 9 | 2.5: '10px', 10 | 3: '12px', 11 | 3.5: '14px', 12 | 4: '16px', 13 | 4.5: '18px', 14 | 5: '20px', 15 | 6: '24px', 16 | 7: '28px', 17 | 8: '32px', 18 | 9: '36px', 19 | 10: '40px', 20 | 12: '48px', 21 | 14: '56px', 22 | 16: '64px', 23 | 20: '80px', 24 | 24: '96px', 25 | 28: '112px', 26 | 32: '128px', 27 | 36: '144px', 28 | 40: '160px', 29 | 44: '176px', 30 | 48: '192px', 31 | 52: '208px', 32 | 56: '224px', 33 | 60: '240px', 34 | 64: '256px', 35 | 72: '288px', 36 | 80: '304px', 37 | 96: '384px', 38 | }; 39 | -------------------------------------------------------------------------------- /packages/ui/theme/foundations/typography.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOverride } from '@chakra-ui/react'; 2 | import { fontSize as primitiveFontSize } from '@core-ds/primitives'; 3 | 4 | export const fontSize: ThemeOverride['fontSize'] = { 5 | sm: primitiveFontSize.sm, 6 | md: primitiveFontSize.md, 7 | lg: primitiveFontSize.lg, 8 | xl: primitiveFontSize.xl, 9 | '2xl': primitiveFontSize['2xl'], 10 | '3xl': primitiveFontSize['3xl'], 11 | '4xl': primitiveFontSize['4xl'], 12 | '5xl': primitiveFontSize['5xl'], 13 | '6xl': primitiveFontSize['6xl'], 14 | '7xl': primitiveFontSize['7xl'], 15 | '8xl': primitiveFontSize['8xl'], 16 | '9xl': primitiveFontSize['9xl'], 17 | }; 18 | -------------------------------------------------------------------------------- /packages/ui/theme/foundations/zIndices.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOverride } from '@chakra-ui/react'; 2 | 3 | export const zIndices: ThemeOverride['zIndices'] = { 4 | header: 2000, 5 | headerNavigation: 2000, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ui/theme/styles.ts: -------------------------------------------------------------------------------- 1 | import { ThemeOverride } from '@chakra-ui/react'; 2 | 3 | export const styles: ThemeOverride['styles'] = { 4 | global: { 5 | '*, *::before, &::after': { 6 | borderColor: 'gray.200', // Overwrite chakra global 7 | }, 8 | html: { 9 | scrollBehavior: 'smooth', 10 | }, 11 | body: { 12 | backgroundColor: 'blueGray.50', 13 | fontSize: 'md', // set cascading font-size at body, leave html at 16px to preserve rem 14 | }, 15 | img: { 16 | maxWidth: '100%', // Images won't overflow their container 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@ifixit/tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /patches/react-lite-yt-embed@1.2.7.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index 732c9a8b0dcd05104013bb0a18eadc74694ee862..5639112b226312bf53210740da727ece1500a163 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -1,7 +1,7 @@ 6 | { 7 | "version": "1.2.7", 8 | "license": "MIT", 9 | - "main": "dist/index.js", 10 | + "main": "dist/react-lite-yt-embed.cjs.production.min.js", 11 | "typings": "dist/index.d.ts", 12 | "files": [ 13 | "dist" -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'frontend' 3 | - 'packages/**' 4 | --------------------------------------------------------------------------------