From 8677d85170424a4c7af94ab105bfaf672bacae18 Mon Sep 17 00:00:00 2001 From: Dawid Dao Xuan Date: Mon, 14 Oct 2024 21:17:29 +0200 Subject: [PATCH] feature: algolia starter (#63) --- README.md | 7 +- starters/shopify-algolia/.eslintignore | 8 + starters/shopify-algolia/.eslintrc.js | 39 + starters/shopify-algolia/.gitignore | 22 + starters/shopify-algolia/.graphqlrc.ts | 20 + starters/shopify-algolia/.prettierignore | 3 + starters/shopify-algolia/.storybook/main.ts | 30 + .../shopify-algolia/.storybook/preview.ts | 17 + .../app/.well-known/vercel/flags/route.ts | 35 + .../app/access-denied/page.tsx | 13 + .../app/actions/cart.actions.ts | 86 + .../app/actions/collection.actions.ts | 26 + .../app/actions/favorites.actions.ts | 20 + .../app/actions/page.actions.ts | 8 + .../app/actions/product.actions.ts | 76 + .../app/actions/reviews.actions.ts | 15 + .../app/actions/user.actions.ts | 34 + .../app/api/feed/sync/route.ts | 121 + .../shopify-algolia/app/api/health/route.ts | 3 + .../app/api/redirects/route.ts | 25 + .../app/api/reviews/ai-summary/route.ts | 173 + .../app/api/reviews/sync/route.ts | 93 + .../app/category/clp/[slug]/[page]/page.tsx | 24 + .../app/category/clp/[slug]/page.tsx | 38 + .../app/category/plp/[slug]/page.tsx | 23 + starters/shopify-algolia/app/error.tsx | 33 + .../shopify-algolia/app/favorites/page.tsx | 61 + starters/shopify-algolia/app/global-error.tsx | 19 + starters/shopify-algolia/app/globals.css | 22 + .../app/home/[bucket]/page.tsx | 44 + starters/shopify-algolia/app/icon.png | Bin 0 -> 24936 bytes starters/shopify-algolia/app/layout.tsx | 271 + starters/shopify-algolia/app/manifest.ts | 20 + starters/shopify-algolia/app/not-found.tsx | 18 + .../shopify-algolia/app/opengraph-image.jpg | Bin 0 -> 1014388 bytes .../app/pages/[slug]/metadata.ts | 14 + .../shopify-algolia/app/pages/[slug]/page.tsx | 32 + .../app/product/[slug]/draft/page.tsx | 114 + .../app/product/[slug]/loading.tsx | 5 + .../app/product/[slug]/metadata.ts | 69 + .../app/product/[slug]/opengraph-image.tsx | 131 + .../app/product/[slug]/page.tsx | 114 + .../app/reviews/[slug]/metadata.ts | 31 + .../app/reviews/[slug]/page.tsx | 111 + starters/shopify-algolia/app/robots.txt | 15 + .../app/search/opengraph-image.tsx | 88 + starters/shopify-algolia/app/search/page.tsx | 26 + .../shopify-algolia/app/settings/page.tsx | 5 + starters/shopify-algolia/app/sitemap.ts | 92 + .../shopify-algolia/app/styles/megamenu.css | 364 + starters/shopify-algolia/app/styles/reset.css | 385 + starters/shopify-algolia/clients/replicate.ts | 11 + starters/shopify-algolia/clients/reviews.ts | 15 + starters/shopify-algolia/clients/search.ts | 10 + .../clients/storefrontClient.ts | 10 + starters/shopify-algolia/components.json | 18 + .../Accordion/Accordion.stories.tsx | 60 + .../components/Accordion/Accordion.tsx | 46 + .../components/Alert/Alert.tsx | 36 + .../AnnouncementBar.stories.tsx | 17 + .../AnnouncementBar/AnnouncementBar.tsx | 13 + .../components/Badge/Badge.tsx | 28 + .../components/Breadcrumb/Breadcrumb.tsx | 51 + .../Breadcrumbs/Breadcrumbs.stories.tsx | 17 + .../components/Breadcrumbs/Breadcrumbs.tsx | 36 + .../components/Button/Button.stories.tsx | 33 + .../components/Button/Button.tsx | 66 + .../components/Button/ButtonNew.tsx | 42 + .../CallToAction/CallToAction.stories.tsx | 17 + .../components/CallToAction/CallToAction.tsx | 32 + .../components/Card/Card.stories.tsx | 29 + .../shopify-algolia/components/Card/Card.tsx | 35 + .../components/Carousel/Carousel.stories.tsx | 27 + .../components/Carousel/Carousel.tsx | 198 + .../components/Checkbox/Checkbox.stories.tsx | 23 + .../components/Checkbox/Checkbox.tsx | 26 + .../components/Dialog/Dialog.stories.tsx | 26 + .../components/Dialog/Dialog.tsx | 68 + .../DropdownMenu/DropdownMenu.stories.tsx | 29 + .../components/DropdownMenu/DropdownMenu.tsx | 162 + .../ExpandableContent/ExpandableContent.tsx | 52 + .../components/Footer/Footer.stories.tsx | 17 + .../components/Footer/Footer.tsx | 156 + .../components/Form/Form.stories.tsx | 52 + .../shopify-algolia/components/Form/Form.tsx | 103 + .../components/GenericModal/GenericModal.tsx | 30 + .../components/Icons/ArrowIcon.tsx | 12 + .../components/Icons/CaretSortIcon.tsx | 12 + .../components/Icons/CheckIcon.tsx | 12 + .../components/Icons/ChevronIcon.tsx | 7 + .../components/Icons/CloseIcon.tsx | 7 + .../components/Icons/FavoritesIcon.tsx | 10 + .../components/Icons/FiltersIcon.tsx | 60 + .../components/Icons/HeartIcon.tsx | 14 + .../components/Icons/Icons.stories.tsx | 35 + .../components/Icons/RobotIcon.tsx | 23 + .../components/Icons/SearchIcon.tsx | 7 + .../components/Icons/StarIcon.tsx | 18 + .../components/Icons/ThinSearchIcon.tsx | 16 + .../components/Input/Input.stories.tsx | 23 + .../components/Input/Input.tsx | 21 + .../components/Label/Label.stories.tsx | 17 + .../components/Label/Label.tsx | 15 + .../LoadingDots/LoadingDots.stories.tsx | 17 + .../components/LoadingDots/LoadingDots.tsx | 12 + .../shopify-algolia/components/Logo/Logo.tsx | 23 + .../components/Modals/LoginModal.tsx | 93 + .../components/Modals/Modals.tsx | 41 + .../components/Modals/ReviewModal.tsx | 183 + .../components/Modals/SearchModal.tsx | 138 + .../components/Modals/SignupModal.tsx | 92 + .../components/NavigationBar/Autocomplete.tsx | 78 + .../components/NavigationBar/Cart.tsx | 30 + .../components/NavigationBar/Favorites.tsx | 17 + .../NavigationBar/NavigationBar.tsx | 111 + .../NavigationBar/NavigationItem.tsx | 24 + .../NavigationBar/OpenCartButton.tsx | 13 + .../components/NavigationBar/SearchButton.tsx | 16 + .../NavigationBar/mobileInlineScript.ts | 78 + .../components/NavigationBar/types.ts | 30 + .../variants/ImageGridVariant.tsx | 28 + .../variants/TextGridVariant.tsx | 35 + .../variants/TextImageGridVariant.tsx | 44 + .../Pagination/Pagination.stories.tsx | 65 + .../components/Pagination/Pagination.tsx | 81 + .../components/ProductCard/ProductCard.tsx | 55 + .../components/ProductCard/QuickAdd.tsx | 27 + .../components/ProductCard/QuickAddButton.tsx | 75 + .../components/ProfileMenu/AuthActions.tsx | 19 + .../components/ProfileMenu/ProfileBar.tsx | 35 + .../components/ProfileMenu/ProfileMenu.tsx | 40 + .../components/Select/Select.stories.tsx | 34 + .../components/Select/Select.tsx | 108 + .../components/Sheet/Sheet.stories.tsx | 47 + .../components/Sheet/Sheet.tsx | 80 + .../components/Skeleton/Skeleton.stories.tsx | 17 + .../components/Skeleton/Skeleton.tsx | 7 + .../components/Spinner/Spinner.stories.tsx | 17 + .../components/Spinner/Spinner.tsx | 5 + .../components/Textarea/Textarea.tsx | 21 + starters/shopify-algolia/constants/index.ts | 17 + starters/shopify-algolia/e2e/example.spec.ts | 6 + starters/shopify-algolia/env.mjs | 58 + starters/shopify-algolia/jest.config.js | 14 + starters/shopify-algolia/jest.setup.js | 2 + .../lib/algolia/filterBuilder.ts | 89 + starters/shopify-algolia/lib/algolia/index.ts | 103 + starters/shopify-algolia/lib/reviews/index.ts | 106 + starters/shopify-algolia/lib/reviews/types.ts | 62 + .../lib/shopify/fragments/cart.ts | 58 + .../lib/shopify/fragments/collection.ts | 23 + .../lib/shopify/fragments/customer.ts | 16 + .../lib/shopify/fragments/image.ts | 10 + .../lib/shopify/fragments/menu.ts | 28 + .../lib/shopify/fragments/page.ts | 21 + .../lib/shopify/fragments/product.ts | 72 + .../lib/shopify/fragments/seo.ts | 8 + starters/shopify-algolia/lib/shopify/index.ts | 265 + .../lib/shopify/mutations/cart.storefront.ts | 45 + .../shopify/mutations/customer.storefront.ts | 60 + .../shopify/mutations/product-feed.admin.ts | 25 + .../lib/shopify/mutations/webhook.admin.ts | 13 + .../shopify-algolia/lib/shopify/normalize.ts | 50 + .../lib/shopify/queries/cart.storefront.ts | 10 + .../shopify/queries/collection.storefront.ts | 32 + .../shopify/queries/customer.storefront.ts | 10 + .../lib/shopify/queries/menu.storefront.ts | 20 + .../lib/shopify/queries/page.storefront.ts | 23 + .../lib/shopify/queries/product-feed.admin.ts | 11 + .../lib/shopify/queries/product.admin.ts | 94 + .../lib/shopify/queries/product.storefront.ts | 49 + .../types/admin/admin-2024-01.schema.json | 1 + .../shopify/types/admin/admin.generated.d.ts | 103 + .../lib/shopify/types/admin/admin.types.d.ts | 49919 ++++++++++++++++ .../lib/shopify/types/index.ts | 153 + .../types/storefront-2024-01.schema.json | 1 + .../shopify/types/storefront.generated.d.ts | 791 + .../lib/shopify/types/storefront.types.d.ts | 8010 +++ starters/shopify-algolia/middleware.ts | 138 + starters/shopify-algolia/next-env.d.ts | 5 + starters/shopify-algolia/next.config.mjs | 56 + starters/shopify-algolia/package.json | 129 + starters/shopify-algolia/playwright.config.ts | 69 + starters/shopify-algolia/postcss.config.js | 6 + starters/shopify-algolia/prettier.config.js | 7 + .../public/category-placeholder-1.svg | 8 + .../public/category-placeholder-2.svg | 10 + .../public/category-placeholder-3.svg | 8 + .../public/category-placeholder-4.svg | 9 + .../public/category-placeholder-5.svg | 9 + .../public/category-placeholder-6.svg | 10 + .../public/default-product-image.svg | 8 + .../public/demo-categories-data.json | 62 + .../shopify-algolia/public/demo-data.json | 3433 ++ .../public/demo-product-reviews-data.json | 103 + .../public/fonts/Inter-Black.ttf | Bin 0 -> 316848 bytes .../public/fonts/Inter-Bold.ttf | Bin 0 -> 316584 bytes .../public/fonts/Inter-ExtraBold.ttf | Bin 0 -> 317184 bytes .../public/fonts/Inter-ExtraLight.ttf | Bin 0 -> 311232 bytes .../public/fonts/Inter-Light.ttf | Bin 0 -> 310832 bytes .../public/fonts/Inter-Medium.ttf | Bin 0 -> 315132 bytes .../public/fonts/Inter-Regular.ttf | Bin 0 -> 310252 bytes .../public/fonts/Inter-SemiBold.ttf | Bin 0 -> 316220 bytes .../public/fonts/Inter-Thin.ttf | Bin 0 -> 310984 bytes .../shopify-algolia/public/menu/beauty-1.png | Bin 0 -> 669562 bytes .../shopify-algolia/public/menu/beauty-2.png | Bin 0 -> 658313 bytes .../shopify-algolia/public/menu/beauty-3.png | Bin 0 -> 453603 bytes .../shopify-algolia/public/menu/beauty-4.png | Bin 0 -> 492662 bytes .../shopify-algolia/public/menu/beauty-5.png | Bin 0 -> 565160 bytes .../public/menu/electronics-1.png | Bin 0 -> 540528 bytes .../public/menu/electronics-2.png | Bin 0 -> 453412 bytes .../public/menu/electronics-3.png | Bin 0 -> 426547 bytes .../public/menu/electronics-4.png | Bin 0 -> 727915 bytes .../public/menu/furniture-1.png | Bin 0 -> 296512 bytes .../public/menu/furniture-2.png | Bin 0 -> 632055 bytes .../public/menu/furniture-3.png | Bin 0 -> 489733 bytes .../public/menu/furniture-4.png | Bin 0 -> 501430 bytes .../redirects/bloom-filter.json | 1 + .../redirects/generate-bloom-filter.ts | 12 + .../redirects/new-redirects.json | 1 + .../shopify-algolia/redirects/redirects.json | 6 + .../shopify-algolia/report-bundle-size.js | 131 + starters/shopify-algolia/reset.d.ts | 13 + .../shopify-algolia/shopify-webhooks.d.ts | 109 + .../shopify-algolia/stores/addProductStore.ts | 18 + starters/shopify-algolia/stores/cartStore.ts | 28 + .../stores/filterTransitionStore.ts | 11 + .../shopify-algolia/stores/filtersStore.ts | 13 + starters/shopify-algolia/stores/modalStore.ts | 35 + starters/shopify-algolia/stores/userStore.ts | 12 + starters/shopify-algolia/tailwind.config.ts | 148 + starters/shopify-algolia/tsconfig.json | 34 + starters/shopify-algolia/types/index.ts | 11 + starters/shopify-algolia/utils/abTesting.ts | 18 + .../utils/authenticate-api-route.ts | 15 + starters/shopify-algolia/utils/cn.ts | 6 + .../shopify-algolia/utils/compare-hmac.ts | 7 + starters/shopify-algolia/utils/demoUtils.ts | 52 + .../shopify-algolia/utils/enrich-product.ts | 89 + starters/shopify-algolia/utils/getCookie.ts | 15 + .../utils/getVercelFlagOverrides.ts | 16 + .../shopify-algolia/utils/highlightedText.tsx | 22 + .../shopify-algolia/utils/makeKeywords.ts | 78 + .../utils/mapCurrencyToSign.ts | 15 + starters/shopify-algolia/utils/opt-in.ts | 31 + .../utils/productOptionsUtils.ts | 70 + starters/shopify-algolia/utils/slug-name.ts | 10 + .../shopify-algolia/utils/useAutocomplete.ts | 59 + .../utils/useHierarchicalMenu.ts | 84 + starters/shopify-algolia/utils/useReadMore.ts | 14 + starters/shopify-algolia/vercel.json | 13 + .../shopify-algolia/views/Cart/CartItem.tsx | 45 + .../shopify-algolia/views/Cart/CartSheet.tsx | 98 + .../shopify-algolia/views/Cart/CartView.tsx | 30 + .../views/Cart/ChangeQuantityButton.tsx | 37 + .../views/Cart/DeleteButton.tsx | 33 + .../views/Category/CategoryView.tsx | 17 + .../views/Category/PageSkeleton.tsx | 37 + .../shopify-algolia/views/DemoModeAlert.tsx | 24 + .../shopify-algolia/views/DraftToolbar.tsx | 11 + starters/shopify-algolia/views/FlagValues.tsx | 18 + .../shopify-algolia/views/GithubBadge.tsx | 40 + .../views/Homepage/BestOffersSection.tsx | 31 + .../views/Homepage/CarouselSection.tsx | 53 + .../views/Homepage/CategoriesSection.tsx | 64 + .../views/Homepage/EverythingUnderSection.tsx | 33 + .../views/Homepage/HeroSection.tsx | 21 + .../views/Homepage/ProductsWeekSection.tsx | 80 + .../views/Listing/CategoryFacet.tsx | 82 + .../views/Listing/Controls.tsx | 17 + .../shopify-algolia/views/Listing/Facet.tsx | 37 + .../views/Listing/FacetsContent.tsx | 207 + .../views/Listing/FacetsDesktop.tsx | 24 + .../views/Listing/FacetsMobile.tsx | 35 + .../views/Listing/HideFilters.tsx | 26 + .../views/Listing/HitsSection.tsx | 19 + .../views/Listing/PageSkeleton.tsx | 37 + .../views/Listing/PaginationSection.tsx | 59 + .../views/Listing/PriceFacet.tsx | 77 + .../views/Listing/RatingFacet.tsx | 41 + .../views/Listing/SearchFacet.tsx | 49 + .../shopify-algolia/views/Listing/Sorter.tsx | 59 + .../views/Listing/composeFilters.test.ts | 76 + .../views/Listing/composeFilters.ts | 48 + .../views/Product/AddToCartButton.tsx | 65 + .../views/Product/BackButton.tsx | 10 + .../views/Product/CenterImageSection.tsx | 36 + .../views/Product/FaqSection.tsx | 51 + .../views/Product/FavoriteMarker.tsx | 55 + .../views/Product/PageSkeleton.tsx | 62 + .../views/Product/ProductAddedAlert.tsx | 63 + .../views/Product/ProductImages.tsx | 32 + .../views/Product/ProductTitle.tsx | 20 + .../views/Product/ReviewButton.tsx | 25 + .../views/Product/ReviewCard.tsx | 33 + .../views/Product/ReviewsSection.tsx | 92 + .../views/Product/RightSection.tsx | 5 + .../views/Product/SideImages.tsx | 40 + .../views/Product/SimilarProductsSection.tsx | 72 + .../SimilarProductsSectionSkeleton.tsx | 20 + .../views/Product/StarRating.tsx | 14 + .../shopify-algolia/views/Product/Variant.tsx | 39 + .../views/Product/VariantsSection.tsx | 39 + .../views/Search/SearchView.tsx | 144 + .../views/Settings/ProfileForm.tsx | 102 + .../views/Settings/SettingsView.tsx | 26 + .../shopify-algolia/views/ThirdParties.tsx | 17 + starters/shopify-algolia/yarn.lock | 14738 +++++ starters/shopify-meilisearch/yarn.lock | 228 +- 309 files changed, 89903 insertions(+), 114 deletions(-) create mode 100644 starters/shopify-algolia/.eslintignore create mode 100644 starters/shopify-algolia/.eslintrc.js create mode 100644 starters/shopify-algolia/.gitignore create mode 100644 starters/shopify-algolia/.graphqlrc.ts create mode 100644 starters/shopify-algolia/.prettierignore create mode 100644 starters/shopify-algolia/.storybook/main.ts create mode 100644 starters/shopify-algolia/.storybook/preview.ts create mode 100644 starters/shopify-algolia/app/.well-known/vercel/flags/route.ts create mode 100644 starters/shopify-algolia/app/access-denied/page.tsx create mode 100644 starters/shopify-algolia/app/actions/cart.actions.ts create mode 100644 starters/shopify-algolia/app/actions/collection.actions.ts create mode 100644 starters/shopify-algolia/app/actions/favorites.actions.ts create mode 100644 starters/shopify-algolia/app/actions/page.actions.ts create mode 100644 starters/shopify-algolia/app/actions/product.actions.ts create mode 100644 starters/shopify-algolia/app/actions/reviews.actions.ts create mode 100644 starters/shopify-algolia/app/actions/user.actions.ts create mode 100644 starters/shopify-algolia/app/api/feed/sync/route.ts create mode 100644 starters/shopify-algolia/app/api/health/route.ts create mode 100644 starters/shopify-algolia/app/api/redirects/route.ts create mode 100644 starters/shopify-algolia/app/api/reviews/ai-summary/route.ts create mode 100644 starters/shopify-algolia/app/api/reviews/sync/route.ts create mode 100644 starters/shopify-algolia/app/category/clp/[slug]/[page]/page.tsx create mode 100644 starters/shopify-algolia/app/category/clp/[slug]/page.tsx create mode 100644 starters/shopify-algolia/app/category/plp/[slug]/page.tsx create mode 100644 starters/shopify-algolia/app/error.tsx create mode 100644 starters/shopify-algolia/app/favorites/page.tsx create mode 100644 starters/shopify-algolia/app/global-error.tsx create mode 100644 starters/shopify-algolia/app/globals.css create mode 100644 starters/shopify-algolia/app/home/[bucket]/page.tsx create mode 100644 starters/shopify-algolia/app/icon.png create mode 100644 starters/shopify-algolia/app/layout.tsx create mode 100644 starters/shopify-algolia/app/manifest.ts create mode 100644 starters/shopify-algolia/app/not-found.tsx create mode 100644 starters/shopify-algolia/app/opengraph-image.jpg create mode 100644 starters/shopify-algolia/app/pages/[slug]/metadata.ts create mode 100644 starters/shopify-algolia/app/pages/[slug]/page.tsx create mode 100644 starters/shopify-algolia/app/product/[slug]/draft/page.tsx create mode 100644 starters/shopify-algolia/app/product/[slug]/loading.tsx create mode 100644 starters/shopify-algolia/app/product/[slug]/metadata.ts create mode 100644 starters/shopify-algolia/app/product/[slug]/opengraph-image.tsx create mode 100644 starters/shopify-algolia/app/product/[slug]/page.tsx create mode 100644 starters/shopify-algolia/app/reviews/[slug]/metadata.ts create mode 100644 starters/shopify-algolia/app/reviews/[slug]/page.tsx create mode 100644 starters/shopify-algolia/app/robots.txt create mode 100644 starters/shopify-algolia/app/search/opengraph-image.tsx create mode 100644 starters/shopify-algolia/app/search/page.tsx create mode 100644 starters/shopify-algolia/app/settings/page.tsx create mode 100644 starters/shopify-algolia/app/sitemap.ts create mode 100644 starters/shopify-algolia/app/styles/megamenu.css create mode 100644 starters/shopify-algolia/app/styles/reset.css create mode 100644 starters/shopify-algolia/clients/replicate.ts create mode 100644 starters/shopify-algolia/clients/reviews.ts create mode 100644 starters/shopify-algolia/clients/search.ts create mode 100644 starters/shopify-algolia/clients/storefrontClient.ts create mode 100644 starters/shopify-algolia/components.json create mode 100644 starters/shopify-algolia/components/Accordion/Accordion.stories.tsx create mode 100644 starters/shopify-algolia/components/Accordion/Accordion.tsx create mode 100644 starters/shopify-algolia/components/Alert/Alert.tsx create mode 100644 starters/shopify-algolia/components/AnnouncementBar/AnnouncementBar.stories.tsx create mode 100644 starters/shopify-algolia/components/AnnouncementBar/AnnouncementBar.tsx create mode 100644 starters/shopify-algolia/components/Badge/Badge.tsx create mode 100644 starters/shopify-algolia/components/Breadcrumb/Breadcrumb.tsx create mode 100644 starters/shopify-algolia/components/Breadcrumbs/Breadcrumbs.stories.tsx create mode 100644 starters/shopify-algolia/components/Breadcrumbs/Breadcrumbs.tsx create mode 100644 starters/shopify-algolia/components/Button/Button.stories.tsx create mode 100644 starters/shopify-algolia/components/Button/Button.tsx create mode 100644 starters/shopify-algolia/components/Button/ButtonNew.tsx create mode 100644 starters/shopify-algolia/components/CallToAction/CallToAction.stories.tsx create mode 100644 starters/shopify-algolia/components/CallToAction/CallToAction.tsx create mode 100644 starters/shopify-algolia/components/Card/Card.stories.tsx create mode 100644 starters/shopify-algolia/components/Card/Card.tsx create mode 100644 starters/shopify-algolia/components/Carousel/Carousel.stories.tsx create mode 100644 starters/shopify-algolia/components/Carousel/Carousel.tsx create mode 100644 starters/shopify-algolia/components/Checkbox/Checkbox.stories.tsx create mode 100644 starters/shopify-algolia/components/Checkbox/Checkbox.tsx create mode 100644 starters/shopify-algolia/components/Dialog/Dialog.stories.tsx create mode 100644 starters/shopify-algolia/components/Dialog/Dialog.tsx create mode 100644 starters/shopify-algolia/components/DropdownMenu/DropdownMenu.stories.tsx create mode 100644 starters/shopify-algolia/components/DropdownMenu/DropdownMenu.tsx create mode 100644 starters/shopify-algolia/components/ExpandableContent/ExpandableContent.tsx create mode 100644 starters/shopify-algolia/components/Footer/Footer.stories.tsx create mode 100644 starters/shopify-algolia/components/Footer/Footer.tsx create mode 100644 starters/shopify-algolia/components/Form/Form.stories.tsx create mode 100644 starters/shopify-algolia/components/Form/Form.tsx create mode 100644 starters/shopify-algolia/components/GenericModal/GenericModal.tsx create mode 100644 starters/shopify-algolia/components/Icons/ArrowIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/CaretSortIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/CheckIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/ChevronIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/CloseIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/FavoritesIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/FiltersIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/HeartIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/Icons.stories.tsx create mode 100644 starters/shopify-algolia/components/Icons/RobotIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/SearchIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/StarIcon.tsx create mode 100644 starters/shopify-algolia/components/Icons/ThinSearchIcon.tsx create mode 100644 starters/shopify-algolia/components/Input/Input.stories.tsx create mode 100644 starters/shopify-algolia/components/Input/Input.tsx create mode 100644 starters/shopify-algolia/components/Label/Label.stories.tsx create mode 100644 starters/shopify-algolia/components/Label/Label.tsx create mode 100644 starters/shopify-algolia/components/LoadingDots/LoadingDots.stories.tsx create mode 100644 starters/shopify-algolia/components/LoadingDots/LoadingDots.tsx create mode 100644 starters/shopify-algolia/components/Logo/Logo.tsx create mode 100644 starters/shopify-algolia/components/Modals/LoginModal.tsx create mode 100644 starters/shopify-algolia/components/Modals/Modals.tsx create mode 100644 starters/shopify-algolia/components/Modals/ReviewModal.tsx create mode 100644 starters/shopify-algolia/components/Modals/SearchModal.tsx create mode 100644 starters/shopify-algolia/components/Modals/SignupModal.tsx create mode 100644 starters/shopify-algolia/components/NavigationBar/Autocomplete.tsx create mode 100644 starters/shopify-algolia/components/NavigationBar/Cart.tsx create mode 100644 starters/shopify-algolia/components/NavigationBar/Favorites.tsx create mode 100644 starters/shopify-algolia/components/NavigationBar/NavigationBar.tsx create mode 100644 starters/shopify-algolia/components/NavigationBar/NavigationItem.tsx create mode 100644 starters/shopify-algolia/components/NavigationBar/OpenCartButton.tsx create mode 100644 starters/shopify-algolia/components/NavigationBar/SearchButton.tsx create mode 100644 starters/shopify-algolia/components/NavigationBar/mobileInlineScript.ts create mode 100644 starters/shopify-algolia/components/NavigationBar/types.ts create mode 100644 starters/shopify-algolia/components/NavigationBar/variants/ImageGridVariant.tsx create mode 100644 starters/shopify-algolia/components/NavigationBar/variants/TextGridVariant.tsx create mode 100644 starters/shopify-algolia/components/NavigationBar/variants/TextImageGridVariant.tsx create mode 100644 starters/shopify-algolia/components/Pagination/Pagination.stories.tsx create mode 100644 starters/shopify-algolia/components/Pagination/Pagination.tsx create mode 100644 starters/shopify-algolia/components/ProductCard/ProductCard.tsx create mode 100644 starters/shopify-algolia/components/ProductCard/QuickAdd.tsx create mode 100644 starters/shopify-algolia/components/ProductCard/QuickAddButton.tsx create mode 100644 starters/shopify-algolia/components/ProfileMenu/AuthActions.tsx create mode 100644 starters/shopify-algolia/components/ProfileMenu/ProfileBar.tsx create mode 100644 starters/shopify-algolia/components/ProfileMenu/ProfileMenu.tsx create mode 100644 starters/shopify-algolia/components/Select/Select.stories.tsx create mode 100644 starters/shopify-algolia/components/Select/Select.tsx create mode 100644 starters/shopify-algolia/components/Sheet/Sheet.stories.tsx create mode 100644 starters/shopify-algolia/components/Sheet/Sheet.tsx create mode 100644 starters/shopify-algolia/components/Skeleton/Skeleton.stories.tsx create mode 100644 starters/shopify-algolia/components/Skeleton/Skeleton.tsx create mode 100644 starters/shopify-algolia/components/Spinner/Spinner.stories.tsx create mode 100644 starters/shopify-algolia/components/Spinner/Spinner.tsx create mode 100644 starters/shopify-algolia/components/Textarea/Textarea.tsx create mode 100644 starters/shopify-algolia/constants/index.ts create mode 100644 starters/shopify-algolia/e2e/example.spec.ts create mode 100644 starters/shopify-algolia/env.mjs create mode 100644 starters/shopify-algolia/jest.config.js create mode 100644 starters/shopify-algolia/jest.setup.js create mode 100644 starters/shopify-algolia/lib/algolia/filterBuilder.ts create mode 100644 starters/shopify-algolia/lib/algolia/index.ts create mode 100644 starters/shopify-algolia/lib/reviews/index.ts create mode 100644 starters/shopify-algolia/lib/reviews/types.ts create mode 100644 starters/shopify-algolia/lib/shopify/fragments/cart.ts create mode 100644 starters/shopify-algolia/lib/shopify/fragments/collection.ts create mode 100644 starters/shopify-algolia/lib/shopify/fragments/customer.ts create mode 100644 starters/shopify-algolia/lib/shopify/fragments/image.ts create mode 100644 starters/shopify-algolia/lib/shopify/fragments/menu.ts create mode 100644 starters/shopify-algolia/lib/shopify/fragments/page.ts create mode 100644 starters/shopify-algolia/lib/shopify/fragments/product.ts create mode 100644 starters/shopify-algolia/lib/shopify/fragments/seo.ts create mode 100644 starters/shopify-algolia/lib/shopify/index.ts create mode 100644 starters/shopify-algolia/lib/shopify/mutations/cart.storefront.ts create mode 100644 starters/shopify-algolia/lib/shopify/mutations/customer.storefront.ts create mode 100644 starters/shopify-algolia/lib/shopify/mutations/product-feed.admin.ts create mode 100644 starters/shopify-algolia/lib/shopify/mutations/webhook.admin.ts create mode 100644 starters/shopify-algolia/lib/shopify/normalize.ts create mode 100644 starters/shopify-algolia/lib/shopify/queries/cart.storefront.ts create mode 100644 starters/shopify-algolia/lib/shopify/queries/collection.storefront.ts create mode 100644 starters/shopify-algolia/lib/shopify/queries/customer.storefront.ts create mode 100644 starters/shopify-algolia/lib/shopify/queries/menu.storefront.ts create mode 100644 starters/shopify-algolia/lib/shopify/queries/page.storefront.ts create mode 100644 starters/shopify-algolia/lib/shopify/queries/product-feed.admin.ts create mode 100644 starters/shopify-algolia/lib/shopify/queries/product.admin.ts create mode 100644 starters/shopify-algolia/lib/shopify/queries/product.storefront.ts create mode 100644 starters/shopify-algolia/lib/shopify/types/admin/admin-2024-01.schema.json create mode 100644 starters/shopify-algolia/lib/shopify/types/admin/admin.generated.d.ts create mode 100644 starters/shopify-algolia/lib/shopify/types/admin/admin.types.d.ts create mode 100644 starters/shopify-algolia/lib/shopify/types/index.ts create mode 100644 starters/shopify-algolia/lib/shopify/types/storefront-2024-01.schema.json create mode 100644 starters/shopify-algolia/lib/shopify/types/storefront.generated.d.ts create mode 100644 starters/shopify-algolia/lib/shopify/types/storefront.types.d.ts create mode 100644 starters/shopify-algolia/middleware.ts create mode 100644 starters/shopify-algolia/next-env.d.ts create mode 100644 starters/shopify-algolia/next.config.mjs create mode 100644 starters/shopify-algolia/package.json create mode 100644 starters/shopify-algolia/playwright.config.ts create mode 100644 starters/shopify-algolia/postcss.config.js create mode 100644 starters/shopify-algolia/prettier.config.js create mode 100644 starters/shopify-algolia/public/category-placeholder-1.svg create mode 100644 starters/shopify-algolia/public/category-placeholder-2.svg create mode 100644 starters/shopify-algolia/public/category-placeholder-3.svg create mode 100644 starters/shopify-algolia/public/category-placeholder-4.svg create mode 100644 starters/shopify-algolia/public/category-placeholder-5.svg create mode 100644 starters/shopify-algolia/public/category-placeholder-6.svg create mode 100644 starters/shopify-algolia/public/default-product-image.svg create mode 100644 starters/shopify-algolia/public/demo-categories-data.json create mode 100644 starters/shopify-algolia/public/demo-data.json create mode 100644 starters/shopify-algolia/public/demo-product-reviews-data.json create mode 100644 starters/shopify-algolia/public/fonts/Inter-Black.ttf create mode 100644 starters/shopify-algolia/public/fonts/Inter-Bold.ttf create mode 100644 starters/shopify-algolia/public/fonts/Inter-ExtraBold.ttf create mode 100644 starters/shopify-algolia/public/fonts/Inter-ExtraLight.ttf create mode 100644 starters/shopify-algolia/public/fonts/Inter-Light.ttf create mode 100644 starters/shopify-algolia/public/fonts/Inter-Medium.ttf create mode 100644 starters/shopify-algolia/public/fonts/Inter-Regular.ttf create mode 100644 starters/shopify-algolia/public/fonts/Inter-SemiBold.ttf create mode 100644 starters/shopify-algolia/public/fonts/Inter-Thin.ttf create mode 100644 starters/shopify-algolia/public/menu/beauty-1.png create mode 100644 starters/shopify-algolia/public/menu/beauty-2.png create mode 100644 starters/shopify-algolia/public/menu/beauty-3.png create mode 100644 starters/shopify-algolia/public/menu/beauty-4.png create mode 100644 starters/shopify-algolia/public/menu/beauty-5.png create mode 100644 starters/shopify-algolia/public/menu/electronics-1.png create mode 100644 starters/shopify-algolia/public/menu/electronics-2.png create mode 100644 starters/shopify-algolia/public/menu/electronics-3.png create mode 100644 starters/shopify-algolia/public/menu/electronics-4.png create mode 100644 starters/shopify-algolia/public/menu/furniture-1.png create mode 100644 starters/shopify-algolia/public/menu/furniture-2.png create mode 100644 starters/shopify-algolia/public/menu/furniture-3.png create mode 100644 starters/shopify-algolia/public/menu/furniture-4.png create mode 100644 starters/shopify-algolia/redirects/bloom-filter.json create mode 100644 starters/shopify-algolia/redirects/generate-bloom-filter.ts create mode 100644 starters/shopify-algolia/redirects/new-redirects.json create mode 100644 starters/shopify-algolia/redirects/redirects.json create mode 100644 starters/shopify-algolia/report-bundle-size.js create mode 100644 starters/shopify-algolia/reset.d.ts create mode 100644 starters/shopify-algolia/shopify-webhooks.d.ts create mode 100644 starters/shopify-algolia/stores/addProductStore.ts create mode 100644 starters/shopify-algolia/stores/cartStore.ts create mode 100644 starters/shopify-algolia/stores/filterTransitionStore.ts create mode 100644 starters/shopify-algolia/stores/filtersStore.ts create mode 100644 starters/shopify-algolia/stores/modalStore.ts create mode 100644 starters/shopify-algolia/stores/userStore.ts create mode 100644 starters/shopify-algolia/tailwind.config.ts create mode 100644 starters/shopify-algolia/tsconfig.json create mode 100644 starters/shopify-algolia/types/index.ts create mode 100644 starters/shopify-algolia/utils/abTesting.ts create mode 100644 starters/shopify-algolia/utils/authenticate-api-route.ts create mode 100644 starters/shopify-algolia/utils/cn.ts create mode 100644 starters/shopify-algolia/utils/compare-hmac.ts create mode 100644 starters/shopify-algolia/utils/demoUtils.ts create mode 100644 starters/shopify-algolia/utils/enrich-product.ts create mode 100644 starters/shopify-algolia/utils/getCookie.ts create mode 100644 starters/shopify-algolia/utils/getVercelFlagOverrides.ts create mode 100644 starters/shopify-algolia/utils/highlightedText.tsx create mode 100644 starters/shopify-algolia/utils/makeKeywords.ts create mode 100644 starters/shopify-algolia/utils/mapCurrencyToSign.ts create mode 100644 starters/shopify-algolia/utils/opt-in.ts create mode 100644 starters/shopify-algolia/utils/productOptionsUtils.ts create mode 100644 starters/shopify-algolia/utils/slug-name.ts create mode 100644 starters/shopify-algolia/utils/useAutocomplete.ts create mode 100644 starters/shopify-algolia/utils/useHierarchicalMenu.ts create mode 100644 starters/shopify-algolia/utils/useReadMore.ts create mode 100644 starters/shopify-algolia/vercel.json create mode 100644 starters/shopify-algolia/views/Cart/CartItem.tsx create mode 100644 starters/shopify-algolia/views/Cart/CartSheet.tsx create mode 100644 starters/shopify-algolia/views/Cart/CartView.tsx create mode 100644 starters/shopify-algolia/views/Cart/ChangeQuantityButton.tsx create mode 100644 starters/shopify-algolia/views/Cart/DeleteButton.tsx create mode 100644 starters/shopify-algolia/views/Category/CategoryView.tsx create mode 100644 starters/shopify-algolia/views/Category/PageSkeleton.tsx create mode 100644 starters/shopify-algolia/views/DemoModeAlert.tsx create mode 100644 starters/shopify-algolia/views/DraftToolbar.tsx create mode 100644 starters/shopify-algolia/views/FlagValues.tsx create mode 100644 starters/shopify-algolia/views/GithubBadge.tsx create mode 100644 starters/shopify-algolia/views/Homepage/BestOffersSection.tsx create mode 100644 starters/shopify-algolia/views/Homepage/CarouselSection.tsx create mode 100644 starters/shopify-algolia/views/Homepage/CategoriesSection.tsx create mode 100644 starters/shopify-algolia/views/Homepage/EverythingUnderSection.tsx create mode 100644 starters/shopify-algolia/views/Homepage/HeroSection.tsx create mode 100644 starters/shopify-algolia/views/Homepage/ProductsWeekSection.tsx create mode 100644 starters/shopify-algolia/views/Listing/CategoryFacet.tsx create mode 100644 starters/shopify-algolia/views/Listing/Controls.tsx create mode 100644 starters/shopify-algolia/views/Listing/Facet.tsx create mode 100644 starters/shopify-algolia/views/Listing/FacetsContent.tsx create mode 100644 starters/shopify-algolia/views/Listing/FacetsDesktop.tsx create mode 100644 starters/shopify-algolia/views/Listing/FacetsMobile.tsx create mode 100644 starters/shopify-algolia/views/Listing/HideFilters.tsx create mode 100644 starters/shopify-algolia/views/Listing/HitsSection.tsx create mode 100644 starters/shopify-algolia/views/Listing/PageSkeleton.tsx create mode 100644 starters/shopify-algolia/views/Listing/PaginationSection.tsx create mode 100644 starters/shopify-algolia/views/Listing/PriceFacet.tsx create mode 100644 starters/shopify-algolia/views/Listing/RatingFacet.tsx create mode 100644 starters/shopify-algolia/views/Listing/SearchFacet.tsx create mode 100644 starters/shopify-algolia/views/Listing/Sorter.tsx create mode 100644 starters/shopify-algolia/views/Listing/composeFilters.test.ts create mode 100644 starters/shopify-algolia/views/Listing/composeFilters.ts create mode 100644 starters/shopify-algolia/views/Product/AddToCartButton.tsx create mode 100644 starters/shopify-algolia/views/Product/BackButton.tsx create mode 100644 starters/shopify-algolia/views/Product/CenterImageSection.tsx create mode 100644 starters/shopify-algolia/views/Product/FaqSection.tsx create mode 100644 starters/shopify-algolia/views/Product/FavoriteMarker.tsx create mode 100644 starters/shopify-algolia/views/Product/PageSkeleton.tsx create mode 100644 starters/shopify-algolia/views/Product/ProductAddedAlert.tsx create mode 100644 starters/shopify-algolia/views/Product/ProductImages.tsx create mode 100644 starters/shopify-algolia/views/Product/ProductTitle.tsx create mode 100644 starters/shopify-algolia/views/Product/ReviewButton.tsx create mode 100644 starters/shopify-algolia/views/Product/ReviewCard.tsx create mode 100644 starters/shopify-algolia/views/Product/ReviewsSection.tsx create mode 100644 starters/shopify-algolia/views/Product/RightSection.tsx create mode 100644 starters/shopify-algolia/views/Product/SideImages.tsx create mode 100644 starters/shopify-algolia/views/Product/SimilarProductsSection.tsx create mode 100644 starters/shopify-algolia/views/Product/SimilarProductsSectionSkeleton.tsx create mode 100644 starters/shopify-algolia/views/Product/StarRating.tsx create mode 100644 starters/shopify-algolia/views/Product/Variant.tsx create mode 100644 starters/shopify-algolia/views/Product/VariantsSection.tsx create mode 100644 starters/shopify-algolia/views/Search/SearchView.tsx create mode 100644 starters/shopify-algolia/views/Settings/ProfileForm.tsx create mode 100644 starters/shopify-algolia/views/Settings/SettingsView.tsx create mode 100644 starters/shopify-algolia/views/ThirdParties.tsx create mode 100644 starters/shopify-algolia/yarn.lock diff --git a/README.md b/README.md index 647bfd33..ba5e8d39 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ $ yarn create commerce [See the live demo](https://blazity.com/r/commerce) or deploy it straight to Vercel: -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FBlazity%2Fenterprise-commerce%2Ftree%2Fmain&envDescription=Full%20explanation%20on%20how%20to%20obtain%20keys&envLink=https%3A%2F%2Fdocs.commerce.blazity.com%2Fsetup&demo-title=Your%20Commerce&demo-description=AI-FIRST%20NEXT.JS%20STOREFRONT%20FOR%20COMPOSABLE%20COMMERCE&demo-url=https%3A%2F%2Fblazity.com%2Fr%2Fcommerce&demo-image=https%3A%2F%2Fcommerce.blazity.com%2Fopengraph-image.jpg&root-directory=apps%2Fweb) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fblazity%2Fenterprise-commerce%2Ftree%2Fmain%2Fstarters%2Fshopify-algolia) - Shopify & Algolia starter +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fblazity%2Fenterprise-commerce%2Ftree%2Fmain%2Fstarters%2Fshopify-meilisearch) - Shopify & Meilsearch starter **Note:** To enable all features, ensure [required environment variables](https://docs.commerce.blazity.com/setup#manual) are set in your `.env.local` @@ -50,9 +51,7 @@ $ yarn create commerce ## Architecture -In Enterprise Commerce high-level architecture, Meilisearch serves as the primary source for all product data and potentially other types of data in the future. The system is designed to easily integrate AI personalization tools without needing to modify any frontend code. While we are integrated with Shopify by default, we are not tightly bound to it, and you can use any system that works with Meilisearch and can adapt data to our format. - -From a structural viewpoint, we use a monorepo (Turborepo) to manage packages, even though we currently have only one Next.js app. We chose this setup because it prepares us for future developments, which will include additional apps. This arrangement helps keep the packages well-separated and self-contained. +In Enterprise Commerce high-level architecture, Search Engine serves as the primary source for all product data and potentially other types of data in the future. The system is designed to easily integrate AI personalization tools without needing to modify any frontend code. While we are integrated with Shopify by default, we are not tightly bound to it, you can use any commerce platform and adapt data to our format. architecture diagram diff --git a/starters/shopify-algolia/.eslintignore b/starters/shopify-algolia/.eslintignore new file mode 100644 index 00000000..8fc978f4 --- /dev/null +++ b/starters/shopify-algolia/.eslintignore @@ -0,0 +1,8 @@ +.next +node_modules +gql + +dist +/dist +dist/* +dist/**/* \ No newline at end of file diff --git a/starters/shopify-algolia/.eslintrc.js b/starters/shopify-algolia/.eslintrc.js new file mode 100644 index 00000000..44fd1243 --- /dev/null +++ b/starters/shopify-algolia/.eslintrc.js @@ -0,0 +1,39 @@ +/* eslint-env es6 */ +/* eslint-disable no-console */ + +module.exports = { + globals: { + React: true, + JSX: true, + }, + extends: ["next", "prettier", "react-app", "react-app/jest", "plugin:storybook/recommended", "plugin:tailwindcss/recommended"], + parserOptions: { + babelOptions: { + presets: [require.resolve("next/babel")], + }, + ecmaVersion: "latest", + }, + env: { + es6: true, + }, + rules: { + "tailwindcss/no-custom-classname": "off", + "testing-library/prefer-screen-queries": "off", + "@next/next/no-html-link-for-pages": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "sort-imports": [ + "error", + { + ignoreCase: true, + ignoreDeclarationSort: true, + }, + ], + "tailwindcss/classnames-order": "off", + }, +} diff --git a/starters/shopify-algolia/.gitignore b/starters/shopify-algolia/.gitignore new file mode 100644 index 00000000..fccde6d1 --- /dev/null +++ b/starters/shopify-algolia/.gitignore @@ -0,0 +1,22 @@ +.next/ +out/ +build + +build/** +dist/** +**/dist/** +.next/** + + +/.npm-only-allow + +storybook-static/ +playwright-report/ +playwright/.cache/ +test-results/ + +graph.svg + +# testing +coverage +.vercel diff --git a/starters/shopify-algolia/.graphqlrc.ts b/starters/shopify-algolia/.graphqlrc.ts new file mode 100644 index 00000000..38b7dc73 --- /dev/null +++ b/starters/shopify-algolia/.graphqlrc.ts @@ -0,0 +1,20 @@ +import { ApiType, shopifyApiProject } from "@shopify/api-codegen-preset" + +export default { + schema: ["https://shopify.dev/storefront-graphql-direct-proxy/2024-01", "https://shopify.dev/admin-graphql-direct-proxy/2024-01"], + documents: ["./**/*.{js,ts,jsx,tsx}"], + projects: { + default: shopifyApiProject({ + apiType: ApiType.Storefront, + apiVersion: "2024-01", + documents: ["./lib/shopify/**/*.storefront.{js,ts,jsx,tsx}", "./lib/shopify/**/fragments/*.{js,ts,jsx,tsx}"], + outputDir: "./lib/shopify/types", + }), + admin: shopifyApiProject({ + apiType: ApiType.Admin, + apiVersion: "2024-01", + documents: ["./lib/shopify/**/*.admin.{js,ts,jsx,tsx}"], + outputDir: "./lib/shopify/types/admin", + }), + }, +} diff --git a/starters/shopify-algolia/.prettierignore b/starters/shopify-algolia/.prettierignore new file mode 100644 index 00000000..8981f58a --- /dev/null +++ b/starters/shopify-algolia/.prettierignore @@ -0,0 +1,3 @@ +.next +node_modules +gql \ No newline at end of file diff --git a/starters/shopify-algolia/.storybook/main.ts b/starters/shopify-algolia/.storybook/main.ts new file mode 100644 index 00000000..136f062f --- /dev/null +++ b/starters/shopify-algolia/.storybook/main.ts @@ -0,0 +1,30 @@ +import { dirname, join } from "path" +import type { StorybookConfig } from "@storybook/nextjs" +const config: StorybookConfig = { + stories: ["../components/**/*.mdx", "../components/**/*.stories.@(js|jsx|ts|tsx)"], + addons: [getAbsolutePath("@storybook/addon-links"), getAbsolutePath("@storybook/addon-essentials"), getAbsolutePath("@storybook/addon-interactions")], + framework: { + name: getAbsolutePath("@storybook/nextjs"), + options: {}, + }, + features: { + experimentalRSC: true, + }, + docs: { + autodocs: "tag", + }, + typescript: { + check: false, + checkOptions: {}, + reactDocgen: "react-docgen-typescript", + reactDocgenTypescriptOptions: { + shouldExtractLiteralValuesFromEnum: true, + propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true), + }, + }, +} +export default config + +function getAbsolutePath(value: string): any { + return dirname(require.resolve(join(value, "package.json"))) +} diff --git a/starters/shopify-algolia/.storybook/preview.ts b/starters/shopify-algolia/.storybook/preview.ts new file mode 100644 index 00000000..266989d5 --- /dev/null +++ b/starters/shopify-algolia/.storybook/preview.ts @@ -0,0 +1,17 @@ +import type { Preview } from "@storybook/react" + +import "../app/globals.css" + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, +} + +export default preview diff --git a/starters/shopify-algolia/app/.well-known/vercel/flags/route.ts b/starters/shopify-algolia/app/.well-known/vercel/flags/route.ts new file mode 100644 index 00000000..30903d4f --- /dev/null +++ b/starters/shopify-algolia/app/.well-known/vercel/flags/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from "next/server" +import { verifyAccess, type ApiData } from "@vercel/flags" + +export async function GET(request: NextRequest) { + const access = await verifyAccess(request.headers.get("Authorization")) + if (!access) return NextResponse.json(null, { status: 401 }) + + const apiData = { + definitions: { + isVercelAnalyticsEnabled: { + description: "Controls whether the new feature is visible", + options: [ + { value: false, label: "Off" }, + { value: true, label: "On" }, + ], + }, + isGoogleTagManagerEnabled: { + description: "Controls whether the new feature is visible", + options: [ + { value: false, label: "Off" }, + { value: true, label: "On" }, + ], + }, + isSpeedInsightsEnabled: { + description: "Controls whether the new feature is visible", + options: [ + { value: false, label: "Off" }, + { value: true, label: "On" }, + ], + }, + }, + } as ApiData + + return NextResponse.json(apiData) +} diff --git a/starters/shopify-algolia/app/access-denied/page.tsx b/starters/shopify-algolia/app/access-denied/page.tsx new file mode 100644 index 00000000..adb01d73 --- /dev/null +++ b/starters/shopify-algolia/app/access-denied/page.tsx @@ -0,0 +1,13 @@ +import Link from "next/link" + +export default function AccessDenied() { + return ( +
+

Looks like you don't have access to this page. If you were logged in before your session might've expired. Please log in again! 😊

+ + + Go Home + +
+ ) +} diff --git a/starters/shopify-algolia/app/actions/cart.actions.ts b/starters/shopify-algolia/app/actions/cart.actions.ts new file mode 100644 index 00000000..4459ef29 --- /dev/null +++ b/starters/shopify-algolia/app/actions/cart.actions.ts @@ -0,0 +1,86 @@ +"use server" + +import { revalidateTag, unstable_cache } from "next/cache" +import { cookies } from "next/headers" +import { storefrontClient } from "clients/storefrontClient" +import { COOKIE_CART_ID, TAGS } from "constants/index" +import { isDemoMode } from "utils/demoUtils" + +export const getCart = unstable_cache(async (cartId: string) => storefrontClient.getCart(cartId), [TAGS.CART], { revalidate: 60 * 15, tags: [TAGS.CART] }) + +export async function addCartItem(prevState: any, variantId: string) { + if (isDemoMode()) return { ok: false, message: "Demo mode active. Filtering, searching, and adding to cart disabled." } + if (!variantId) return { ok: false } + + let cartId = cookies().get(COOKIE_CART_ID)?.value + let cart + + if (cartId) cart = await storefrontClient.getCart(cartId) + + if (!cartId || !cart) { + cart = await storefrontClient.createCart([]) + cartId = cart?.id + cartId && cookies().set(COOKIE_CART_ID, cartId) + + revalidateTag(TAGS.CART) + } + + const itemAvailability = await getItemAvailability(cartId, variantId) + + if (!itemAvailability || itemAvailability.inCartQuantity >= itemAvailability.inStockQuantity) + return { + ok: false, + message: "This product is out of stock", + } + + await storefrontClient.createCartItem(cartId!, [{ merchandiseId: variantId, quantity: 1 }]) + revalidateTag(TAGS.CART) + + return { ok: true } +} + +export async function getItemAvailability(cartId: string | null | undefined, variantId: string | null | undefined) { + if (!cartId || !variantId) return { inCartQuantity: 0, inStockQuantity: Infinity } + + const cart = await storefrontClient.getCart(cartId) + const cartItem = cart?.items?.find((item) => item.merchandise.id === variantId) + + return { inCartQuantity: cartItem?.quantity ?? 0, inStockQuantity: cartItem?.merchandise.quantityAvailable ?? Infinity } +} + +export async function removeCartItem(prevState: any, itemId: string) { + const cartId = cookies().get(COOKIE_CART_ID)?.value + + if (!cartId) return { ok: false } + + await storefrontClient.deleteCartItem(cartId!, [itemId]) + revalidateTag(TAGS.CART) + + return { ok: true } +} + +export async function updateItemQuantity(prevState: any, payload: { itemId: string; variantId: string; quantity: number }) { + const cartId = cookies().get(COOKIE_CART_ID)?.value + + if (!cartId) return { ok: false } + + const { itemId, variantId, quantity } = payload + + if (quantity === 0) { + await storefrontClient.deleteCartItem(cartId, [itemId]) + revalidateTag(TAGS.CART) + return { ok: true } + } + + const itemAvailability = await getItemAvailability(cartId, variantId) + if (!itemAvailability || quantity > itemAvailability.inStockQuantity) + return { + ok: false, + message: "This product is out of stock", + } + + await storefrontClient.updateCartItem(cartId, [{ id: itemId, merchandiseId: variantId, quantity }]) + + revalidateTag(TAGS.CART) + return { ok: true } +} diff --git a/starters/shopify-algolia/app/actions/collection.actions.ts b/starters/shopify-algolia/app/actions/collection.actions.ts new file mode 100644 index 00000000..50a6c5a9 --- /dev/null +++ b/starters/shopify-algolia/app/actions/collection.actions.ts @@ -0,0 +1,26 @@ +"use server" + +import { unstable_cache } from "next/cache" +import { algolia } from "clients/search" +import { getDemoSingleCategory, isDemoMode } from "utils/demoUtils" +import type { PlatformCollection } from "lib/shopify/types" +import { env } from "env.mjs" + +export const getCollection = unstable_cache( + async (slug: string) => { + if (isDemoMode()) return getDemoSingleCategory(slug) + + const results = await algolia.search({ + indexName: env.ALGOLIA_CATEGORIES_INDEX, + searchParams: { + filters: algolia.filterBuilder().where("handle", slug).build(), + hitsPerPage: 1, + attributesToRetrieve: ["handle", "title", "seo"], + }, + }) + + return results.hits.find(Boolean) || null + }, + ["category-by-handle"], + { revalidate: 3600 } +) diff --git a/starters/shopify-algolia/app/actions/favorites.actions.ts b/starters/shopify-algolia/app/actions/favorites.actions.ts new file mode 100644 index 00000000..b4ec63d1 --- /dev/null +++ b/starters/shopify-algolia/app/actions/favorites.actions.ts @@ -0,0 +1,20 @@ +"use server" + +import { COOKIE_FAVORITES } from "constants/index" +import { cookies } from "next/headers" + +export async function toggleFavoriteProduct(prevState: any, handle: string) { + const handles = await getParsedFavoritesHandles() + const isFavorite = handles.includes(handle) + const newFavorites = handles.includes(handle) ? handles.filter((i) => i !== handle) : [...handles, handle] + + cookies().set(COOKIE_FAVORITES, JSON.stringify(newFavorites)) + + return !isFavorite +} + +export async function getParsedFavoritesHandles() { + const favoritesCookie = cookies().get(COOKIE_FAVORITES)?.value || "[]" + const favoritesHandles = JSON.parse(favoritesCookie) as string[] + return favoritesHandles +} diff --git a/starters/shopify-algolia/app/actions/page.actions.ts b/starters/shopify-algolia/app/actions/page.actions.ts new file mode 100644 index 00000000..74d98775 --- /dev/null +++ b/starters/shopify-algolia/app/actions/page.actions.ts @@ -0,0 +1,8 @@ +"use server" + +import { storefrontClient } from "clients/storefrontClient" +import { unstable_cache } from "next/cache" + +export const getPage = unstable_cache(async (handle: string) => await storefrontClient.getPage(handle), ["page"], { revalidate: 3600 }) + +export const getAllPages = unstable_cache(async () => await storefrontClient.getAllPages(), ["page"], { revalidate: 3600 }) diff --git a/starters/shopify-algolia/app/actions/product.actions.ts b/starters/shopify-algolia/app/actions/product.actions.ts new file mode 100644 index 00000000..42bffde1 --- /dev/null +++ b/starters/shopify-algolia/app/actions/product.actions.ts @@ -0,0 +1,76 @@ +"use server" + +import { unstable_cache } from "next/cache" +import { env } from "env.mjs" + +import { algolia } from "clients/search" +import type { Review } from "lib/reviews/types" + +import { getDemoProductReviews, getDemoSingleProduct, isDemoMode } from "utils/demoUtils" +import type { CommerceProduct } from "types" +import { notifyOptIn } from "utils/opt-in" + +export const searchProducts = unstable_cache( + async (query: string, limit: number = 4) => { + if (isDemoMode()) + return { + hits: [], + hasMore: false, + } + + const { hits, estimatedTotalHits } = await algolia.search({ + indexName: env.ALGOLIA_PRODUCTS_INDEX, + searchParams: { + query, + hitsPerPage: limit, + attributesToRetrieve: ["id", "handle", "title", "featuredImage", "images", "variants"], + }, + }) + return { hits, hasMore: estimatedTotalHits > limit } + }, + ["autocomplete-search"], + { revalidate: 3600 } +) + +export const getProduct = unstable_cache( + async (handle: string) => { + if (isDemoMode()) return getDemoSingleProduct(handle) + + const { hits } = await algolia.search({ + indexName: env.ALGOLIA_PRODUCTS_INDEX, + searchParams: { + filters: algolia.filterBuilder().where("handle", handle).build(), + hitsPerPage: 1, + }, + }) + + return hits.find(Boolean) || null + }, + ["product-by-handle"], + { revalidate: 3600 } +) + +export const getProductReviews = unstable_cache( + async (handle: string, { page = 1, limit = 10 } = { page: 1, limit: 10 }) => { + if (isDemoMode()) return getDemoProductReviews() + + if (!env.ALGOLIA_REVIEWS_INDEX) { + notifyOptIn({ feature: "reviews", source: "product.actions.ts" }) + return { reviews: [], total: 0 } + } + + const { hits, nbHits } = await algolia.search({ + indexName: env.ALGOLIA_REVIEWS_INDEX, + searchParams: { + filters: algolia.filterBuilder().where("product_handle", handle).and().where("published", "true").and().where("hidden", "false").build(), + hitsPerPage: limit, + page, + attributesToRetrieve: ["body", "rating", "verified", "reviewer", "published", "created_at", "hidden", "featured"], + }, + }) + + return { reviews: hits.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()), total: nbHits } + }, + ["product-reviews-by-handle"], + { revalidate: 3600 } +) diff --git a/starters/shopify-algolia/app/actions/reviews.actions.ts b/starters/shopify-algolia/app/actions/reviews.actions.ts new file mode 100644 index 00000000..ac9c03e3 --- /dev/null +++ b/starters/shopify-algolia/app/actions/reviews.actions.ts @@ -0,0 +1,15 @@ +"use server" + +import { reviewsClient } from "clients/reviews" +import type { ProductReviewBody } from "lib/reviews/types" + +import { headers } from "next/headers" + +export const submitReview = async (payload: Omit) => { + try { + const ipAddress = headers().get("x-forwarded-for") || null + await reviewsClient.createProductReview({ ...payload, ip_addr: ipAddress }) + } catch (err) { + throw new Error(err as string) + } +} diff --git a/starters/shopify-algolia/app/actions/user.actions.ts b/starters/shopify-algolia/app/actions/user.actions.ts new file mode 100644 index 00000000..f79d8f5f --- /dev/null +++ b/starters/shopify-algolia/app/actions/user.actions.ts @@ -0,0 +1,34 @@ +"use server" + +import { PlatformUserCreateInput } from "lib/shopify/types" +import { cookies } from "next/headers" +import { storefrontClient } from "clients/storefrontClient" +import { COOKIE_ACCESS_TOKEN } from "constants/index" + +export async function signupUser({ email, password }: { email: string; password: string }) { + const user = await storefrontClient.createUser({ email, password }) + return user +} + +export async function loginUser({ email, password }: { email: string; password: string }) { + const user = await storefrontClient.createUserAccessToken({ email, password }) + cookies().set(COOKIE_ACCESS_TOKEN, user?.accessToken || "", { expires: new Date(user?.expiresAt || "") }) + return user +} + +export async function getCurrentUser() { + const accessToken = cookies().get(COOKIE_ACCESS_TOKEN)?.value + const user = await storefrontClient.getUser(accessToken || "") + return user +} + +export async function updateUser(input: Pick) { + const accessToken = cookies().get(COOKIE_ACCESS_TOKEN)?.value + + const user = await storefrontClient.updateUser(accessToken!, { ...input }) + return user +} + +export async function logoutUser() { + cookies().delete(COOKIE_ACCESS_TOKEN) +} diff --git a/starters/shopify-algolia/app/api/feed/sync/route.ts b/starters/shopify-algolia/app/api/feed/sync/route.ts new file mode 100644 index 00000000..0086cb10 --- /dev/null +++ b/starters/shopify-algolia/app/api/feed/sync/route.ts @@ -0,0 +1,121 @@ +import type { PlatformProduct } from "lib/shopify/types" +import { algolia } from "clients/search" +import { storefrontClient } from "clients/storefrontClient" +import { env } from "env.mjs" +import { compareHmac } from "utils/compare-hmac" +import { enrichProduct } from "utils/enrich-product" + +type SupportedTopic = "products/update" | "products/delete" | "products/create" | "collections/update" | "collections/delete" | "collections/create" + +/* + * Callback Endpoint for Shopify Webhook product updates + */ +export async function POST(req: Request) { + const hmac = req.headers.get("X-Shopify-Hmac-Sha256") + const topic = req.headers.get("X-Shopify-Topic") + const secret = env.SHOPIFY_APP_API_SECRET_KEY + const rawPayload = await req.text() + + if (!secret || !hmac || !topic) { + return new Response(JSON.stringify({ message: "Not all credentials were provided for the deployment" }), { status: 500, headers: { "Content-Type": "application/json" } }) + } + + if ( + !compareHmac({ + body: rawPayload, + hmac, + secret, + }) + ) { + return new Response(JSON.stringify({ message: "Could not verify request." }), { status: 401, headers: { "Content-Type": "application/json" } }) + } + // there is no clear docs for what the payload looks like for different topics + const { id } = JSON.parse(rawPayload) as Record + + if (!id) { + return new Response(JSON.stringify({ message: "Invalid payload" }), { status: 400, headers: { "Content-Type": "application/json" } }) + } + + if (topic.startsWith("products")) { + return await handleProductTopics(topic as SupportedTopic, { id }) + } else if (topic.startsWith("collections")) { + return await handleCollectionTopics(topic as SupportedTopic, { id }) + } else { + return new Response(JSON.stringify({ message: "Unsupported topic" }), { status: 400, headers: { "Content-Type": "application/json" } }) + } +} + +async function handleCollectionTopics(topic: SupportedTopic, { id }: Record): Promise { + switch (topic) { + case "collections/update": + case "collections/create": + const collection = await storefrontClient.getCollectionById(makeShopifyId(`${id}`, "Collection")) + if (!collection) { + console.error(`Collection ${id} not found`) + return new Response(JSON.stringify({ message: "Collection not found" }), { status: 404, headers: { "Content-Type": "application/json" } }) + } + await algolia.update({ + indexName: env.ALGOLIA_CATEGORIES_INDEX, + objects: [{ ...collection, id: `${id}` }], + }) + + break + + case "collections/delete": + await algolia.delete({ + indexName: env.ALGOLIA_CATEGORIES_INDEX, + objectIDs: [id], + }) + break + + default: + return new Response(JSON.stringify({ message: "Unsupported topic" }), { status: 400, headers: { "Content-Type": "application/json" } }) + } + + return new Response(JSON.stringify({ message: "Success" }), { status: 200, headers: { "Content-Type": "application/json" } }) +} + +async function handleProductTopics(topic: SupportedTopic, { id }: Record): Promise { + switch (topic) { + case "products/update": + case "products/create": + const product = await storefrontClient.getProduct(makeShopifyId(`${id}`, "Product")) + const items = env.SHOPIFY_HIERARCHICAL_NAV_HANDLE ? (await storefrontClient.getHierarchicalCollections(env.SHOPIFY_HIERARCHICAL_NAV_HANDLE)).items : [] + + if (!product) { + console.error(`Product ${id} not found`) + return new Response(JSON.stringify({ message: "Product not found" }), { status: 404, headers: { "Content-Type": "application/json" } }) + } + + const enrichedProduct = await enrichProduct(product, items) + await algolia.update({ + indexName: env.ALGOLIA_PRODUCTS_INDEX, + objects: [normalizeProduct(enrichedProduct, id)], + }) + + break + case "products/delete": + await algolia.delete({ + indexName: env.ALGOLIA_PRODUCTS_INDEX, + objectIDs: [id], + }) + break + + default: + return new Response(JSON.stringify({ message: "Unsupported topic" }), { status: 400, headers: { "Content-Type": "application/json" } }) + } + + return new Response(JSON.stringify({ message: "Success" }), { status: 200, headers: { "Content-Type": "application/json" } }) +} + +/* Extract into utils */ +function normalizeProduct(product: PlatformProduct, originalId: string): PlatformProduct { + return { + ...product, + id: originalId, + } +} + +function makeShopifyId(id: string, type: "Product" | "Collection") { + return id.startsWith("gid://shopify/") ? id : `gid://shopify/${type}/${id}` +} diff --git a/starters/shopify-algolia/app/api/health/route.ts b/starters/shopify-algolia/app/api/health/route.ts new file mode 100644 index 00000000..e8fd8c53 --- /dev/null +++ b/starters/shopify-algolia/app/api/health/route.ts @@ -0,0 +1,3 @@ +export async function GET() { + return Response.json({ status: "ok" }) +} diff --git a/starters/shopify-algolia/app/api/redirects/route.ts b/starters/shopify-algolia/app/api/redirects/route.ts new file mode 100644 index 00000000..7b1fd799 --- /dev/null +++ b/starters/shopify-algolia/app/api/redirects/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from "next/server" +import redirects from "../../../redirects/new-redirects.json" + +type RedirectEntry = { + destination: string + permanent: boolean +} + +export const runtime = "edge" + +export function GET(request: NextRequest) { + const pathname = request.nextUrl.searchParams.get("pathname") + + if (!pathname) { + return new Response("Bad Request", { status: 400 }) + } + + const redirect = (redirects as Record)[pathname] + + if (!redirect) { + return new Response("No redirect", { status: 400 }) + } + + return NextResponse.json(redirect) +} diff --git a/starters/shopify-algolia/app/api/reviews/ai-summary/route.ts b/starters/shopify-algolia/app/api/reviews/ai-summary/route.ts new file mode 100644 index 00000000..7fc36d89 --- /dev/null +++ b/starters/shopify-algolia/app/api/reviews/ai-summary/route.ts @@ -0,0 +1,173 @@ +import { generateObject } from "ai" +import z from "zod" +import { openai } from "@ai-sdk/openai" +import type { Review } from "lib/reviews/types" +import type { CommerceProduct } from "types" +import { algolia } from "clients/search" +import { env } from "env.mjs" +import { authenticate } from "utils/authenticate-api-route" +import { isOptIn, notifyOptIn } from "utils/opt-in" +import { unstable_noStore } from "next/cache" +import { isDemoMode } from "utils/demoUtils" + +const summarySchema = z.object({ + products: z.array( + z.object({ + handle: z.string(), + id: z.string(), + reviewsSummary: z.string(), + }) + ), +}) + +export const maxDuration = 60 + +/* + * This API route will be used for cron job, running once a week to re-generate AI summary based on all user reviews, tweak to your needs + */ +export async function GET(req: Request) { + unstable_noStore() + if (!authenticate(req)) { + return new Response("Unauthorized", { + status: 401, + }) + } + + if (!isOptIn("ai-reviews")) { + const res = notifyOptIn({ feature: "ai-reviews", source: "api/reviews/ai-summary" }) + return new Response(JSON.stringify(res), { status: 200 }) + } + + if (isDemoMode() || !env.ALGOLIA_REVIEWS_INDEX) { + console.error({ + message: "Lacking environment variables", + source: "api/reviews/ai-summary", + }) + return new Response(JSON.stringify({ message: "Sorry, something went wrong" }), { status: 500 }) + } + + const [{ hits: allReviews = [] }, { hits: allProducts = [] }] = await Promise.all([ + algolia.getAllResults({ + indexName: env.ALGOLIA_REVIEWS_INDEX, + browseParams: { + attributesToRetrieve: ["body", "title", "product_handle", "rating"], + filters: "published:true AND hidden:false", + }, + }), + algolia.getAllResults({ + indexName: env.ALGOLIA_PRODUCTS_INDEX, + browseParams: { + attributesToRetrieve: ["handle", "title", "id", "totalReviews"], + }, + }), + ]) + + const mappedReviews: Record = allReviews.reduce((acc, review) => { + const productHandle = review.product_handle + if (acc[productHandle]) { + acc[productHandle].push(review) + } else { + acc[productHandle] = [review] + } + + return acc + }, {}) + + const productsWithNewReviews = allProducts.filter((product) => product.totalReviews !== (mappedReviews[product.handle]?.length || 0)) + + if (!productsWithNewReviews.length) { + return new Response(JSON.stringify({ message: "No new reviews to re-generate summary" }), { status: 200 }) + } + + const productsWithReviews = productsWithNewReviews + .map((product) => { + if (!mappedReviews[product.handle]) { + return null + } + return { + product_title: product.title, + id: product.id, + handle: product.handle, + reviews: mappedReviews[product.handle], + } + }) + .filter(Boolean) + + const batches: Batch[] = [] + const results: { handle: string; id: string; reviewsSummary: string }[] = [] + const batchSize = 25 + + for (let i = 0; i < productsWithReviews.length; i += batchSize) { + const batch = productsWithReviews.slice(i, i + batchSize) + + batches.push(batch) + } + + for (const batch of batches) { + const products = await generateBatchSummaries(batch) + results.push(...products) + } + + const updatedProducts = results + .map((result) => { + const id = productsWithReviews?.find(({ id }) => id === result.id)?.id + + if (!id) return null + + return { + id, + reviewsSummary: result.reviewsSummary, + } + }) + .filter(Boolean) + + await algolia.update({ indexName: env.ALGOLIA_PRODUCTS_INDEX, objects: updatedProducts }) + + return new Response(JSON.stringify({ message: "Reviews synced" }), { status: 200 }) +} + +type Batch = { + handle: string + id: string + reviews: Review[] +}[] + +const instructions = ` + You will be given a list of products and a list reviews for each of them. Your task is to generate a short summary (maximum up to 4 sentences) for each product based on the reviews provided highlighting best features and one or two areas of improvement if any are mentioned if not just don't mention it at all. Avoid repeating the same information in the summary whilst keeping casual tone. + + Example of its structure: + { + "products": [{ + "product_title": "Sleek Watch", + "id": "1", + "handle": "sleek-watch", + "reviews": [ + { + "title": "Great watch", + "body": "I love this watch, it's sleek and stylish. The only downside is that the battery life is not as long as I would like.", + "rating": 5 + }, + { + "title": "Very stylish", + "body": "I love the design of this watch, it's very stylish and goes with everything. The only downside is that the strap is a bit uncomfortable.", + "rating": 4 + }, + + ] + }] + } + + Here's the list of products and reviews: + ` + +async function generateBatchSummaries(batch: Batch) { + const { object } = await generateObject({ + model: openai("gpt-4o"), + system: instructions, + prompt: JSON.stringify(batch), + schema: summarySchema, + mode: "json", + }) + + return object.products +} diff --git a/starters/shopify-algolia/app/api/reviews/sync/route.ts b/starters/shopify-algolia/app/api/reviews/sync/route.ts new file mode 100644 index 00000000..88085b5f --- /dev/null +++ b/starters/shopify-algolia/app/api/reviews/sync/route.ts @@ -0,0 +1,93 @@ +import { unstable_noStore } from "next/cache" +import { algolia } from "clients/search" +import { reviewsClient } from "clients/reviews" +import { env } from "env.mjs" +import { authenticate } from "utils/authenticate-api-route" +import { isOptIn, notifyOptIn } from "utils/opt-in" +import type { Review } from "lib/reviews/types" +import type { CommerceProduct } from "types" +import { isDemoMode } from "utils/demoUtils" + +export const maxDuration = 60 + +export async function GET(req: Request) { + unstable_noStore() + if (!authenticate(req)) { + return new Response("Unauthorized", { + status: 401, + }) + } + + if (!isOptIn("reviews")) { + const res = notifyOptIn({ feature: "reviews", source: "api/reviews/sync" }) + return new Response(JSON.stringify(res), { status: 200 }) + } + + if (isDemoMode() || !env.ALGOLIA_REVIEWS_INDEX) { + console.error({ + message: "Lacking environment variables", + source: "api/reviews/sync", + }) + return new Response(JSON.stringify({ message: "Sorry, something went wrong" }), { status: 500 }) + } + + const [allReviews = [], { hits: allProducts = [] }, { hits: allIndexReviews = [] }] = await Promise.all([ + reviewsClient.getAllProductReviews(), + algolia.getAllResults({ + indexName: env.ALGOLIA_PRODUCTS_INDEX, + browseParams: { + attributesToRetrieve: ["handle", "totalReviews", "avgRating", "id"], + }, + }), + algolia.getAllResults({ + indexName: env.ALGOLIA_REVIEWS_INDEX, + browseParams: { + attributesToRetrieve: ["updated_at", "id"], + }, + }), + ]) + + const reviewsDelta = allReviews.filter((review) => { + const indexReview = allIndexReviews.find((r) => r.id === review.id) + return indexReview?.updated_at !== review.updated_at + }) + + const productTotalReviewsDelta = allProducts + .map((product) => { + const productReviews = allReviews.filter((review) => review.product_handle === product.handle && review.published && !review.hidden) + if (!!productReviews.length && productReviews.length !== product.totalReviews) { + const avgRating = productReviews.reduce((acc, review) => acc + review.rating, 0) / productReviews.length || 0 + return { ...product, avgRating, totalReviews: productReviews.length } + } + + return null + }) + .filter(Boolean) + + if (!reviewsDelta.length && !productTotalReviewsDelta.length) { + return new Response(JSON.stringify({ message: "Nothing to sync" }), { + status: 200, + }) + } + + !!reviewsDelta.length && + (async () => { + algolia.update({ + indexName: env.ALGOLIA_REVIEWS_INDEX!, + objects: reviewsDelta, + }) + console.log("API/sync: Reviews synced", reviewsDelta.length) + })() + !!productTotalReviewsDelta.length && + (async () => { + algolia.update({ + indexName: env.ALGOLIA_PRODUCTS_INDEX, + objects: productTotalReviewsDelta, + }) + console.log("API/sync:Products synced", productTotalReviewsDelta.length) + })() + + return new Response(JSON.stringify({ message: "All synced" }), { + status: 200, + }) +} diff --git a/starters/shopify-algolia/app/category/clp/[slug]/[page]/page.tsx b/starters/shopify-algolia/app/category/clp/[slug]/[page]/page.tsx new file mode 100644 index 00000000..ba9b0dfb --- /dev/null +++ b/starters/shopify-algolia/app/category/clp/[slug]/[page]/page.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from "next" +import { CategoryView } from "views/Category/CategoryView" + +export const revalidate = 86400 +export const dynamic = "force-static" + +interface CategoryPageProps { + params: { slug: string; page: string } +} + +export async function generateMetadata({ params }: CategoryPageProps): Promise { + return { + title: `${params.slug} | Enterprise Commerce`, + description: "In excepteur elit mollit in.", + } +} + +export async function generateStaticParams() { + return [] +} + +export default async function CategoryPage({ params }: CategoryPageProps) { + return +} diff --git a/starters/shopify-algolia/app/category/clp/[slug]/page.tsx b/starters/shopify-algolia/app/category/clp/[slug]/page.tsx new file mode 100644 index 00000000..b2961f13 --- /dev/null +++ b/starters/shopify-algolia/app/category/clp/[slug]/page.tsx @@ -0,0 +1,38 @@ +import { PlatformCollection } from "lib/shopify/types" +import { algolia } from "clients/search" +import { env } from "env.mjs" +import type { Metadata } from "next" +import { isDemoMode } from "utils/demoUtils" +import { CategoryView } from "views/Category/CategoryView" + +export const revalidate = 86400 +export const dynamic = "force-static" + +interface CategoryPageProps { + params: { slug: string } +} + +export async function generateMetadata({ params }: CategoryPageProps): Promise { + return { + title: `${params.slug} | Enterprise Commerce`, + description: "In excepteur elit mollit in.", + } +} + +export async function generateStaticParams() { + if (isDemoMode()) return [] + + const { hits } = await algolia.search({ + indexName: env.ALGOLIA_CATEGORIES_INDEX, + searchParams: { + hitsPerPage: 50, + attributesToRetrieve: ["handle"], + }, + }) + + return hits.map(({ handle }) => ({ slug: handle })) +} + +export default async function CategoryPage({ params }: CategoryPageProps) { + return +} diff --git a/starters/shopify-algolia/app/category/plp/[slug]/page.tsx b/starters/shopify-algolia/app/category/plp/[slug]/page.tsx new file mode 100644 index 00000000..a1ca05ef --- /dev/null +++ b/starters/shopify-algolia/app/category/plp/[slug]/page.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from "next" +import { SearchParamsType } from "types" +import { CategoryView } from "views/Category/CategoryView" + +export const runtime = "nodejs" + +export const revalidate = 86400 + +interface ProductListingPageProps { + searchParams: SearchParamsType + params: { slug: string } +} + +export async function generateMetadata({ params }: ProductListingPageProps): Promise { + return { + title: `${params.slug} | Enterprise Commerce`, + description: "In excepteur elit mollit in.", + } +} + +export default async function ProductListingPage({ searchParams, params }: ProductListingPageProps) { + return +} diff --git a/starters/shopify-algolia/app/error.tsx b/starters/shopify-algolia/app/error.tsx new file mode 100644 index 00000000..454e8dcf --- /dev/null +++ b/starters/shopify-algolia/app/error.tsx @@ -0,0 +1,33 @@ +"use client" + +import { Button } from "components/Button/Button" +import { isDemoMode } from "utils/demoUtils" + +export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( +
+
+ {isDemoMode() ? ( + <> +

Something went wrong!

+

+ This feature requires full functionality. Please exit Demo Mode by setting the required environment variables. +

+
{JSON.stringify(error, null, 2)}
+ + + ) : ( + <> +

Something went wrong!

+
{JSON.stringify(error, null, 2)}
+ + + )} +
+
+ ) +} diff --git a/starters/shopify-algolia/app/favorites/page.tsx b/starters/shopify-algolia/app/favorites/page.tsx new file mode 100644 index 00000000..432e1bce --- /dev/null +++ b/starters/shopify-algolia/app/favorites/page.tsx @@ -0,0 +1,61 @@ +import { cookies } from "next/headers" +import { Suspense } from "react" +import { getProduct } from "app/actions/product.actions" +import { ProductCard } from "components/ProductCard/ProductCard" +import { Skeleton } from "components/Skeleton/Skeleton" +import { COOKIE_FAVORITES } from "constants/index" + +export const revalidate = 86400 + +export const dynamicParams = true + +export default async function Favorites() { + return ( +
+
+

Favorite products

+
+ }> + + +
+ ) +} + +async function FavoritesView() { + let favoritesHandles: string[] = [] + const favoritesCookie = cookies().get(COOKIE_FAVORITES)?.value + + if (favoritesCookie) { + favoritesHandles = JSON.parse(favoritesCookie) as string[] + } + + const products = await Promise.all(favoritesHandles.map((handle) => getProduct(handle)).filter(Boolean)) + + return ( + <> + {products.length === 0 ?

No favorite products. You can add them by clicking on a heart icon on product page

: null} +
+ {products.map((singleResult, idx) => ( + + ))} +
+ + ) +} + +function FavoritesSkeleton() { + return ( +
+ {Array.from({ length: 8 }).map((_, index) => ( +
+ +
+ + +
+
+ ))} +
+ ) +} diff --git a/starters/shopify-algolia/app/global-error.tsx b/starters/shopify-algolia/app/global-error.tsx new file mode 100644 index 00000000..754630ea --- /dev/null +++ b/starters/shopify-algolia/app/global-error.tsx @@ -0,0 +1,19 @@ +"use client" + +import { Button } from "components/Button/Button" + +export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( + + +
+

Something went wrong!

+
{JSON.stringify(error, null, 2)}
+ +
+ + + ) +} diff --git a/starters/shopify-algolia/app/globals.css b/starters/shopify-algolia/app/globals.css new file mode 100644 index 00000000..f8c4a02a --- /dev/null +++ b/starters/shopify-algolia/app/globals.css @@ -0,0 +1,22 @@ +@import "./styles/reset.css"; +@import "./styles/megamenu.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; +} + +@layer utilities { + /* Hide scrollbar for Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + /* Hide scrollbar for IE, Edge and Firefox */ + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} diff --git a/starters/shopify-algolia/app/home/[bucket]/page.tsx b/starters/shopify-algolia/app/home/[bucket]/page.tsx new file mode 100644 index 00000000..9e78392c --- /dev/null +++ b/starters/shopify-algolia/app/home/[bucket]/page.tsx @@ -0,0 +1,44 @@ +import { Suspense } from "react" +import { BUCKETS } from "constants/index" +import { BestOffersSection } from "views/Homepage/BestOffersSection" +import { CarouselSectionSkeleton } from "views/Homepage/CarouselSection" +import { CategoriesSection, CategoriesSectionSkeleton } from "views/Homepage/CategoriesSection" +import { EverythingUnderSection } from "views/Homepage/EverythingUnderSection" +import { AnnouncementBar } from "components/AnnouncementBar/AnnouncementBar" +import { HeroSection } from "views/Homepage/HeroSection" + +export const revalidate = 86400 + +export const dynamic = "force-static" + +export const dynamicParams = true + +export default function Homepage({ params: { bucket } }: { params: { bucket: string } }) { + const heroTitles = { + a: "Your daily trendsetting deals", + b: "Spring into Savings! Up to 60% Off", + } + + return ( +
+ + + + }> + + + + }> + + + + }> + + +
+ ) +} + +export async function generateStaticParams() { + return BUCKETS.HOME.map((bucket) => ({ bucket })) +} diff --git a/starters/shopify-algolia/app/icon.png b/starters/shopify-algolia/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3fab1350884dba849a1a22fe43b086aa98c9f08b GIT binary patch literal 24936 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 IGbYCTA4l}JyZ`_I literal 0 HcmV?d00001 diff --git a/starters/shopify-algolia/app/layout.tsx b/starters/shopify-algolia/app/layout.tsx new file mode 100644 index 00000000..233d5c0a --- /dev/null +++ b/starters/shopify-algolia/app/layout.tsx @@ -0,0 +1,271 @@ +import "./globals.css" + +import nextDynamic from "next/dynamic" +import Script from "next/script" +import { Suspense } from "react" +import { Toaster } from "sonner" +import { CallToAction } from "components/CallToAction/CallToAction" +import { Footer } from "components/Footer/Footer" +import { Modals } from "components/Modals/Modals" +import { mobileInlineScript } from "components/NavigationBar/mobileInlineScript" +import { NavigationBar } from "components/NavigationBar/NavigationBar" +import { NavItem } from "components/NavigationBar/types" +import { FlagValues } from "views/FlagValues" +import { ThirdParties } from "views/ThirdParties" +import { env } from "env.mjs" +import { Metadata } from "next" +import { GithubBadge } from "views/GithubBadge" +import { DemoModeAlert } from "views/DemoModeAlert" +import { CartView } from "views/Cart/CartView" + +const DraftToolbar = nextDynamic(() => import("views/DraftToolbar"), { ssr: false }) + +export const revalidate = 86400 + +const navigationItems: NavItem[] = [ + { + text: "Fashion", + href: "/category/fashion", + submenu: { + variant: "text-grid", + items: [ + { + text: "Women", + href: "/category/women", + items: [ + { text: "Shirts & Blouses", href: "/category/shirts-and-blouses" }, + { text: "Blazers & Vests", href: "/category/blazers-and-vests" }, + { text: "Cardigans & Sweaters", href: "/category/cardigans-and-sweaters" }, + { text: "Dresses", href: "/category/dresses" }, + { text: "Skirts", href: "/category/skirts" }, + ], + }, + { + text: "Men", + href: "/category/men", + items: [ + { text: "T-shirts & Tanks", href: "/category/t-shirts-and-tanks" }, + { text: "Hoodies & Sweatshirts", href: "/category/hoodies-and-sweatshirts" }, + { text: "Blazers & Suits", href: "/category/blazers-and-suits" }, + { text: "Shorts", href: "/category/shorts" }, + { text: "Outerwear", href: "/category/outerwear" }, + ], + }, + { + text: "Kids", + href: "/category/kids", + items: [ + { text: "Clothing", href: "/category/clothing" }, + { text: "Activewear", href: "/category/activewear" }, + { text: "Accessories", href: "/category/kids-accessories" }, + { text: "Footwear", href: "/category/footwear" }, + ], + }, + ], + }, + }, + { + text: "Electronics", + href: "/category/electronics", + submenu: { + variant: "text-grid", + items: [ + { + text: "Audio Devices", + href: "/category/audio-devices", + items: [ + { text: "Headphones", href: "/category/headphones" }, + { text: "Speakers", href: "/category/speakers" }, + ], + }, + { + text: "Cameras", + href: "/category/cameras", + items: [ + { text: "Digital Cameras", href: "/category/digital-cameras" }, + { text: "Action Cameras", href: "/category/action-cameras" }, + ], + }, + { + text: "Smartphones", + href: "/category/smartphones", + }, + { + text: "Laptops", + href: "/category/laptops", + }, + { + text: "Screens", + href: "/category/screens", + }, + ], + }, + }, + { + text: "Sports & Outdoors", + href: "/category/sports-and-outdoors", + submenu: { + variant: "text-grid", + items: [ + { + href: "/category/exercise-equipment", + text: "Exercise Equipment", + }, + { + href: "/category/outdoor-gear", + text: "Outdoor Gear", + }, + { + href: "/category/sportswear", + text: "Sportswear", + }, + { + href: "/category/athletic-footwear", + text: "Athletic Footwear", + }, + ], + }, + }, + { + text: "Beauty", + href: "/category/beauty", + submenu: { + variant: "text-grid", + items: [ + { + text: "Skin Care", + href: "/category/skin-care", + items: [ + { text: "Cleansers", href: "/category/cleansers" }, + { text: "Moisturizers", href: "/category/moisturizers" }, + { text: "Treatments & Serums", href: "/category/treatments-and-serums" }, + ], + }, + { + text: "Makeup", + href: "/category/makeup", + items: [ + { text: "Face Makeup", href: "/category/face-makeup" }, + { text: "Eye Makeup", href: "/category/eye-makeup" }, + { text: "Lip Makeup", href: "/category/lip-makeup" }, + ], + }, + { + text: "Haircare", + href: "/category/haircare", + items: [ + { text: "Shampoos & Conditioners", href: "/category/shampoos-and-conditioners" }, + { text: "Styling Products", href: "/category/styling-products" }, + ], + }, + + { + text: "Fragrances", + href: "/category/fragrances", + items: [ + { text: "Perfumes", href: "/category/perfumes" }, + { text: "Body Sprays", href: "/category/body-sprays" }, + ], + }, + ], + }, + }, + { + text: "Furniture", + href: "/category/furniture", + submenu: { + variant: "text-grid", + items: [ + { + text: "Living Room", + href: "/category/living-room-furniture", + items: [ + { text: "Sofas & Sectionals", href: "/category/sofas-and-sectionals" }, + { text: "Coffee Tables", href: "/category/coffee-tables" }, + { text: "TV Stands", href: "/category/tv-stands" }, + ], + }, + + { + text: "Bedroom", + href: "/category/bedroom-furniture", + items: [ + { text: "Beds", href: "/category/beds" }, + { text: "Dressers", href: "/category/dressers" }, + { text: "Nightstands", href: "/category/nightstands" }, + ], + }, + + { + text: "Office", + href: "/category/office-furniture", + items: [ + { text: "Desks", href: "/category/desks" }, + { text: "Office Chairs", href: "/category/office-chairs" }, + { text: "Storage Solutions", href: "/category/storage-solutions" }, + ], + }, + ], + }, + }, +] + +export const metadata: Metadata = { + title: "Next.js Enterprise Commerce | Blazity", + description: "AI-FIRST NEXT.JS STOREFRONT FOR COMPOSABLE COMMERCE", + metadataBase: new URL(env.LIVE_URL!), + openGraph: { + title: "Next.js Enterprise Commerce | Blazity", + description: "AI-FIRST NEXT.JS STOREFRONT FOR COMPOSABLE COMMERCE", + images: ["/opengraph-image.jpg"], + }, + twitter: { + card: "summary_large_image", + title: "Next.js Enterprise Commerce | Blazity", + description: "AI-FIRST NEXT.JS STOREFRONT FOR COMPOSABLE COMMERCE", + creator: "@blazity", + images: ["/opengraph-image.jpg"], + }, + verification: { + google: "google", + yandex: "yandex", + yahoo: "yahoo", + }, + generator: "Next.js", + applicationName: "Next.js", +} + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {children} + + +