├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── README.md
├── api
└── apiInstance.ts
├── app
├── api
│ ├── admin
│ │ └── goods
│ │ │ ├── delete-many
│ │ │ └── route.ts
│ │ │ ├── delete
│ │ │ └── route.ts
│ │ │ ├── list
│ │ │ └── route.ts
│ │ │ └── one
│ │ │ └── route.ts
│ ├── cart
│ │ ├── add-many
│ │ │ └── route.ts
│ │ ├── add
│ │ │ └── route.ts
│ │ ├── all
│ │ │ └── route.ts
│ │ ├── count
│ │ │ └── route.ts
│ │ ├── delete-many
│ │ │ └── route.ts
│ │ └── delete
│ │ │ └── route.ts
│ ├── comparison
│ │ ├── add-many
│ │ │ └── route.ts
│ │ ├── add
│ │ │ └── route.ts
│ │ ├── all
│ │ │ └── route.ts
│ │ └── delete
│ │ │ └── route.ts
│ ├── favorites
│ │ ├── add-many
│ │ │ └── route.ts
│ │ ├── add
│ │ │ └── route.ts
│ │ ├── all
│ │ │ └── route.ts
│ │ └── delete
│ │ │ └── route.ts
│ ├── goods
│ │ ├── bestsellers
│ │ │ └── route.ts
│ │ ├── filter
│ │ │ └── route.ts
│ │ ├── new
│ │ │ └── route.ts
│ │ ├── one
│ │ │ └── route.ts
│ │ ├── search
│ │ │ └── route.ts
│ │ └── watched
│ │ │ └── route.ts
│ ├── payment
│ │ ├── check
│ │ │ └── route.ts
│ │ ├── notify
│ │ │ └── route.ts
│ │ └── route.ts
│ └── users
│ │ ├── avatar
│ │ └── route.ts
│ │ ├── delete
│ │ └── route.ts
│ │ ├── edit
│ │ ├── email
│ │ │ ├── route.ts
│ │ │ └── verify-email
│ │ │ │ └── route.ts
│ │ └── name
│ │ │ └── route.ts
│ │ ├── email
│ │ └── route.ts
│ │ ├── login-check
│ │ └── route.ts
│ │ ├── login
│ │ └── route.ts
│ │ ├── oauth
│ │ └── route.ts
│ │ ├── password-restore
│ │ ├── route.ts
│ │ └── verify-email
│ │ │ └── route.ts
│ │ ├── refresh
│ │ └── route.ts
│ │ ├── signup
│ │ └── route.ts
│ │ └── verify-code
│ │ └── route.ts
├── cart
│ └── page.tsx
├── catalog
│ ├── [category]
│ │ ├── [productId]
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── collection-products
│ └── page.tsx
├── comparison
│ ├── [type]
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── favorites
│ └── page.tsx
├── globalStyles
│ ├── auth-popup.css
│ ├── breadcrumbs.css
│ ├── cart-popup.css
│ ├── catalog-menu.css
│ ├── cookie-popup.css
│ ├── footer.css
│ ├── globals.css
│ ├── header-profile.css
│ ├── header.css
│ ├── map.css
│ ├── menu.css
│ ├── mobile-navbar.css
│ ├── normalize.css
│ ├── search-modal.css
│ ├── slick-theme.css
│ └── slick.css
├── icon.svg
├── layout.tsx
├── logo.png
├── manifest.ts
├── not-found.tsx
├── order
│ └── page.tsx
├── page.tsx
├── password-restore
│ └── page.tsx
├── payment-success
│ └── page.tsx
├── personal-data-policy
│ └── page.tsx
├── privacy-policy
│ └── page.tsx
├── profile
│ └── page.tsx
└── watched-products
│ └── page.tsx
├── components
├── elements
│ ├── AddToCartIcon
│ │ └── AddToCartIcon.tsx
│ ├── AllLink
│ │ └── AllLink.tsx
│ ├── DeleteCartItemBtn
│ │ └── DeleteCartItemBtn.tsx
│ ├── HeadingWithCount
│ │ └── HeadingWithCount.tsx
│ ├── Logo
│ │ └── Logo.tsx
│ ├── NameErrorMessage
│ │ └── NameErrorMessage.tsx
│ ├── ProductAvailable
│ │ └── ProductAvailable.tsx
│ ├── ProductItemActionBtn
│ │ └── ProductItemActionBtn.tsx
│ ├── ProductSubtitle
│ │ └── ProductSubtitle.tsx
│ ├── QuickViewModalSliderArrow
│ │ └── QuickViewModalSliderArrow.tsx
│ ├── Skeleton
│ │ └── Skeleton.tsx
│ └── Tooltip
│ │ └── Tooltip.tsx
├── hocs
│ └── withClickOutside.tsx
├── layouts
│ ├── CatalogLayout.tsx
│ ├── ComparisonLayout.tsx
│ ├── Layout.tsx
│ └── PagesLayout.tsx
├── modules
│ ├── Accordion
│ │ └── Accordion.tsx
│ ├── AuthPopup
│ │ ├── AuthPopup.tsx
│ │ ├── AuthPopupClose.tsx
│ │ ├── AuthPopupLogin.tsx
│ │ ├── AuthPopupRegistration.tsx
│ │ ├── AuthPopupSocials.tsx
│ │ ├── EmailInput.tsx
│ │ ├── NameInput.tsx
│ │ └── PasswordInput.tsx
│ ├── Breadcrumbs
│ │ ├── Breadcrumbs.tsx
│ │ └── Crumb.tsx
│ ├── CartPage
│ │ ├── CartList.tsx
│ │ ├── CartListItem.tsx
│ │ └── PromotionalCode.tsx
│ ├── CatalogFilters
│ │ ├── CatalogFilters.tsx
│ │ ├── CategoryFilterList.tsx
│ │ ├── CategorySelect.tsx
│ │ ├── CheckboxSelectItem.tsx
│ │ ├── ColorsSelect.tsx
│ │ ├── FiltersPopup
│ │ │ ├── ColorsFilter.tsx
│ │ │ ├── FiltersPopup.tsx
│ │ │ ├── PriceFilter.tsx
│ │ │ └── SizesFilter.tsx
│ │ ├── PriceSelect.tsx
│ │ ├── SelectBtn.tsx
│ │ ├── SelectInfoItem.tsx
│ │ ├── SelectItem.tsx
│ │ ├── SizesSelect.tsx
│ │ └── SortSelect.tsx
│ ├── Comparison
│ │ ├── ComparisonItem.tsx
│ │ ├── ComparisonLinksList.tsx
│ │ └── ComparisonList.tsx
│ ├── CookieAlert
│ │ └── CookieAlert.tsx
│ ├── EmptyPageContent
│ │ ├── ContentLinks.tsx
│ │ ├── ContentTitle.tsx
│ │ └── EmptyPageContent.tsx
│ ├── FavoritesPage
│ │ ├── FavoritesList.tsx
│ │ └── FavoritesListItem.tsx
│ ├── Footer
│ │ ├── Footer.tsx
│ │ ├── FooterLinks.tsx
│ │ └── FooterMobileLink.tsx
│ ├── Header
│ │ ├── BuyersListItems.tsx
│ │ ├── CartPopup
│ │ │ ├── CartPopup.tsx
│ │ │ └── CartPopupItem.tsx
│ │ ├── CatalogMenu.tsx
│ │ ├── CatalogMenuButton.tsx
│ │ ├── CatalogMenuList.tsx
│ │ ├── ContactsListItems.tsx
│ │ ├── Header.tsx
│ │ ├── HeaderProfile.tsx
│ │ ├── Menu.tsx
│ │ ├── MenuLinkItem.tsx
│ │ └── SearchModal.tsx
│ ├── MainPage
│ │ ├── BestsellerGoods.tsx
│ │ ├── BrandLife.tsx
│ │ ├── Categories
│ │ │ └── Categories.tsx
│ │ ├── Hero
│ │ │ ├── Hero.tsx
│ │ │ ├── HeroSlide.tsx
│ │ │ └── HeroSlideTooltip.tsx
│ │ ├── MainPageSection.tsx
│ │ ├── MainSlider.tsx
│ │ └── NewGoods.tsx
│ ├── MobileNavbar
│ │ └── MobileNavbar.tsx
│ ├── OrderInfoBlock
│ │ └── OrderInfoBlock.tsx
│ ├── OrderPage
│ │ ├── AddressesList.tsx
│ │ ├── CourierAddressInfo.tsx
│ │ ├── CourierAddressesItem.tsx
│ │ ├── MapModal.tsx
│ │ ├── OrderCartItem.tsx
│ │ ├── OrderDelivery.tsx
│ │ ├── OrderDetailsForm.tsx
│ │ ├── OrderPayment.tsx
│ │ ├── OrderTitle.tsx
│ │ ├── PickupAddressItem.tsx
│ │ └── TabControls.tsx
│ ├── PasswordRestorePage
│ │ └── PasswordRestoreForm.tsx
│ ├── ProductPage
│ │ ├── ProductImages.tsx
│ │ ├── ProductImagesItem.tsx
│ │ ├── ProductInfoAccordion.tsx
│ │ ├── ProductPageContent.tsx
│ │ └── ProductsByCollection.tsx
│ ├── ProductsListItem
│ │ ├── AddToCartBtn.tsx
│ │ ├── ProductColor.tsx
│ │ ├── ProductComposition.tsx
│ │ ├── ProductCountBySize.tsx
│ │ ├── ProductCounter.tsx
│ │ ├── ProductLabel.tsx
│ │ ├── ProductSizeTableBtn.tsx
│ │ ├── ProductSizesItem.tsx
│ │ └── ProductsListItem.tsx
│ ├── ProfilePage
│ │ ├── CodeInputBlock
│ │ │ ├── CodeInput.tsx
│ │ │ └── CodeInputBlock.tsx
│ │ ├── ProfileAvatar.tsx
│ │ ├── ProfileEmail.tsx
│ │ ├── ProfileInfoActions.tsx
│ │ ├── ProfileInfoBlock.tsx
│ │ └── ProfileName.tsx
│ ├── QuickViewModal
│ │ ├── QuickViewModal.tsx
│ │ └── QuickViewModalSlider.tsx
│ ├── ShareModal
│ │ └── ShareModal.tsx
│ ├── SizeTable
│ │ └── SizeTable.tsx
│ └── WatchedProducts
│ │ └── WatchedProducts.tsx
└── templates
│ ├── CartPage
│ └── CartPage.tsx
│ ├── CollectionProductsPage
│ └── CollectionProductsPage.tsx
│ ├── ComparisonPage
│ └── ComparisonPage.tsx
│ ├── FavoritesPage
│ └── FavoritesPage.tsx
│ ├── MainPage
│ └── MainPage.tsx
│ ├── OrderPage
│ └── OrderPage.tsx
│ ├── PasswordRestorePage
│ └── PasswordRestorePage.tsx
│ ├── PersonalDataPolicyPage
│ └── PersonalDataPolicyPage.tsx
│ ├── PrivacyPolicyPage
│ └── PrivacyPolicyPage.tsx
│ ├── ProductPage
│ └── ProductPage.tsx
│ ├── ProductsPage
│ └── ProductsPage.tsx
│ ├── ProfilePage
│ └── ProfilePage.tsx
│ └── WatchedProductsPage
│ └── WatchedProductsPage.tsx
├── constants
├── corsHeaders.ts
├── jwt.ts
├── lang.ts
├── map.ts
├── motion.ts
├── product.ts
└── slider.ts
├── context
├── auth
│ ├── index.ts
│ ├── init.ts
│ └── state.ts
├── cart
│ ├── index.ts
│ ├── init.ts
│ └── state.ts
├── catalog
│ ├── index.ts
│ └── state.ts
├── comparison
│ ├── index.ts
│ ├── init.ts
│ └── state.ts
├── favorites
│ ├── index.ts
│ ├── init.ts
│ └── state.ts
├── goods
│ ├── index.ts
│ ├── init.ts
│ └── state.ts
├── lang
│ ├── index.ts
│ └── state.ts
├── modals
│ ├── index.ts
│ └── state.ts
├── order
│ ├── index.ts
│ ├── init.ts
│ └── state.ts
├── passwordRestore
│ ├── index.ts
│ └── init.ts
├── profile
│ ├── index.ts
│ ├── init.ts
│ └── state.ts
├── sizeTable
│ ├── index.ts
│ └── state.ts
└── user
│ ├── index.ts
│ ├── init.ts
│ └── state.ts
├── hooks
├── useAuthForm.ts
├── useBreadcrumbs.ts
├── useCartAction.tsx
├── useCartItemAction.ts
├── useCategoryFilter.ts
├── useClickOutside.ts
├── useColorsFilter.ts
├── useComparisonAction.ts
├── useComparisonItems.ts
├── useComparisonLinks.ts
├── useCrumbText.ts
├── useDebounceCallback.ts
├── useFavoritesAction.ts
├── useGoodsByAuth.ts
├── useImagePreloader.ts
├── useLang.ts
├── useLogout.ts
├── useMediaQuery.ts
├── useMenuAnimation.ts
├── usePageTitle.ts
├── usePricceAnimation.ts
├── usePriceAction.ts
├── usePriceFilter.ts
├── useProductDelete.ts
├── useProductFilters.ts
├── useProductImages.tsx
├── useProductsByCollection.ts
├── useProfileEdit.ts
├── useSizeFilter.ts
├── useTTMap.ts
├── useTotalPrice.ts
├── useUserAvatar.ts
└── useWatchedProducts.ts
├── lib
├── mongodb
│ └── index.ts
└── utils
│ ├── api-routes.ts
│ ├── auth.ts
│ ├── cart.ts
│ ├── catalog.ts
│ ├── common.ts
│ ├── comparison.ts
│ ├── errors.ts
│ ├── favorites.ts
│ └── map.ts
├── migrate-mongo-config.js
├── migrations
├── 20240107135348-cloth.js
├── 20240107154411-accessories.js
├── 20240127125424-users.js
├── 20240129162135-cart.js
├── 20240223160629-favorites.js
├── 20240309115529-office.js
├── 20240309121125-souvenirs.js
├── 20240313172721-comparison.js
└── 20240611155233-codes.js
├── next.config.js
├── package.json
├── public
├── avatars
│ ├── 666c7b0c7bd6fa2fb8dc03e8__comet.png
│ └── 6671bb69b58fa0e16fcf40e7__nestjs.png
├── fonts
│ ├── Rostelecom-Basis-Bold.woff
│ ├── Rostelecom-Basis-Medium.woff
│ └── Rostelecom-Basis-Regular.woff
├── img
│ ├── accessories
│ │ ├── accessories-bags-1.png
│ │ ├── accessories-bags-2.png
│ │ ├── accessories-bags-3.png
│ │ ├── accessories-bags-4.png
│ │ ├── accessories-headdress.png
│ │ └── accessories-umbrella.png
│ ├── add-photo.svg
│ ├── arrow-right.svg
│ ├── black-t.png
│ ├── brands-life.png
│ ├── burger.svg
│ ├── cart-checked.svg
│ ├── cart-plus.svg
│ ├── cart.svg
│ ├── catalog.svg
│ ├── categories-img-1.png
│ ├── categories-img-2.png
│ ├── categories-img-3.png
│ ├── categories-img-4.png
│ ├── category-filter.svg
│ ├── checked-favorite.svg
│ ├── checked.svg
│ ├── close-small.svg
│ ├── clothes
│ │ ├── cloth-hoodie-1.png
│ │ ├── cloth-long-sleeves-1.png
│ │ ├── cloth-long-sleeves-2.png
│ │ ├── cloth-outerwear-1.png
│ │ ├── cloth-outerwear-2.png
│ │ ├── cloth-t-shirts-1.png
│ │ └── cloth-t-shirts-2.png
│ ├── comparison-checked.svg
│ ├── comparison-icon.png
│ ├── comparison.svg
│ ├── edit.svg
│ ├── empty-cart-page.png
│ ├── empty-cart.svg
│ ├── empty-comparison-page.png
│ ├── favorites-empty-page.png
│ ├── favorites.svg
│ ├── filters.svg
│ ├── gray-ellipse.svg
│ ├── green-ellipse.svg
│ ├── home.svg
│ ├── info-icon.svg
│ ├── logo.svg
│ ├── map-marker.svg
│ ├── menu-bg-small.png
│ ├── menu-bg.png
│ ├── menu-line-small.svg
│ ├── menu-line-tiny.svg
│ ├── menu-line.svg
│ ├── minus.svg
│ ├── mir-logo.svg
│ ├── more.svg
│ ├── not-found.png
│ ├── office
│ │ ├── office-notebook-1.png
│ │ ├── office-notebook-2.png
│ │ ├── office-pen-1.png
│ │ └── office-pen-2.png
│ ├── orange-t.png
│ ├── plus-big.svg
│ ├── plus.svg
│ ├── popup-arrow.svg
│ ├── profile.svg
│ ├── quick-view.svg
│ ├── red-ellipse.svg
│ ├── save-icon.svg
│ ├── sber-pay-logo.svg
│ ├── search-icon.svg
│ ├── search.svg
│ ├── select-arrow.svg
│ ├── share.svg
│ ├── simple-arrow.svg
│ ├── slider-arrow.svg
│ ├── sort.svg
│ ├── souvenirs
│ │ ├── business-souvenirs-1.png
│ │ ├── business-souvenirs-2.png
│ │ ├── promotional-souvenirs-1.png
│ │ └── promotional-souvenirs-2.png
│ ├── spb-logo.svg
│ ├── subtitle-rect.svg
│ ├── telegram.svg
│ ├── trash.svg
│ ├── violet-t.png
│ ├── vk.svg
│ ├── white-ellipse.svg
│ └── ytb.svg
├── next.svg
├── translations
│ └── translations.json
└── vercel.svg
├── service
└── mailService.js
├── styles
├── ad
│ └── index.module.scss
├── auth-popup
│ └── index.module.scss
├── cart-page
│ └── index.module.scss
├── cart-skeleton
│ └── index.module.scss
├── catalog
│ └── index.module.scss
├── comparison-links-skeleton
│ └── index.module.scss
├── comparison-list-skeleton
│ └── index.module.scss
├── comparison-skeleton
│ └── index.module.scss
├── comparison
│ └── index.module.scss
├── empty-content
│ └── index.module.scss
├── favorites
│ └── index.module.scss
├── heading-with-count
│ └── index.module.scss
├── main-page
│ └── index.module.scss
├── not-found
│ └── index.module.scss
├── order-block
│ └── index.module.scss
├── order
│ └── index.module.scss
├── password-restore
│ └── index.module.scss
├── payment-success
│ └── index.module.scss
├── policy
│ └── index.module.scss
├── product-count-indicator
│ └── index.module.scss
├── product-item-action-btn
│ └── index.module.scss
├── product-list-item
│ └── index.module.scss
├── product
│ └── index.module.scss
├── productSubtitle
│ └── index.module.scss
├── profile
│ └── index.module.scss
├── quick-view-modal
│ └── index.module.scss
├── share-modal
│ └── index.module.scss
├── size-table
│ └── index.module.scss
├── skeleton
│ └── index.module.scss
├── tooltip
│ └── index.module.scss
├── watched-products-page
│ └── index.module.scss
└── watched-products
│ └── index.module.scss
├── tsconfig.json
├── types
├── authPopup.ts
├── cart.ts
├── catalog.ts
├── common.ts
├── comparison.ts
├── elements.ts
├── favorites.ts
├── goods.ts
├── hocs.ts
├── main-page.ts
├── modules.ts
├── order.ts
├── passwordRestore.ts
├── product.ts
├── profile.ts
└── user.ts
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_style = space
3 | indent_size = 2
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 |
9 | [*.md]
10 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | .env
29 | # local env files
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "trailingComma": "es5",
4 | "singleQuote": true,
5 | "tabWidth": 2,
6 | "useTabs": false,
7 | "jsxSingleQuote": true
8 | }
9 |
--------------------------------------------------------------------------------
/api/apiInstance.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const instance = axios.create({
4 | baseURL: '',
5 | })
6 |
7 | export default instance
8 |
--------------------------------------------------------------------------------
/app/api/admin/goods/delete-many/route.ts:
--------------------------------------------------------------------------------
1 | import { corsHeaders } from '@/constants/corsHeaders'
2 | import clientPromise from '@/lib/mongodb'
3 | import { getDbAndReqBody } from '@/lib/utils/api-routes'
4 | import { ObjectId } from 'mongodb'
5 | import { NextResponse } from 'next/server'
6 |
7 | export async function GET(req: Request) {
8 | try {
9 | const { db } = await getDbAndReqBody(clientPromise, null)
10 | const url = new URL(req.url)
11 | const ids = url.searchParams.get('ids')
12 |
13 | if (!ids) {
14 | return NextResponse.json(
15 | {
16 | status: 404,
17 | message: 'ids required',
18 | },
19 | corsHeaders
20 | )
21 | }
22 |
23 | const parsedIds = JSON.parse(ids) as string[]
24 |
25 | const deleteManyFromCollection = async (collection: string) => {
26 | await db.collection(collection).deleteMany({
27 | _id: {
28 | $in: parsedIds.map((id) => new ObjectId(id)),
29 | },
30 | })
31 | }
32 |
33 | await Promise.allSettled([
34 | deleteManyFromCollection('cloth'),
35 | deleteManyFromCollection('accessories'),
36 | ])
37 |
38 | return NextResponse.json(
39 | {
40 | status: 204,
41 | },
42 | corsHeaders
43 | )
44 | } catch (error) {
45 | throw new Error((error as Error).message)
46 | }
47 | }
48 |
49 | export const dynamic = 'force-dynamic'
50 |
--------------------------------------------------------------------------------
/app/api/admin/goods/delete/route.ts:
--------------------------------------------------------------------------------
1 | import { ObjectId } from 'mongodb'
2 | import { NextResponse } from 'next/server'
3 | import { corsHeaders } from '@/constants/corsHeaders'
4 | import clientPromise from '@/lib/mongodb'
5 | import { getDbAndReqBody } from '@/lib/utils/api-routes'
6 |
7 | export async function GET(req: Request) {
8 | try {
9 | const { db } = await getDbAndReqBody(clientPromise, null)
10 | const url = new URL(req.url)
11 | const id = url.searchParams.get('id')
12 | const category = url.searchParams.get('category')
13 |
14 | await db
15 | .collection(category as string)
16 | .deleteOne({ _id: new ObjectId(id as string) })
17 |
18 | return NextResponse.json(
19 | {
20 | status: 204,
21 | },
22 | corsHeaders
23 | )
24 | } catch (error) {
25 | throw new Error((error as Error).message)
26 | }
27 | }
28 |
29 | export const dynamic = 'force-dynamic'
30 |
--------------------------------------------------------------------------------
/app/api/admin/goods/one/route.ts:
--------------------------------------------------------------------------------
1 | import { ObjectId } from 'mongodb'
2 | import { NextResponse } from 'next/server'
3 | import { corsHeaders } from '@/constants/corsHeaders'
4 | import clientPromise from '@/lib/mongodb'
5 | import { getDbAndReqBody } from '@/lib/utils/api-routes'
6 |
7 | export async function GET(req: Request) {
8 | try {
9 | const { db } = await getDbAndReqBody(clientPromise, null)
10 | const url = new URL(req.url)
11 | const id = url.searchParams.get('id')
12 | const category = url.searchParams.get('category')
13 | const isValidId = ObjectId.isValid(id as string)
14 |
15 | if (!isValidId) {
16 | return NextResponse.json(
17 | {
18 | message: 'Wrong product id',
19 | status: 404,
20 | },
21 | corsHeaders
22 | )
23 | }
24 |
25 | const productItem = await db
26 | .collection(category as string)
27 | .findOne({ _id: new ObjectId(id as string) })
28 |
29 | return NextResponse.json(
30 | {
31 | status: 200,
32 | productItem: {
33 | ...productItem,
34 | id: productItem?._id,
35 | images: productItem?.images.map((src: string) => ({
36 | url: src,
37 | desc: productItem.name,
38 | })),
39 | },
40 | },
41 | corsHeaders
42 | )
43 | } catch (error) {
44 | throw new Error((error as Error).message)
45 | }
46 | }
47 |
48 | export const dynamic = 'force-dynamic'
49 |
--------------------------------------------------------------------------------
/app/api/cart/add-many/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import { replaceProductsInCollection } from '@/lib/utils/api-routes'
3 |
4 | export async function POST(req: Request) {
5 | try {
6 | return replaceProductsInCollection(clientPromise, req, 'cart')
7 | } catch (error) {
8 | throw new Error((error as Error).message)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/api/cart/all/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import { getDataFromDBByCollection } from '@/lib/utils/api-routes'
3 |
4 | export async function GET(req: Request) {
5 | try {
6 | return getDataFromDBByCollection(clientPromise, req, 'cart')
7 | } catch (error) {
8 | throw new Error((error as Error).message)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/api/cart/count/route.ts:
--------------------------------------------------------------------------------
1 | import { ObjectId } from 'mongodb'
2 | import { NextResponse } from 'next/server'
3 | import clientPromise from '@/lib/mongodb'
4 | import { getAuthRouteData } from '@/lib/utils/api-routes'
5 |
6 | export async function PATCH(req: Request) {
7 | try {
8 | const { db, reqBody, validatedTokenResult } = await getAuthRouteData(
9 | clientPromise,
10 | req
11 | )
12 |
13 | if (validatedTokenResult.status !== 200) {
14 | return NextResponse.json(validatedTokenResult)
15 | }
16 |
17 | const id = req.url.split('id=')[1]
18 | const count = reqBody.count
19 |
20 | await db.collection('cart').updateOne(
21 | { _id: new ObjectId(id) },
22 | {
23 | $set: {
24 | count,
25 | },
26 | }
27 | )
28 |
29 | return NextResponse.json({ status: 204, id, count })
30 | } catch (error) {
31 | throw new Error((error as Error).message)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/api/cart/delete-many/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import clientPromise from '@/lib/mongodb'
3 | import { getAuthRouteData, parseJwt } from '@/lib/utils/api-routes'
4 |
5 | export async function DELETE(req: Request) {
6 | try {
7 | const { db, validatedTokenResult, token } = await getAuthRouteData(
8 | clientPromise,
9 | req,
10 | false
11 | )
12 |
13 | if (validatedTokenResult.status !== 200) {
14 | return NextResponse.json(validatedTokenResult)
15 | }
16 |
17 | const user = await db
18 | .collection('users')
19 | .findOne({ email: parseJwt(token as string).email })
20 |
21 | await db.collection('cart').deleteMany({ userId: user?._id })
22 |
23 | return NextResponse.json({ status: 204 })
24 | } catch (error) {
25 | throw new Error((error as Error).message)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/api/cart/delete/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import { deleteProduct } from '@/lib/utils/api-routes'
3 |
4 | export async function DELETE(req: Request) {
5 | try {
6 | return deleteProduct(clientPromise, req, req.url.split('id=')[1], 'cart')
7 | } catch (error) {
8 | throw new Error((error as Error).message)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/api/comparison/add-many/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import { replaceProductsInCollection } from '@/lib/utils/api-routes'
3 |
4 | export async function POST(req: Request) {
5 | try {
6 | return replaceProductsInCollection(clientPromise, req, 'comparison')
7 | } catch (error) {
8 | throw new Error((error as Error).message)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/api/comparison/all/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import { getDataFromDBByCollection } from '@/lib/utils/api-routes'
3 |
4 | export async function GET(req: Request) {
5 | try {
6 | return getDataFromDBByCollection(clientPromise, req, 'comparison')
7 | } catch (error) {
8 | throw new Error((error as Error).message)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/api/comparison/delete/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import { deleteProduct } from '@/lib/utils/api-routes'
3 |
4 | export async function DELETE(req: Request) {
5 | try {
6 | return deleteProduct(
7 | clientPromise,
8 | req,
9 | req.url.split('id=')[1],
10 | 'comparison'
11 | )
12 | } catch (error) {
13 | throw new Error((error as Error).message)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/api/favorites/add-many/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import { replaceProductsInCollection } from '@/lib/utils/api-routes'
3 |
4 | export async function POST(req: Request) {
5 | try {
6 | return replaceProductsInCollection(clientPromise, req, 'favorites')
7 | } catch (error) {
8 | throw new Error((error as Error).message)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/api/favorites/all/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import { getDataFromDBByCollection } from '@/lib/utils/api-routes'
3 |
4 | export async function GET(req: Request) {
5 | try {
6 | return getDataFromDBByCollection(clientPromise, req, 'favorites')
7 | } catch (error) {
8 | throw new Error((error as Error).message)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/api/favorites/delete/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import { deleteProduct } from '@/lib/utils/api-routes'
3 |
4 | export async function DELETE(req: Request) {
5 | try {
6 | return deleteProduct(
7 | clientPromise,
8 | req,
9 | req.url.split('id=')[1],
10 | 'favorites'
11 | )
12 | } catch (error) {
13 | throw new Error((error as Error).message)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/api/goods/bestsellers/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import clientPromise from '@/lib/mongodb'
3 | import {
4 | getDbAndReqBody,
5 | getNewAndBestsellerGoods,
6 | } from '@/lib/utils/api-routes'
7 |
8 | export async function GET() {
9 | const { db } = await getDbAndReqBody(clientPromise, null)
10 |
11 | return NextResponse.json(await getNewAndBestsellerGoods(db, 'isBestseller'))
12 | }
13 |
--------------------------------------------------------------------------------
/app/api/goods/new/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import clientPromise from '@/lib/mongodb'
3 | import {
4 | getDbAndReqBody,
5 | getNewAndBestsellerGoods,
6 | } from '@/lib/utils/api-routes'
7 |
8 | export async function GET() {
9 | const { db } = await getDbAndReqBody(clientPromise, null)
10 |
11 | return NextResponse.json(await getNewAndBestsellerGoods(db, 'isNew'))
12 | }
13 |
--------------------------------------------------------------------------------
/app/api/goods/one/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import { getDbAndReqBody } from '@/lib/utils/api-routes'
3 | import { ObjectId } from 'mongodb'
4 | import { NextResponse } from 'next/server'
5 |
6 | export async function POST(req: Request) {
7 | try {
8 | const { db, reqBody } = await getDbAndReqBody(clientPromise, req)
9 | const isValidId = ObjectId.isValid(reqBody.productId)
10 |
11 | if (!isValidId) {
12 | return NextResponse.json({
13 | message: 'Wrong product id',
14 | status: 404,
15 | })
16 | }
17 |
18 | const productItem = await db
19 | .collection(reqBody.category)
20 | .findOne({ _id: new ObjectId(reqBody.productId) })
21 |
22 | return NextResponse.json({ status: 200, productItem })
23 | } catch (error) {
24 | throw new Error((error as Error).message)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/api/payment/check/route.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { NextResponse } from 'next/server'
3 |
4 | export async function POST(req: Request) {
5 | try {
6 | const reqBody = await req.json()
7 |
8 | const { data } = await axios({
9 | method: 'get',
10 | url: `https://api.yookassa.ru/v3/payments/${reqBody.paymentId}`,
11 | auth: {
12 | username: '284434',
13 | password: 'test_qDOAK1qBsglEy7Pbf2ZkSq7-uWERPH-LNAwPyPNS8hc',
14 | },
15 | })
16 |
17 | return NextResponse.json({ result: data })
18 | } catch (error) {
19 | throw new Error((error as Error).message)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/api/payment/notify/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import { sendMail } from '@/service/mailService'
3 |
4 | export async function POST(req: Request) {
5 | try {
6 | const reqBody = await req.json()
7 |
8 | await sendMail('Rostelecom Shop', reqBody.email, reqBody.message)
9 |
10 | return NextResponse.json({ status: 200 })
11 | } catch (error) {
12 | throw new Error((error as Error).message)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/api/payment/route.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { NextResponse } from 'next/server'
3 | import clientPromise from '@/lib/mongodb'
4 | import { getAuthRouteData } from '@/lib/utils/api-routes'
5 |
6 | export async function POST(req: Request) {
7 | try {
8 | const { validatedTokenResult, reqBody } = await getAuthRouteData(
9 | clientPromise,
10 | req
11 | )
12 |
13 | if (validatedTokenResult.status !== 200) {
14 | return NextResponse.json(validatedTokenResult)
15 | }
16 |
17 | const { data } = await axios({
18 | method: 'post',
19 | url: 'https://api.yookassa.ru/v3/payments',
20 | headers: {
21 | 'Content-Type': 'application/json',
22 | 'Idempotence-Key': Date.now(),
23 | },
24 | auth: {
25 | username: '284434',
26 | password: 'test_qDOAK1qBsglEy7Pbf2ZkSq7-uWERPH-LNAwPyPNS8hc',
27 | },
28 | data: {
29 | amount: {
30 | value: reqBody.amount,
31 | currency: 'RUB',
32 | },
33 | confirmation: {
34 | type: 'redirect',
35 | return_url: 'https://rostelecom-shop.vercel.app/payment-success',
36 | },
37 | capture: true,
38 | description: reqBody.description,
39 | metadata: reqBody.metadata,
40 | },
41 | })
42 |
43 | return NextResponse.json({ result: data })
44 | } catch (error) {
45 | throw new Error((error as Error).message)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/api/users/delete/route.ts:
--------------------------------------------------------------------------------
1 | import { ObjectId } from 'mongodb'
2 | import { NextResponse } from 'next/server'
3 | import clientPromise from '@/lib/mongodb'
4 | import { getAuthRouteData } from '@/lib/utils/api-routes'
5 |
6 | export async function DELETE(req: Request) {
7 | try {
8 | const { db, validatedTokenResult } = await getAuthRouteData(
9 | clientPromise,
10 | req,
11 | false
12 | )
13 |
14 | if (validatedTokenResult.status !== 200) {
15 | return NextResponse.json(validatedTokenResult)
16 | }
17 |
18 | const id = req.url.split('id=')[1]
19 |
20 | await db.collection('users').deleteOne({ _id: new ObjectId(id) })
21 |
22 | return NextResponse.json({
23 | status: 204,
24 | id,
25 | })
26 | } catch (error) {
27 | throw new Error((error as Error).message)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/api/users/edit/email/route.ts:
--------------------------------------------------------------------------------
1 | import { ObjectId } from 'mongodb'
2 | import { NextResponse } from 'next/server'
3 | import clientPromise from '@/lib/mongodb'
4 | import {
5 | generateTokens,
6 | getAuthRouteData,
7 | parseJwt,
8 | } from '@/lib/utils/api-routes'
9 |
10 | export async function PATCH(req: Request) {
11 | try {
12 | const { db, validatedTokenResult, reqBody, token } = await getAuthRouteData(
13 | clientPromise,
14 | req
15 | )
16 |
17 | if (validatedTokenResult.status !== 200) {
18 | return NextResponse.json(validatedTokenResult)
19 | }
20 |
21 | if (!reqBody.email) {
22 | return NextResponse.json({
23 | error: { message: 'email field is required' },
24 | status: 400,
25 | })
26 | }
27 |
28 | const user = await db
29 | .collection('users')
30 | .findOne({ email: parseJwt(token as string).email })
31 |
32 | await db.collection('users').updateOne(
33 | {
34 | _id: new ObjectId(user?._id),
35 | },
36 | {
37 | $set: {
38 | email: reqBody.email,
39 | },
40 | }
41 | )
42 |
43 | const tokens = generateTokens(user?.name, reqBody.email)
44 |
45 | return NextResponse.json({ status: 200, email: reqBody.email, tokens })
46 | } catch (error) {
47 | throw new Error((error as Error).message)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/api/users/edit/name/route.ts:
--------------------------------------------------------------------------------
1 | import { ObjectId } from 'mongodb'
2 | import { NextResponse } from 'next/server'
3 | import clientPromise from '@/lib/mongodb'
4 | import {
5 | generateTokens,
6 | getAuthRouteData,
7 | parseJwt,
8 | } from '@/lib/utils/api-routes'
9 |
10 | export async function PATCH(req: Request) {
11 | try {
12 | const { db, validatedTokenResult, reqBody, token } = await getAuthRouteData(
13 | clientPromise,
14 | req
15 | )
16 |
17 | if (validatedTokenResult.status !== 200) {
18 | return NextResponse.json(validatedTokenResult)
19 | }
20 |
21 | if (!reqBody.name) {
22 | return NextResponse.json({
23 | message: 'name field is required',
24 | status: 400,
25 | })
26 | }
27 |
28 | const user = await db
29 | .collection('users')
30 | .findOne({ email: parseJwt(token as string).email })
31 |
32 | await db.collection('users').updateOne(
33 | {
34 | _id: new ObjectId(user?._id),
35 | },
36 | {
37 | $set: {
38 | name: reqBody.name,
39 | },
40 | }
41 | )
42 |
43 | const tokens = generateTokens(reqBody.name, parseJwt(token as string).email)
44 | return NextResponse.json({ status: 200, name: reqBody.name, tokens })
45 | } catch (error) {
46 | throw new Error((error as Error).message)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/api/users/email/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import { sendMail } from '@/service/mailService'
3 |
4 | export async function POST(req: Request) {
5 | const res = await req.json()
6 | try {
7 | await sendMail(
8 | 'Rostelecom',
9 | res.email,
10 | `Ваши данные для входа - пароль: ${res.password}, логин: ${res.email}`
11 | )
12 | return NextResponse.json({ message: 'Success' })
13 | } catch (err) {
14 | return NextResponse.json({ message: (err as Error).message })
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/api/users/login-check/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import clientPromise from '@/lib/mongodb'
3 | import {
4 | getAuthRouteData,
5 | findUserByEmail,
6 | parseJwt,
7 | } from '@/lib/utils/api-routes'
8 | import { IUser } from '@/types/user'
9 |
10 | export async function GET(req: Request) {
11 | try {
12 | const { db, validatedTokenResult, token } = await getAuthRouteData(
13 | clientPromise,
14 | req,
15 | false
16 | )
17 |
18 | if (validatedTokenResult.status !== 200) {
19 | return NextResponse.json(validatedTokenResult)
20 | }
21 |
22 | const user = (await findUserByEmail(
23 | db,
24 | parseJwt(token as string).email
25 | )) as unknown as IUser
26 |
27 | return NextResponse.json({
28 | status: 200,
29 | message: 'token is valid',
30 | user: {
31 | email: user.email,
32 | name: user.name,
33 | _id: user?._id,
34 | image: user.image,
35 | },
36 | })
37 | } catch (error) {
38 | throw new Error((error as Error).message)
39 | }
40 | }
41 |
42 | export const dynamic = 'force-dynamic'
43 |
--------------------------------------------------------------------------------
/app/api/users/login/route.ts:
--------------------------------------------------------------------------------
1 | import bcrypt from 'bcryptjs'
2 | import { NextResponse } from 'next/server'
3 | import clientPromise from '@/lib/mongodb'
4 | import {
5 | findUserByEmail,
6 | generateTokens,
7 | getDbAndReqBody,
8 | } from '@/lib/utils/api-routes'
9 |
10 | export async function POST(req: Request) {
11 | const { db, reqBody } = await getDbAndReqBody(clientPromise, req)
12 | const user = await findUserByEmail(db, reqBody.email)
13 |
14 | if (!user) {
15 | return NextResponse.json({
16 | warningMessage: 'Пользователя не существует',
17 | })
18 | }
19 |
20 | if (!bcrypt.compareSync(reqBody.password, user.password)) {
21 | return NextResponse.json({
22 | warningMessage: 'Неправильный логин или пароль!',
23 | })
24 | }
25 |
26 | const tokens = generateTokens(user.name, reqBody.email)
27 |
28 | return NextResponse.json(tokens)
29 | }
30 |
--------------------------------------------------------------------------------
/app/api/users/oauth/route.ts:
--------------------------------------------------------------------------------
1 | import clientPromise from '@/lib/mongodb'
2 | import {
3 | getDbAndReqBody,
4 | findUserByEmail,
5 | createUserAndGenerateTokens,
6 | generateTokens,
7 | } from '@/lib/utils/api-routes'
8 | import { NextResponse } from 'next/server'
9 |
10 | export async function POST(req: Request) {
11 | const { db, reqBody } = await getDbAndReqBody(clientPromise, req)
12 | const user = await findUserByEmail(db, reqBody.email)
13 |
14 | if (!user) {
15 | const tokens = await createUserAndGenerateTokens(db, reqBody)
16 |
17 | return NextResponse.json(tokens)
18 | }
19 |
20 | const tokens = generateTokens(user.name, reqBody.email)
21 |
22 | return NextResponse.json(tokens)
23 | }
24 |
--------------------------------------------------------------------------------
/app/api/users/password-restore/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import bcrypt from 'bcryptjs'
3 | import { ObjectId } from 'mongodb'
4 | import clientPromise from '@/lib/mongodb'
5 | import { getDbAndReqBody } from '@/lib/utils/api-routes'
6 |
7 | export async function POST(req: Request) {
8 | try {
9 | const { db, reqBody } = await getDbAndReqBody(clientPromise, req)
10 | const user = await db.collection('users').findOne({ email: reqBody.email })
11 | const salt = bcrypt.genSaltSync(10)
12 | const hash = bcrypt.hashSync(reqBody.password, salt)
13 |
14 | await db.collection('users').updateOne(
15 | {
16 | _id: new ObjectId(user?._id),
17 | },
18 | {
19 | $set: {
20 | password: hash,
21 | },
22 | }
23 | )
24 |
25 | return NextResponse.json({ status: 200 })
26 | } catch (error) {
27 | throw new Error((error as Error).message)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/api/users/password-restore/verify-email/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import clientPromise from '@/lib/mongodb'
3 | import { findUserByEmail, getDbAndReqBody } from '@/lib/utils/api-routes'
4 | import { sendMail } from '@/service/mailService'
5 |
6 | export async function POST(req: Request) {
7 | try {
8 | const { db, reqBody } = await getDbAndReqBody(clientPromise, req)
9 | const user = await findUserByEmail(db, reqBody.email)
10 |
11 | if (!user) {
12 | return NextResponse.json({
13 | error: { message: 'Пользователь с таким email не найден' },
14 | status: 400,
15 | })
16 | }
17 |
18 | const code = Math.floor(100000 + Math.random() * 900000)
19 |
20 | await sendMail(
21 | 'Rostelecom',
22 | reqBody.email,
23 | `Ваш код подтверждения для восстановления пароля: ${code}`
24 | )
25 |
26 | const { insertedId } = await db.collection('codes').insertOne({
27 | code,
28 | })
29 |
30 | return NextResponse.json({ status: 200, codeId: insertedId })
31 | } catch (error) {
32 | throw new Error((error as Error).message)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/api/users/signup/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import clientPromise from '@/lib/mongodb'
3 | import {
4 | createUserAndGenerateTokens,
5 | findUserByEmail,
6 | getDbAndReqBody,
7 | } from '@/lib/utils/api-routes'
8 |
9 | export async function POST(req: Request) {
10 | try {
11 | const { db, reqBody } = await getDbAndReqBody(clientPromise, req)
12 | const user = await findUserByEmail(db, reqBody.email)
13 |
14 | if (user) {
15 | return NextResponse.json({
16 | warningMessage: 'Пользователь уже существует',
17 | })
18 | }
19 |
20 | const tokens = await createUserAndGenerateTokens(db, reqBody)
21 |
22 | return NextResponse.json(tokens)
23 | } catch (error) {
24 | throw new Error((error as Error).message)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/api/users/verify-code/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import clientPromise from '@/lib/mongodb'
3 | import { getDbAndReqBody } from '@/lib/utils/api-routes'
4 | import { ObjectId } from 'mongodb'
5 |
6 | export async function POST(req: Request) {
7 | try {
8 | const { db, reqBody } = await getDbAndReqBody(clientPromise, req)
9 |
10 | const codeData = await db
11 | .collection('codes')
12 | .findOne({ _id: new ObjectId(reqBody.codeId) })
13 |
14 | if (!codeData) {
15 | return NextResponse.json({
16 | status: 400,
17 | error: { message: 'Неправильный айди кода!' },
18 | })
19 | }
20 |
21 | if (codeData.code === reqBody.code) {
22 | await db
23 | .collection('codes')
24 | .deleteOne({ _id: new ObjectId(reqBody.codeId) })
25 |
26 | return NextResponse.json({
27 | status: 200,
28 | result: true,
29 | newEmail: codeData.newEmail,
30 | })
31 | }
32 |
33 | return NextResponse.json({
34 | status: 400,
35 | result: false,
36 | error: { message: 'Неправильный код!' },
37 | })
38 | } catch (error) {
39 | throw new Error((error as Error).message)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/cart/page.tsx:
--------------------------------------------------------------------------------
1 | import CartPage from '@/components/templates/CartPage/CartPage'
2 |
3 | export default function Cart() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/catalog/[category]/[productId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from 'next/navigation'
2 | import { productCategories } from '@/constants/product'
3 | import ProductPage from '@/components/templates/ProductPage/ProductPage'
4 |
5 | export default function Product({
6 | params,
7 | }: {
8 | params: { productId: string; category: string }
9 | }) {
10 | if (!productCategories.includes(params.category)) {
11 | notFound()
12 | }
13 |
14 | return
15 | }
16 |
--------------------------------------------------------------------------------
/app/catalog/[category]/page.tsx:
--------------------------------------------------------------------------------
1 | import ProductsPage from '@/components/templates/ProductsPage/ProductsPage'
2 | import { productCategories } from '@/constants/product'
3 | import { notFound } from 'next/navigation'
4 |
5 | export default function Category({ params }: { params: { category: string } }) {
6 | if (!productCategories.includes(params.category)) {
7 | notFound()
8 | }
9 |
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/app/catalog/layout.tsx:
--------------------------------------------------------------------------------
1 | import CatalogLayout from '../../components/layouts/CatalogLayout'
2 |
3 | export const metadata = {
4 | title: 'Ростелеком | Каталог',
5 | }
6 |
7 | export default function ComparisonRootLayout({
8 | children,
9 | }: {
10 | children: React.ReactNode
11 | }) {
12 | return {children}
13 | }
14 |
--------------------------------------------------------------------------------
/app/catalog/page.tsx:
--------------------------------------------------------------------------------
1 | import ProductsPage from '@/components/templates/ProductsPage/ProductsPage'
2 | import { SearchParams } from '@/types/catalog'
3 |
4 | export default function Catalog({
5 | searchParams,
6 | }: {
7 | searchParams?: SearchParams
8 | }) {
9 | return
10 | }
11 |
--------------------------------------------------------------------------------
/app/collection-products/page.tsx:
--------------------------------------------------------------------------------
1 | import CollectionProductsPage from '@/components/templates/CollectionProductsPage/CollectionProductsPage'
2 |
3 | export default function CollectionProducts() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/comparison/[type]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import ComparisonList from '@/components/modules/Comparison/ComparisonList'
3 | import { productTypes } from '@/constants/product'
4 | import { useComparisonItems } from '@/hooks/useComparisonItems'
5 | import { notFound } from 'next/navigation'
6 |
7 | export default function ComparisonType({
8 | params,
9 | }: {
10 | params: { type: string }
11 | }) {
12 | if (!productTypes.includes(params.type)) {
13 | notFound()
14 | }
15 |
16 | const { items } = useComparisonItems(params.type)
17 |
18 | return
19 | }
20 |
--------------------------------------------------------------------------------
/app/comparison/layout.tsx:
--------------------------------------------------------------------------------
1 | import ComparisonLayout from '@/components/layouts/ComparisonLayout'
2 |
3 | export const metadata = {
4 | title: 'Ростелеком | Сравнение товаров',
5 | }
6 |
7 | export default function ComparisonRootLayout({
8 | children,
9 | }: {
10 | children: React.ReactNode
11 | }) {
12 | return {children}
13 | }
14 |
--------------------------------------------------------------------------------
/app/comparison/page.tsx:
--------------------------------------------------------------------------------
1 | import ComparisonPage from '@/components/templates/ComparisonPage/ComparisonPage'
2 |
3 | export default function Comparison() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/favorites/page.tsx:
--------------------------------------------------------------------------------
1 | import FavoritesPage from '@/components/templates/FavoritesPage/FavoritesPage'
2 |
3 | export default function Favorites() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/globalStyles/breadcrumbs.css:
--------------------------------------------------------------------------------
1 | .breadcrumbs__container {
2 | overflow-x: auto;
3 | }
4 |
5 | .breadcrumbs {
6 | margin-top: 24px !important;
7 | margin-bottom: 40px !important;
8 | display: flex;
9 | align-items: center;
10 | min-width: max-content;
11 | }
12 |
13 | .breadcrumbs__item:not(:last-child) {
14 | margin-right: 16px;
15 | }
16 |
17 | .breadcrumbs__item__link {
18 | display: inline-block;
19 | min-width: 10px;
20 | min-height: 11px;
21 | position: relative;
22 | color: rgba(255, 255, 255, 0.90);
23 | font-size: 13px;
24 | font-weight: 500;
25 | text-decoration: none;
26 | transition: color .2s ease-in-out;
27 | }
28 |
29 | .breadcrumbs__item__link::before {
30 | content: '';
31 | position: absolute;
32 | left: -9px;
33 | top: 7px;
34 | width: 3px;
35 | height: 3px;
36 | border-radius: 50%;
37 | background-color: white;
38 | }
39 |
40 | .first-crumb::before {
41 | content: none;
42 | }
43 |
44 | .breadcrumbs__item__link:hover {
45 | color: #9466FF;
46 | transition: color .2s ease-in-out;
47 | }
48 |
49 | .last-crumb {
50 | color: rgba(255, 255, 255, 0.40);
51 | }
52 |
53 | .last-crumb:hover {
54 | color: rgba(255, 255, 255, 0.40);
55 | }
56 |
--------------------------------------------------------------------------------
/app/globalStyles/mobile-navbar.css:
--------------------------------------------------------------------------------
1 | .mobile-navbar {
2 | position: fixed;
3 | bottom: 0;
4 | left: 0;
5 | right: 0;
6 | padding: 20px 24px;
7 | background-color: #1D2533;
8 | display: flex;
9 | align-items: flex-end;
10 | justify-content: space-between;
11 | z-index: 100;
12 | }
13 |
14 | .mobile-navbar__btn {
15 | min-width: max-content;
16 | height: 52px;
17 | position: relative;
18 | display: flex;
19 | align-items: end;
20 | text-decoration: none;
21 | color: #E8E9EA;
22 | font-size: 14px;
23 | background-repeat: no-repeat;
24 | background-position: top 5px center;
25 | }
26 |
27 | .mobile-navbar__btn:first-child {
28 | background-image: url(/img/home.svg);
29 | }
30 |
31 | .mobile-navbar__btn:nth-child(2) {
32 | background-image: url(/img/catalog.svg);
33 | }
34 |
35 | .mobile-navbar__btn:nth-child(3) {
36 | background-image: url(/img/favorites.svg);
37 | }
38 |
39 | .mobile-navbar__btn:nth-child(4) {
40 | background-image: url(/img/cart.svg);
41 | }
42 |
43 | .mobile-navbar__btn:last-child {
44 | background-image: url(/img/more.svg);
45 | background-position: top 13px center;
46 | }
47 |
48 | @media (max-width: 450px) {
49 | .mobile-navbar {
50 | padding: 10px 15px;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/icon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata, Viewport } from 'next'
2 | import PagesLayout from '@/components/layouts/PagesLayout'
3 | import './globalStyles/normalize.css'
4 | import './globalStyles/globals.css'
5 | import './globalStyles/header.css'
6 | import './globalStyles/menu.css'
7 | import './globalStyles/mobile-navbar.css'
8 | import './globalStyles/catalog-menu.css'
9 | import './globalStyles/search-modal.css'
10 | import './globalStyles/cart-popup.css'
11 | import './globalStyles/footer.css'
12 | import './globalStyles/slick-theme.css'
13 | import './globalStyles/slick.css'
14 | import './globalStyles/auth-popup.css'
15 | import './globalStyles/header-profile.css'
16 | import './globalStyles/cookie-popup.css'
17 | import './globalStyles/breadcrumbs.css'
18 | import './globalStyles/map.css'
19 |
20 | export const metadata: Metadata = {
21 | title: 'Rostelecom',
22 | description: 'Rostelecom магазин одежды, аксесуаров, концелярии и сувениров',
23 | }
24 |
25 | export const viewport: Viewport = {
26 | themeColor: 'white',
27 | }
28 |
29 | export default function RootLayout({
30 | children,
31 | }: {
32 | children: React.ReactNode
33 | }) {
34 | return {children}
35 | }
36 |
--------------------------------------------------------------------------------
/app/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/app/logo.png
--------------------------------------------------------------------------------
/app/manifest.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from 'next'
2 |
3 | export default function manifest(): MetadataRoute.Manifest {
4 | return {
5 | name: 'Rostelecom Application',
6 | short_name: 'Rostelecom App',
7 | description:
8 | 'Rostelecom магазин одежды, аксесуаров, концелярии и сувениров',
9 | start_url: '/',
10 | background_color: '#fff',
11 | theme_color: '#fff',
12 | display: 'standalone',
13 | icons: [
14 | {
15 | src: '/img/icon.svg',
16 | sizes: '196x196 512x512 144x144 192x192 128x128 120x120 180x180',
17 | type: 'image/svg',
18 | purpose: 'maskable',
19 | },
20 | {
21 | src: '/img/logo.png',
22 | sizes: '196x196 512x512 144x144 192x192 128x128 120x120 180x180',
23 | type: 'image/png',
24 | purpose: 'any',
25 | },
26 | ],
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import EmptyPageContent from '@/components/modules/EmptyPageContent/EmptyPageContent'
3 | import { useLang } from '@/hooks/useLang'
4 | import styles from '@/styles/not-found/index.module.scss'
5 |
6 | const NotFound = () => {
7 | const { lang, translations } = useLang()
8 |
9 | return (
10 |
11 |
25 |
26 | )
27 | }
28 |
29 | export default NotFound
30 |
--------------------------------------------------------------------------------
/app/order/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import OrderPage from '@/components/templates/OrderPage/OrderPage'
3 |
4 | export default function Order() {
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import MainPage from '@/components/templates/MainPage/MainPage'
2 |
3 | export default function Home() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/password-restore/page.tsx:
--------------------------------------------------------------------------------
1 | import PasswordRestorePage from '@/components/templates/PasswordRestorePage/PasswordRestorePage'
2 |
3 | export default function PasswordRestore() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/personal-data-policy/page.tsx:
--------------------------------------------------------------------------------
1 | import PersonalDataPolicyPage from '@/components/templates/PersonalDataPolicyPage/PersonalDataPolicyPage'
2 |
3 | export default function PersonalDataPolicy() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/privacy-policy/page.tsx:
--------------------------------------------------------------------------------
1 | import PrivacyPolicyPage from '@/components/templates/PrivacyPolicyPage/PrivacyPolicyPage'
2 |
3 | export default function PersonalDataPolicy() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/profile/page.tsx:
--------------------------------------------------------------------------------
1 | import ProfilePage from '@/components/templates/ProfilePage/ProfilePage'
2 |
3 | export default function Profile() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/watched-products/page.tsx:
--------------------------------------------------------------------------------
1 | import WatchedProductsPage from '@/components/templates/WatchedProductsPage/WatchedProductsPage'
2 |
3 | export default function WatchedProducts() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/components/elements/AddToCartIcon/AddToCartIcon.tsx:
--------------------------------------------------------------------------------
1 | import { faSpinner } from '@fortawesome/free-solid-svg-icons'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { IAddToCartIconProps } from '@/types/elements'
4 |
5 | const AddToCartIcon = ({
6 | isProductInCart,
7 | addedClassName,
8 | className,
9 | addToCartSpinner,
10 | callback,
11 | }: IAddToCartIconProps) => (
12 | <>
13 | {isProductInCart ? (
14 |
15 | ) : (
16 |
23 | )}
24 | >
25 | )
26 |
27 | export default AddToCartIcon
28 |
--------------------------------------------------------------------------------
/components/elements/AllLink/AllLink.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import { useLang } from '@/hooks/useLang'
4 | import styles from '@/styles/main-page/index.module.scss'
5 |
6 | const AllLink = ({ link }: { link?: string }) => {
7 | const { lang, translations } = useLang()
8 |
9 | return (
10 |
11 |
12 | {translations[lang].common.all_link}
13 |
14 | )
15 | }
16 |
17 | export default AllLink
18 |
--------------------------------------------------------------------------------
/components/elements/DeleteCartItemBtn/DeleteCartItemBtn.tsx:
--------------------------------------------------------------------------------
1 | import { faSpinner } from '@fortawesome/free-solid-svg-icons'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { IDeleteCartItemBtnProps } from '@/types/cart'
4 |
5 | const DeleteItemBtn = ({
6 | btnDisabled,
7 | callback,
8 | className,
9 | }: IDeleteCartItemBtnProps) => (
10 |
21 | )
22 |
23 | export default DeleteItemBtn
24 |
--------------------------------------------------------------------------------
/components/elements/HeadingWithCount/HeadingWithCount.tsx:
--------------------------------------------------------------------------------
1 | import { faSpinner } from '@fortawesome/free-solid-svg-icons'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { useLang } from '@/hooks/useLang'
4 | import { showCountMessage } from '@/lib/utils/common'
5 | import { IHeadingWithCountProps } from '@/types/elements'
6 | import styles from '@/styles/heading-with-count/index.module.scss'
7 |
8 | const HeadingWithCount = ({
9 | count,
10 | title,
11 | spinner,
12 | }: IHeadingWithCountProps) => {
13 | const { lang } = useLang()
14 |
15 | return (
16 |
17 | {title}
18 |
19 | {spinner ? : count}{' '}
20 | {showCountMessage(`${count}`, lang)}
21 |
22 |
23 | )
24 | }
25 |
26 | export default HeadingWithCount
27 |
--------------------------------------------------------------------------------
/components/elements/Logo/Logo.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | const Logo = () => (
4 |
5 |
6 |
7 | )
8 |
9 | export default Logo
10 |
--------------------------------------------------------------------------------
/components/elements/NameErrorMessage/NameErrorMessage.tsx:
--------------------------------------------------------------------------------
1 | import { useLang } from '@/hooks/useLang'
2 | import { INameErrorMessageProps } from '@/types/authPopup'
3 |
4 | const NameErrorMessage = ({
5 | errors,
6 | className,
7 | fieldName,
8 | }: INameErrorMessageProps) => {
9 | const { lang, translations } = useLang()
10 |
11 | return (
12 | <>
13 | {errors[fieldName] && (
14 | {errors[fieldName]?.message}
15 | )}
16 | {errors[fieldName] && errors[fieldName]?.type === 'minLength' && (
17 | {translations[lang].validation.min_2}
18 | )}
19 | {errors[fieldName] && errors[fieldName]?.type === 'maxLength' && (
20 |
21 | {translations[lang].validation.max_15}
22 |
23 | )}
24 | >
25 | )
26 | }
27 |
28 | export default NameErrorMessage
29 |
--------------------------------------------------------------------------------
/components/elements/ProductAvailable/ProductAvailable.tsx:
--------------------------------------------------------------------------------
1 | import { IProductAvailableProps } from '@/types/elements'
2 | import { useLang } from '@/hooks/useLang'
3 | import styles from '@/styles/product-list-item/index.module.scss'
4 |
5 | const ProductAvailable = ({ vendorCode, inStock }: IProductAvailableProps) => {
6 | const isInStock = +inStock > 0
7 | const { lang, translations } = useLang()
8 |
9 | return (
10 |
11 |
16 | {isInStock
17 | ? translations[lang].product.available
18 | : translations[lang].product.not_available}
19 |
20 |
21 | {translations[lang].product.vendor_code}
22 | .: {vendorCode}
23 |
24 |
25 | )
26 | }
27 |
28 | export default ProductAvailable
29 |
--------------------------------------------------------------------------------
/components/elements/ProductSubtitle/ProductSubtitle.tsx:
--------------------------------------------------------------------------------
1 | import { useLang } from '@/hooks/useLang'
2 | import { IProductSubtitleProps } from '@/types/elements'
3 |
4 | const ProductSubtitle = ({
5 | subtitleClassName,
6 | subtitleRectClassName,
7 | }: IProductSubtitleProps) => {
8 | const { lang, translations } = useLang()
9 | const descriptionSlicePosition = lang === 'ru' ? 5 : 2
10 |
11 | return (
12 |
13 |
14 |
15 | {translations[lang].main_page.hero_description.slice(
16 | 0,
17 | descriptionSlicePosition
18 | )}
19 |
20 |
21 |
22 | {translations[lang].main_page.hero_description.slice(
23 | descriptionSlicePosition
24 | )}
25 |
26 |
27 | )
28 | }
29 |
30 | export default ProductSubtitle
31 |
--------------------------------------------------------------------------------
/components/elements/QuickViewModalSliderArrow/QuickViewModalSliderArrow.tsx:
--------------------------------------------------------------------------------
1 | import { IQuickViewModalSliderArrowProps } from '@/types/elements'
2 | import styles from '@/styles/quick-view-modal/index.module.scss'
3 |
4 | const QuickViewModalSliderArrow = (props: IQuickViewModalSliderArrowProps) => (
5 |
9 | )
10 |
11 | export default QuickViewModalSliderArrow
12 |
--------------------------------------------------------------------------------
/components/elements/Skeleton/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 | import { basePropsForMotion } from '@/constants/motion'
3 | import { ISkeletonProps } from '@/types/elements'
4 |
5 | const Skeleton = ({ styles, count = 4 }: ISkeletonProps) => (
6 |
10 | {Array.from(new Array(count)).map((_, i) => (
11 |
12 |
13 |
14 | ))}
15 |
16 | )
17 |
18 | export default Skeleton
19 |
--------------------------------------------------------------------------------
/components/elements/Tooltip/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import styles from '@/styles/tooltip/index.module.scss'
2 |
3 | const Tooltip = ({ text }: { text: string }) => (
4 |
5 | {text}
6 |
7 | )
8 |
9 | export default Tooltip
10 |
--------------------------------------------------------------------------------
/components/hocs/withClickOutside.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ForwardRefExoticComponent, RefAttributes } from 'react'
3 | import { IWrappedComponentProps } from '@/types/hocs'
4 | import { useClickOutside } from '@/hooks/useClickOutside'
5 |
6 | export function withClickOutside(
7 | WrappedComponent: ForwardRefExoticComponent<
8 | IWrappedComponentProps & RefAttributes
9 | >
10 | ) {
11 | const Component = () => {
12 | const { open, setOpen, ref } = useClickOutside()
13 |
14 | return
15 | }
16 |
17 | return Component
18 | }
19 |
--------------------------------------------------------------------------------
/components/layouts/CatalogLayout.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useBreadcrumbs } from '@/hooks/useBreadcrumbs'
3 | import Breadcrumbs from '../modules/Breadcrumbs/Breadcrumbs'
4 | import styles from '@/styles/catalog/index.module.scss'
5 |
6 | const CatalogLayout = ({ children }: { children: React.ReactNode }) => {
7 | const { getDefaultTextGenerator, getTextGenerator } =
8 | useBreadcrumbs('catalog')
9 |
10 | return (
11 |
12 |
16 |
19 |
20 | )
21 | }
22 |
23 | export default CatalogLayout
24 |
--------------------------------------------------------------------------------
/components/modules/Accordion/Accordion.tsx:
--------------------------------------------------------------------------------
1 | import { IAccordionProps } from '@/types/modules'
2 | import { AnimatePresence, motion } from 'framer-motion'
3 | import React, { useState } from 'react'
4 |
5 | const Accordion = ({
6 | children,
7 | title,
8 | titleClass,
9 | rotateIconClass,
10 | }: IAccordionProps) => {
11 | const [expanded, setExpanded] = useState(false)
12 |
13 | const toggleAccordion = () => setExpanded(!expanded)
14 |
15 | return (
16 | <>
17 |
24 | {title}
25 |
26 |
27 | {expanded && (
28 |
40 | {children}
41 |
42 | )}
43 |
44 | >
45 | )
46 | }
47 |
48 | export default Accordion
49 |
--------------------------------------------------------------------------------
/components/modules/AuthPopup/AuthPopup.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import AuthPopupRegistration from './AuthPopupRegistration'
3 | import AuthPopupLogin from './AuthPopupLogin'
4 |
5 | const AuthPopup = () => {
6 | const [isAuthSwitched, setIsAuthSwitched] = useState(false)
7 | const [isSignInActive, setIsSignInActive] = useState(false)
8 | const [isSignupActive, setIsSignupActive] = useState(true)
9 |
10 | const toggleAuth = () => {
11 | setIsAuthSwitched(!isAuthSwitched)
12 | setIsSignInActive(!isSignInActive)
13 | setIsSignupActive(!isSignupActive)
14 | }
15 |
16 | return (
17 |
37 | )
38 | }
39 |
40 | export default AuthPopup
41 |
--------------------------------------------------------------------------------
/components/modules/AuthPopup/AuthPopupClose.tsx:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { closeAuthPopupWhenSomeModalOpened } from '@/lib/utils/common'
3 | import { $showQuickViewModal, $showSizeTable } from '@/context/modals/state'
4 |
5 | const AuthPopupClose = () => {
6 | const showQuickViewModal = useUnit($showQuickViewModal)
7 | const showSizeTable = useUnit($showSizeTable)
8 |
9 | const closePopup = () =>
10 | closeAuthPopupWhenSomeModalOpened(showQuickViewModal, showSizeTable)
11 |
12 | return
13 | }
14 |
15 | export default AuthPopupClose
16 |
--------------------------------------------------------------------------------
/components/modules/AuthPopup/AuthPopupSocials.tsx:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
2 | import {
3 | faGithub,
4 | faGoogle,
5 | faVk,
6 | faYandex,
7 | } from '@fortawesome/free-brands-svg-icons'
8 |
9 | const AuthPopupSocials = ({
10 | handleSignupWithOAuth,
11 | }: {
12 | handleSignupWithOAuth: VoidFunction
13 | }) => (
14 |
15 |
21 |
27 |
33 |
39 |
40 | )
41 |
42 | export default AuthPopupSocials
43 |
--------------------------------------------------------------------------------
/components/modules/AuthPopup/EmailInput.tsx:
--------------------------------------------------------------------------------
1 | import { useLang } from '@/hooks/useLang'
2 | import { emailValidationRules } from '@/lib/utils/auth'
3 | import { IAuthInput } from '@/types/authPopup'
4 | import styles from '@/styles/auth-popup/index.module.scss'
5 |
6 | const EmailInput = ({ register, errors }: IAuthInput) => {
7 | const { lang, translations } = useLang()
8 |
9 | return (
10 |
11 |
23 | {errors.email && (
24 | {errors.email?.message}
25 | )}
26 |
27 | )
28 | }
29 |
30 | export default EmailInput
31 |
--------------------------------------------------------------------------------
/components/modules/AuthPopup/NameInput.tsx:
--------------------------------------------------------------------------------
1 | import NameErrorMessage from '@/components/elements/NameErrorMessage/NameErrorMessage'
2 | import { useLang } from '@/hooks/useLang'
3 | import { nameValidationRules } from '@/lib/utils/auth'
4 | import { IAuthInput } from '@/types/authPopup'
5 | import styles from '@/styles/auth-popup/index.module.scss'
6 |
7 | const NameInput = ({ register, errors }: IAuthInput) => {
8 | const { lang, translations } = useLang()
9 |
10 | return (
11 |
12 |
24 |
29 |
30 | )
31 | }
32 |
33 | export default NameInput
34 |
--------------------------------------------------------------------------------
/components/modules/AuthPopup/PasswordInput.tsx:
--------------------------------------------------------------------------------
1 | import { useLang } from '@/hooks/useLang'
2 | import { IAuthInput } from '@/types/authPopup'
3 | import styles from '@/styles/auth-popup/index.module.scss'
4 |
5 | const PasswordInput = ({ register, errors }: IAuthInput) => {
6 | const { lang, translations } = useLang()
7 |
8 | return (
9 |
10 |
20 | {errors.password && (
21 | {errors.password?.message}
22 | )}
23 | {errors.password && errors.password.type === 'minLength' && (
24 |
25 | {translations[lang].validation.min_4}
26 |
27 | )}
28 | {errors.password && errors.password.type === 'maxLength' && (
29 |
30 | {translations[lang].validation.max_20}
31 |
32 | )}
33 |
34 | )
35 | }
36 | export default PasswordInput
37 |
--------------------------------------------------------------------------------
/components/modules/Breadcrumbs/Crumb.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { ICrumbProps } from '@/types/modules'
3 |
4 | const Crumb = ({ text: defaultText, href, last = false }: ICrumbProps) =>
5 | last ? (
6 |
7 | {defaultText}
8 |
9 | ) : (
10 |
11 | {defaultText}
12 |
13 | )
14 | export default Crumb
15 |
--------------------------------------------------------------------------------
/components/modules/CartPage/CartList.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatePresence, motion } from 'framer-motion'
2 | import { basePropsForMotion } from '@/constants/motion'
3 | import CartListItem from './CartListItem'
4 | import { useGoodsByAuth } from '@/hooks/useGoodsByAuth'
5 | import { $cart, $cartFromLs } from '@/context/cart/state'
6 | import styles from '@/styles/cart-page/index.module.scss'
7 |
8 | const CartList = () => {
9 | const currentCartByAuth = useGoodsByAuth($cart, $cartFromLs)
10 |
11 | return (
12 | <>
13 |
14 | {currentCartByAuth.map((item) => (
15 |
20 |
21 |
22 | ))}
23 |
24 | >
25 | )
26 | }
27 |
28 | export default CartList
29 |
--------------------------------------------------------------------------------
/components/modules/CartPage/PromotionalCode.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { useLang } from '@/hooks/useLang'
3 | import styles from '@/styles/cart-page/index.module.scss'
4 |
5 | const PromotionalCode = ({
6 | setIsCorrectPromotionalCode,
7 | }: {
8 | setIsCorrectPromotionalCode: (arg0: boolean) => void
9 | }) => {
10 | const [value, setValue] = useState('')
11 | const isCorrectCode = value === 'SKILLBLOG'
12 | const { lang, translations } = useLang()
13 |
14 | const handleChangeValue = (e: React.ChangeEvent) => {
15 | setValue(e.target.value)
16 |
17 | if (e.target.value === 'SKILLBLOG') {
18 | setIsCorrectPromotionalCode(true)
19 | } else {
20 | setIsCorrectPromotionalCode(false)
21 | }
22 | }
23 |
24 | return (
25 |
26 |
35 |
{translations[lang].order.promo_code_text}
36 |
37 | )
38 | }
39 |
40 | export default PromotionalCode
41 |
--------------------------------------------------------------------------------
/components/modules/CatalogFilters/CheckboxSelectItem.tsx:
--------------------------------------------------------------------------------
1 | import { ICheckboxSelectItemProps } from '@/types/catalog'
2 | import styles from '@/styles/catalog/index.module.scss'
3 |
4 | const CheckboxSelectItem = ({
5 | item,
6 | callback,
7 | mobileClassName,
8 | }: ICheckboxSelectItemProps) => {
9 | const handleChangeOption = () => callback(item.id)
10 |
11 | return (
12 |
17 |
32 |
33 | )
34 | }
35 |
36 | export default CheckboxSelectItem
37 |
--------------------------------------------------------------------------------
/components/modules/CatalogFilters/FiltersPopup/ColorsFilter.tsx:
--------------------------------------------------------------------------------
1 | import { useColorsFilter } from '@/hooks/useColorsFilter'
2 | import { useLang } from '@/hooks/useLang'
3 | import CheckboxSelectItem from '../CheckboxSelectItem'
4 | import styles from '@/styles/catalog/index.module.scss'
5 |
6 | const ColorsFilter = ({
7 | handleApplyFiltersWithColors,
8 | }: {
9 | handleApplyFiltersWithColors: (sizes: string[]) => void
10 | }) => {
11 | const { lang, translations } = useLang()
12 | const { handleSelectColor, colorsOptions } = useColorsFilter(
13 | handleApplyFiltersWithColors
14 | )
15 |
16 | return (
17 | <>
18 |
19 | {translations[lang].catalog.color}
20 |
21 |
24 | {colorsOptions.map((item) => (
25 |
31 | ))}
32 |
33 | >
34 | )
35 | }
36 |
37 | export default ColorsFilter
38 |
--------------------------------------------------------------------------------
/components/modules/CatalogFilters/FiltersPopup/SizesFilter.tsx:
--------------------------------------------------------------------------------
1 | import CheckboxSelectItem from '../CheckboxSelectItem'
2 | import { useSizeFilter } from '@/hooks/useSizeFilter'
3 | import { useLang } from '@/hooks/useLang'
4 | import styles from '@/styles/catalog/index.module.scss'
5 |
6 | const SizesFilter = ({
7 | handleApplyFiltersWithSizes,
8 | }: {
9 | handleApplyFiltersWithSizes: (sizes: string[]) => void
10 | }) => {
11 | const { lang, translations } = useLang()
12 | const { handleSelectSize, sizesOptions } = useSizeFilter(
13 | handleApplyFiltersWithSizes
14 | )
15 |
16 | return (
17 | <>
18 |
19 | {translations[lang].catalog.size}
20 |
21 |
24 | {sizesOptions.map((item) => (
25 |
31 | ))}
32 |
33 | >
34 | )
35 | }
36 |
37 | export default SizesFilter
38 |
--------------------------------------------------------------------------------
/components/modules/CatalogFilters/SelectBtn.tsx:
--------------------------------------------------------------------------------
1 | import { ISelectBtnProps } from '@/types/catalog'
2 | import styles from '@/styles/catalog/index.module.scss'
3 |
4 | const SelectBtn = ({
5 | open,
6 | toggle,
7 | dynamicText,
8 | defaultText,
9 | bgClassName,
10 | }: ISelectBtnProps) => (
11 |
30 | )
31 |
32 | export default SelectBtn
33 |
--------------------------------------------------------------------------------
/components/modules/CatalogFilters/SelectInfoItem.tsx:
--------------------------------------------------------------------------------
1 | import { ISelectInfoItem } from '@/types/catalog'
2 | import styles from '@/styles/catalog/index.module.scss'
3 |
4 | const SelectInfoItem = ({ text, handleRemoveItem, id }: ISelectInfoItem) => {
5 | const handleClick = () => handleRemoveItem(id)
6 |
7 | return (
8 |
9 |
10 | {text}
11 |
12 |
16 |
17 | )
18 | }
19 |
20 | export default SelectInfoItem
21 |
--------------------------------------------------------------------------------
/components/modules/CatalogFilters/SelectItem.tsx:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
2 | import { faSpinner } from '@fortawesome/free-solid-svg-icons'
3 | import { useUnit } from 'effector-react'
4 | import { loadProductsByFilterFx } from '@/context/goods'
5 | import { ISelectItemProps } from '@/types/catalog'
6 | import styles from '@/styles/catalog/index.module.scss'
7 |
8 | const SelectItem = ({
9 | isActive,
10 | mobileClassName,
11 | item,
12 | setOption,
13 | }: ISelectItemProps) => {
14 | const spinner = useUnit(loadProductsByFilterFx.pending)
15 |
16 | const handleSelectOption = () => {
17 | if (isActive) {
18 | return
19 | }
20 |
21 | setOption(item.title)
22 | item.filterHandler()
23 | }
24 |
25 | return (
26 |
31 | {spinner && isActive && (
32 |
35 |
36 |
37 | )}
38 |
44 |
45 | )
46 | }
47 |
48 | export default SelectItem
49 |
--------------------------------------------------------------------------------
/components/modules/Comparison/ComparisonLinksList.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 | import Link from 'next/link'
3 | import { IComparisonLinksListProps } from '@/types/comparison'
4 | import { basePropsForMotion } from '@/constants/motion'
5 | import styles from '@/styles/comparison/index.module.scss'
6 |
7 | const ComparisonLinksList = ({
8 | links,
9 | className,
10 | }: IComparisonLinksListProps) => (
11 |
12 | {links.map((item) => (
13 |
14 |
15 | {item.title}
16 | {item.itemsCount}
17 |
18 |
19 | ))}
20 |
21 | )
22 |
23 | export default ComparisonLinksList
24 |
--------------------------------------------------------------------------------
/components/modules/Comparison/ComparisonList.tsx:
--------------------------------------------------------------------------------
1 | import { motion, AnimatePresence } from 'framer-motion'
2 | import { IComparisonItem } from '@/types/comparison'
3 | import styles from '@/styles/comparison/index.module.scss'
4 | import ComparisonItem from './ComparisonItem'
5 |
6 | const ComparisonList = ({ items }: { items: IComparisonItem[] }) => (
7 | <>
8 | {items.length ? (
9 |
10 |
11 | {items.map((item) => (
12 |
13 | ))}
14 |
15 |
16 | ) : (
17 |
18 | )}
19 | >
20 | )
21 |
22 | export default ComparisonList
23 |
--------------------------------------------------------------------------------
/components/modules/CookieAlert/CookieAlert.tsx:
--------------------------------------------------------------------------------
1 | import toast from 'react-hot-toast'
2 | import { useLang } from '@/hooks/useLang'
3 |
4 | const CookieAlert = ({
5 | setCookieAlertOpen,
6 | }: {
7 | setCookieAlertOpen: (arg0: boolean) => void
8 | }) => {
9 | const { lang, translations } = useLang()
10 |
11 | const handleAcceptCookie = () => {
12 | document.cookie = 'CookieBy=Rostelecom; max-age=' + 60 * 60 * 24 * 30
13 |
14 | if (document.cookie) {
15 | setCookieAlertOpen(false)
16 | } else {
17 | toast.error(
18 | // eslint-disable-next-line max-len
19 | 'Файл cookie не может быть установлен! Пожалуйста, разблокируйте этот сайт с помощью настроек cookie вашего браузера..'
20 | )
21 | }
22 | }
23 |
24 | const handleCloseAlert = () => setCookieAlertOpen(false)
25 |
26 | return (
27 |
28 |
32 |
38 |
44 |
45 | )
46 | }
47 |
48 | export default CookieAlert
49 |
--------------------------------------------------------------------------------
/components/modules/EmptyPageContent/ContentLinks.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { useLang } from '@/hooks/useLang'
3 | import styles from '@/styles/empty-content/index.module.scss'
4 |
5 | const ContentLinks = ({ btnText }: { btnText: string }) => {
6 | const { lang, translations } = useLang()
7 |
8 | return (
9 |
10 | {btnText}
11 | {translations[lang].common.back_to_main}
12 |
13 | )
14 | }
15 |
16 | export default ContentLinks
17 |
--------------------------------------------------------------------------------
/components/modules/EmptyPageContent/ContentTitle.tsx:
--------------------------------------------------------------------------------
1 | import { IContentTitleProps } from '@/types/modules'
2 | import styles from '@/styles/empty-content/index.module.scss'
3 |
4 | const ContentTitle = ({ title, oopsWord }: IContentTitleProps) => (
5 |
6 | {oopsWord}
7 | {title}
8 |
9 | )
10 |
11 | export default ContentTitle
12 |
--------------------------------------------------------------------------------
/components/modules/FavoritesPage/FavoritesList.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatePresence, motion } from 'framer-motion'
2 | import { basePropsForMotion } from '@/constants/motion'
3 | import { useGoodsByAuth } from '@/hooks/useGoodsByAuth'
4 | import FavoritesListItem from './FavoritesListItem'
5 | import { $favorites, $favoritesFromLS } from '@/context/favorites/state'
6 | import styles from '@/styles/favorites/index.module.scss'
7 |
8 | const FavoritesList = () => {
9 | const currentFavoritesByAuth = useGoodsByAuth($favorites, $favoritesFromLS)
10 |
11 | return (
12 |
13 | {currentFavoritesByAuth.map((item) => (
14 |
19 |
20 |
21 | ))}
22 |
23 | )
24 | }
25 |
26 | export default FavoritesList
27 |
--------------------------------------------------------------------------------
/components/modules/Footer/FooterLinks.tsx:
--------------------------------------------------------------------------------
1 | const FooterLinks = () => (
2 |
10 | )
11 |
12 | export default FooterLinks
13 |
--------------------------------------------------------------------------------
/components/modules/Footer/FooterMobileLink.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | const FooterMobileLink = ({ text }: { text: string }) => (
4 |
5 | {text}
6 |
7 | )
8 |
9 | export default FooterMobileLink
10 |
--------------------------------------------------------------------------------
/components/modules/Header/BuyersListItems.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { useLang } from '@/hooks/useLang'
3 |
4 | const BuyersListItems = () => {
5 | const { lang, translations } = useLang()
6 |
7 | return (
8 | <>
9 |
10 |
14 | {translations[lang].main_menu.about}
15 |
16 |
17 |
18 |
19 | {translations[lang].main_menu.blog}
20 |
21 |
22 |
23 |
27 | {translations[lang].main_menu.shipping}
28 |
29 |
30 |
31 |
35 | {translations[lang].main_menu.returns}
36 |
37 |
38 | >
39 | )
40 | }
41 |
42 | export default BuyersListItems
43 |
--------------------------------------------------------------------------------
/components/modules/Header/CatalogMenuButton.tsx:
--------------------------------------------------------------------------------
1 | import { ICatalogMenuButtonProps } from '@/types/modules'
2 |
3 | const CatalogMenuButton = ({
4 | name,
5 | isActive,
6 | handler,
7 | }: ICatalogMenuButtonProps) => (
8 |
17 | )
18 |
19 | export default CatalogMenuButton
20 |
--------------------------------------------------------------------------------
/components/modules/Header/CatalogMenuList.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 | import Link from 'next/link'
3 |
4 | const CatalogMenuList = ({
5 | items,
6 | }: {
7 | items: {
8 | title: string
9 | href: string
10 | handleCloseMenu: () => void
11 | }[]
12 | }) => (
13 |
19 | {items.map((items, i) => (
20 |
25 |
30 | {items.title}
31 |
32 |
33 | ))}
34 |
35 | )
36 |
37 | export default CatalogMenuList
38 |
--------------------------------------------------------------------------------
/components/modules/Header/ContactsListItems.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { useLang } from '@/hooks/useLang'
3 |
4 | const ContactsListItems = () => {
5 | const { lang, translations } = useLang()
6 |
7 | return (
8 | <>
9 |
10 |
14 | +7 (499) 555 82 93
15 |
16 |
17 |
18 |
22 | Email
23 |
24 |
25 |
26 |
30 | {translations[lang].main_menu.tg}
31 |
32 |
33 |
34 |
35 | {translations[lang].main_menu.vk}
36 |
37 |
38 | >
39 | )
40 | }
41 |
42 | export default ContactsListItems
43 |
--------------------------------------------------------------------------------
/components/modules/Header/MenuLinkItem.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import React from 'react'
3 | import { IMenuLinkItemProps } from '@/types/modules'
4 |
5 | const MenuLinkItem = ({
6 | item,
7 | handleRedirectToCatalog,
8 | }: IMenuLinkItemProps) => {
9 | const onRedirect = () => handleRedirectToCatalog(item.href)
10 |
11 | return (
12 |
13 |
18 | {item.text}
19 |
20 |
21 | )
22 | }
23 |
24 | export default MenuLinkItem
25 |
--------------------------------------------------------------------------------
/components/modules/MainPage/BestsellerGoods.tsx:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { useLang } from '@/hooks/useLang'
3 | import MainPageSection from './MainPageSection'
4 | import { $bestsellerProducts } from '@/context/goods/state'
5 | import { getBestsellerProductsFx } from '@/context/goods'
6 |
7 | const BestsellerGoods = () => {
8 | const goods = useUnit($bestsellerProducts)
9 | const spinner = useUnit(getBestsellerProductsFx.pending)
10 | const { lang, translations } = useLang()
11 |
12 | return (
13 |
18 | )
19 | }
20 |
21 | export default BestsellerGoods
22 |
--------------------------------------------------------------------------------
/components/modules/MainPage/Hero/HeroSlide.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import Image from 'next/image'
3 | import HeroSlideTooltip from './HeroSlideTooltip'
4 | import { IHeroSlide } from '@/types/main-page'
5 | import styles from '@/styles/main-page/index.module.scss'
6 |
7 | const HeroSlide = ({ slide }: { slide: IHeroSlide }) => (
8 | <>
9 |
10 |
16 |
17 | >
18 | )
19 |
20 | export default HeroSlide
21 |
--------------------------------------------------------------------------------
/components/modules/MainPage/Hero/HeroSlideTooltip.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import { IHeroSlideTooltip } from '@/types/main-page'
3 | import styles from '@/styles/main-page/index.module.scss'
4 |
5 | const HeroSlideTooltip = ({ title, image }: IHeroSlideTooltip) => (
6 |
7 |
8 |
13 |
14 | {title}
15 | 760 ₽
16 |
17 |
18 | )
19 |
20 | export default HeroSlideTooltip
21 |
--------------------------------------------------------------------------------
/components/modules/MainPage/NewGoods.tsx:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import MainPageSection from './MainPageSection'
3 | import { useLang } from '@/hooks/useLang'
4 | import { $newProducts } from '@/context/goods/state'
5 | import { getNewProductsFx } from '@/context/goods'
6 |
7 | const NewGoods = () => {
8 | const goods = useUnit($newProducts)
9 | const spinner = useUnit(getNewProductsFx.pending)
10 | const { lang, translations } = useLang()
11 |
12 | return (
13 |
18 | )
19 | }
20 |
21 | export default NewGoods
22 |
--------------------------------------------------------------------------------
/components/modules/OrderPage/OrderTitle.tsx:
--------------------------------------------------------------------------------
1 | import { IOrderTitleProps } from '@/types/order'
2 | import styles from '@/styles/order/index.module.scss'
3 |
4 | const OrderTitle = ({ orderNumber, text }: IOrderTitleProps) => (
5 |
6 | {orderNumber}
7 | {text}
8 |
9 | )
10 |
11 | export default OrderTitle
12 |
--------------------------------------------------------------------------------
/components/modules/OrderPage/PickupAddressItem.tsx:
--------------------------------------------------------------------------------
1 | import { IPickupAddressItemProps } from '@/types/order'
2 | import { setChosenCourierAddressData } from '@/context/order'
3 | import styles from '@/styles/order/index.module.scss'
4 |
5 | const PickupAddressItem = ({
6 | addressItem,
7 | handleChosenAddressData,
8 | handleSelectAddress,
9 | }: IPickupAddressItemProps) => {
10 | const selectAddress = () => {
11 | handleChosenAddressData(addressItem)
12 | handleSelectAddress(addressItem.bbox, {
13 | lat: addressItem.lat,
14 | lon: addressItem.lon,
15 | })
16 | setChosenCourierAddressData({})
17 | }
18 |
19 | return (
20 |
21 |
27 |
28 | )
29 | }
30 |
31 | export default PickupAddressItem
32 |
--------------------------------------------------------------------------------
/components/modules/OrderPage/TabControls.tsx:
--------------------------------------------------------------------------------
1 | import { ITabControlsProps } from '@/types/order'
2 | import styles from '@/styles/order/index.module.scss'
3 |
4 | const TabControls = ({
5 | handleTab1,
6 | handleTab2,
7 | tab1Active,
8 | tab2Active,
9 | tab1Text,
10 | tab2Text,
11 | }: ITabControlsProps) => (
12 |
13 |
21 |
29 |
30 | )
31 |
32 | export default TabControls
33 |
--------------------------------------------------------------------------------
/components/modules/ProductPage/ProductImagesItem.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import { IProductImagesItemProps } from '@/types/product'
3 | import useImagePreloader from '@/hooks/useImagePreloader'
4 | import styles from '@/styles/product/index.module.scss'
5 |
6 | const ProductImagesItem = ({ image, imgSize }: IProductImagesItemProps) => {
7 | const { handleLoadingImageComplete, imgSpinner } = useImagePreloader()
8 |
9 | return (
10 |
15 |
23 |
24 | )
25 | }
26 |
27 | export default ProductImagesItem
28 |
--------------------------------------------------------------------------------
/components/modules/ProductPage/ProductInfoAccordion.tsx:
--------------------------------------------------------------------------------
1 | import { IProductInfoAccordionProps } from '@/types/product'
2 | import Accordion from '../Accordion/Accordion'
3 | import styles from '@/styles/product/index.module.scss'
4 |
5 | const ProductInfoAccordion = ({
6 | children,
7 | title,
8 | }: IProductInfoAccordionProps) => (
9 |
14 | {children}
15 |
16 | )
17 |
18 | export default ProductInfoAccordion
19 |
--------------------------------------------------------------------------------
/components/modules/ProductsListItem/AddToCartBtn.tsx:
--------------------------------------------------------------------------------
1 | import { faSpinner } from '@fortawesome/free-solid-svg-icons'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { IAddToCartBtnProps } from '@/types/goods'
4 |
5 | const AddToCartBtn = ({
6 | handleAddToCart,
7 | addToCartSpinner,
8 | text,
9 | btnDisabled = false,
10 | className,
11 | }: IAddToCartBtnProps) => (
12 |
23 | )
24 |
25 | export default AddToCartBtn
26 |
--------------------------------------------------------------------------------
/components/modules/ProductsListItem/ProductColor.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useLang } from '@/hooks/useLang'
3 | import styles from '@/styles/product-list-item/index.module.scss'
4 | import { IProductInfoLabelProps } from '@/types/modules'
5 |
6 | const ProductColor = ({ color, className }: IProductInfoLabelProps) => {
7 | const { lang, translations } = useLang()
8 |
9 | return (
10 |
11 | {translations[lang].catalog.color}:{' '}
12 | {(translations[lang].catalog as { [index: string]: string })[color]}
13 |
14 | )
15 | }
16 |
17 | export default ProductColor
18 |
--------------------------------------------------------------------------------
/components/modules/ProductsListItem/ProductComposition.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import styles from '@/styles/product-list-item/index.module.scss'
3 | import { useLang } from '@/hooks/useLang'
4 |
5 | const ProductComposition = ({ composition }: { composition: string }) => {
6 | const { lang, translations } = useLang()
7 |
8 | return (
9 |
10 | {translations[lang].product.composition}:{' '}
11 | {/**eslint-disable-next-line @typescript-eslint/ban-ts-comment
12 | * @ts-ignore */}
13 | {translations[lang].catalog[composition]}
14 |
15 | )
16 | }
17 |
18 | export default ProductComposition
19 |
--------------------------------------------------------------------------------
/components/modules/ProductsListItem/ProductCountBySize.tsx:
--------------------------------------------------------------------------------
1 | import { getCartItemCountBySize } from '@/lib/utils/common'
2 | import { IProductCountBySizeProps } from '@/types/goods'
3 | import styles from '@/styles/product-count-indicator/index.module.scss'
4 |
5 | const ProductCountBySize = ({
6 | products,
7 | size,
8 | withCartIcon = true,
9 | }: IProductCountBySizeProps) => (
10 | <>
11 | {!!getCartItemCountBySize(products, size) && (
12 |
15 | {getCartItemCountBySize(products, size)}
16 |
17 | )}
18 | >
19 | )
20 |
21 | export default ProductCountBySize
22 |
--------------------------------------------------------------------------------
/components/modules/ProductsListItem/ProductLabel.tsx:
--------------------------------------------------------------------------------
1 | import { useLang } from '@/hooks/useLang'
2 | import { IProductLabelProps } from '@/types/modules'
3 | import styles from '@/styles/product-list-item/index.module.scss'
4 |
5 | const ProductLabel = ({ isNew, isBestseller }: IProductLabelProps) => {
6 | const { lang, translations } = useLang()
7 |
8 | const bestsellerLabel = (
9 |
12 | {translations[lang].main_page.is_bestseller}
13 |
14 | )
15 | const newLabel = (
16 |
20 | {translations[lang].main_page.is_new}
21 |
22 | )
23 | const allLabel = (
24 |
25 | {newLabel}
26 | {bestsellerLabel}
27 |
28 | )
29 |
30 | if (isNew && isBestseller) {
31 | return allLabel
32 | }
33 |
34 | if (isBestseller) {
35 | return bestsellerLabel
36 | }
37 |
38 | return newLabel
39 | }
40 |
41 | export default ProductLabel
42 |
--------------------------------------------------------------------------------
/components/modules/ProductsListItem/ProductSizeTableBtn.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useUnit } from 'effector-react'
3 | import { showSizeTable } from '@/context/modals'
4 | import { addOverflowHiddenToBody } from '@/lib/utils/common'
5 | import { ISelectedSizes } from '@/types/common'
6 | import { setSizeTableSizes } from '@/context/sizeTable'
7 | import { useLang } from '@/hooks/useLang'
8 | import { setIsAddToFavorites } from '@/context/favorites'
9 | import { $showQuickViewModal } from '@/context/modals/state'
10 |
11 | const ProductSizeTableBtn = ({ sizes, type, className }: ISelectedSizes) => {
12 | const { lang, translations } = useLang()
13 | const showQuickViewModal = useUnit($showQuickViewModal)
14 |
15 | const handleShowSizeTable = () => {
16 | setIsAddToFavorites(false)
17 |
18 | if (!showQuickViewModal) {
19 | addOverflowHiddenToBody()
20 | }
21 |
22 | setSizeTableSizes({ sizes, type })
23 | showSizeTable()
24 | }
25 |
26 | return (
27 |
30 | )
31 | }
32 |
33 | export default ProductSizeTableBtn
34 |
--------------------------------------------------------------------------------
/components/modules/ProductsListItem/ProductSizesItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { IProductSizesItemProps } from '@/types/goods'
3 | import styles from '@/styles/quick-view-modal/index.module.scss'
4 | import ProductCountBySize from './ProductCountBySize'
5 |
6 | const ProductSizesItem = ({
7 | currentSize,
8 | selectedSize,
9 | setSelectedSize,
10 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
11 | currentCartItems,
12 | }: IProductSizesItemProps) => {
13 | const handleSelectSize = () => setSelectedSize(currentSize[0])
14 |
15 | return (
16 |
29 |
34 |
37 |
38 | )
39 | }
40 |
41 | export default ProductSizesItem
42 |
--------------------------------------------------------------------------------
/components/modules/ProfilePage/CodeInputBlock/CodeInput.tsx:
--------------------------------------------------------------------------------
1 | import { ICodeInputProps } from '@/types/profile'
2 |
3 | const CodeInput = ({
4 | processInput,
5 | onKeyUp,
6 | index,
7 | handlePushCurrentInput,
8 | num,
9 | autoFocus,
10 | }: ICodeInputProps) => {
11 | const handleProcessInput = (e: React.ChangeEvent) =>
12 | processInput(e, index)
13 |
14 | const handleOnKeyUp = (e: React.KeyboardEvent) =>
15 | onKeyUp(e, index)
16 |
17 | return (
18 |
28 | )
29 | }
30 |
31 | export default CodeInput
32 |
--------------------------------------------------------------------------------
/components/modules/ProfilePage/ProfileInfoActions.tsx:
--------------------------------------------------------------------------------
1 | import { faSpinner } from '@fortawesome/free-solid-svg-icons'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { IProfileInfoActionsProps } from '@/types/profile'
4 | import styles from '@/styles/profile/index.module.scss'
5 |
6 | const ProfileInfoActions = ({
7 | spinner,
8 | handleSaveInfo,
9 | disabled,
10 | handleCancelEdit,
11 | }: IProfileInfoActionsProps) => (
12 |
13 |
22 |
26 |
27 | )
28 |
29 | export default ProfileInfoActions
30 |
--------------------------------------------------------------------------------
/components/modules/ProfilePage/ProfileInfoBlock.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 | import { IProfileInfoBlockProps } from '@/types/profile'
3 | import { basePropsForMotion } from '@/constants/motion'
4 | import styles from '@/styles/profile/index.module.scss'
5 |
6 | const ProfileInfoBlock = ({ allowEdit, text }: IProfileInfoBlockProps) => (
7 |
8 | {text}
9 |
13 |
14 | )
15 |
16 | export default ProfileInfoBlock
17 |
--------------------------------------------------------------------------------
/components/modules/ShareModal/ShareModal.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import {
3 | WhatsappShareButton,
4 | WhatsappIcon,
5 | TelegramShareButton,
6 | TelegramIcon,
7 | } from 'react-share'
8 | import { useLang } from '@/hooks/useLang'
9 | import { handleCloseShareModal } from '@/lib/utils/common'
10 | import styles from '@/styles/share-modal/index.module.scss'
11 |
12 | const ShareModal = () => {
13 | const { lang, translations } = useLang()
14 |
15 | return (
16 |
17 |
18 | {translations[lang].product.share}
19 |
20 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | export default ShareModal
38 |
--------------------------------------------------------------------------------
/components/templates/ComparisonPage/ComparisonPage.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import ComparisonLinksList from '@/components/modules/Comparison/ComparisonLinksList'
3 | import { useComparisonLinks } from '@/hooks/useComparisonLinks'
4 | import styles from '@/styles/comparison/index.module.scss'
5 |
6 | const ComparisonPage = () => {
7 | const { availableProductLinks } = useComparisonLinks()
8 |
9 | return (
10 |
14 | )
15 | }
16 |
17 | export default ComparisonPage
18 |
--------------------------------------------------------------------------------
/components/templates/MainPage/MainPage.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useGate } from 'effector-react'
3 | import Categories from '@/components/modules/MainPage/Categories/Categories'
4 | import Hero from '@/components/modules/MainPage/Hero/Hero'
5 | import { MainPageGate } from '@/context/goods'
6 | import BestsellerGoods from '@/components/modules/MainPage/BestsellerGoods'
7 | import NewGoods from '@/components/modules/MainPage/NewGoods'
8 | import BrandLife from '@/components/modules/MainPage/BrandLife'
9 | import { usePageTitle } from '@/hooks/usePageTitle'
10 |
11 | const MainPage = () => {
12 | useGate(MainPageGate)
13 | usePageTitle('main')
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default MainPage
27 |
--------------------------------------------------------------------------------
/constants/corsHeaders.ts:
--------------------------------------------------------------------------------
1 | export const corsHeaders = {
2 | headers: {
3 | 'Access-Control-Allow-Origin': '*',
4 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
5 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/constants/jwt.ts:
--------------------------------------------------------------------------------
1 | export enum JWTError {
2 | INVALID_JWT_TOKEN = 'JsonWebTokenError',
3 | EXPIRED_JWT_TOKEN = 'TokenExpiredError',
4 | }
5 |
--------------------------------------------------------------------------------
/constants/lang.ts:
--------------------------------------------------------------------------------
1 | export enum AllowedLangs {
2 | RU = 'ru',
3 | EN = 'en',
4 | }
5 |
--------------------------------------------------------------------------------
/constants/map.ts:
--------------------------------------------------------------------------------
1 | export const mapOptions = {
2 | searchOptions: {
3 | key: process.env.NEXT_PUBLIC_TOMTOM_API_KEY,
4 | language: 'ru-RU',
5 | limit: 5,
6 | },
7 | autocompleteOptions: {
8 | key: process.env.NEXT_PUBLIC_TOMTOM_API_KEY,
9 | language: 'ru-RU',
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/constants/motion.ts:
--------------------------------------------------------------------------------
1 | export const basePropsForMotion = {
2 | initial: { opacity: 0 },
3 | animate: { opacity: 1 },
4 | exit: { opacity: 0 },
5 | }
6 |
--------------------------------------------------------------------------------
/constants/product.ts:
--------------------------------------------------------------------------------
1 | export const productsWithoutSizes = [
2 | 'umbrella',
3 | 'pen',
4 | 'notebook',
5 | 'promotional-souvenirs',
6 | 'business-souvenirs',
7 | ]
8 |
9 | export const productTypes = [
10 | 'long-sleeves',
11 | 'bags',
12 | 'business-souvenirs',
13 | 'headdress',
14 | 'hoodie',
15 | 'notebook',
16 | 'pen',
17 | 'outerwear',
18 | 'promotional-souvenirs',
19 | 't-shirts',
20 | 'umbrella',
21 | ]
22 |
23 | export const productCategories = ['cloth', 'accessories', 'office', 'souvenirs']
24 | export const allowedColors = ['purpure', 'yellow', 'orange', 'black', 'white']
25 | export const allowedSizes = ['s', 'l', 'm', 'xl', 'xxl']
26 | export const allowedCollectionsCategories = ['cloth', 'accessories']
27 | export const allowedCollections = ['street', 'black', 'casual', 'orange', 'line']
28 |
--------------------------------------------------------------------------------
/constants/slider.ts:
--------------------------------------------------------------------------------
1 | export const baseSliderSettings = {
2 | dots: false,
3 | infinite: true,
4 | slidesToScroll: 1,
5 | variableWidth: true,
6 | speed: 500,
7 | autoplay: true,
8 | arrows: false,
9 | }
10 |
--------------------------------------------------------------------------------
/context/auth/init.ts:
--------------------------------------------------------------------------------
1 | import { sample } from 'effector'
2 | import { handleSignUp, singUpFx, handleSignIn, singInFx } from '.'
3 | import { $auth } from './state'
4 |
5 | sample({
6 | clock: handleSignUp,
7 | source: $auth,
8 | fn: (_, { name, email, password, isOAuth }) => ({
9 | name,
10 | password,
11 | email,
12 | isOAuth,
13 | }),
14 | target: singUpFx,
15 | })
16 |
17 | sample({
18 | clock: handleSignIn,
19 | source: $auth,
20 | fn: (_, { email, password, isOAuth, name }) => ({
21 | email,
22 | password,
23 | isOAuth,
24 | name,
25 | }),
26 | target: singInFx,
27 | })
28 |
--------------------------------------------------------------------------------
/context/auth/state.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import toast from 'react-hot-toast'
4 | import {
5 | auth,
6 | closeAuthPopup,
7 | openAuthPopup,
8 | setIsAuth,
9 | singInFx,
10 | singUpFx,
11 | } from '.'
12 |
13 | export const $openAuthPopup = auth
14 | .createStore(false)
15 | .on(openAuthPopup, () => true)
16 | .on(closeAuthPopup, () => false)
17 |
18 | export const $isAuth = auth
19 | .createStore(false)
20 | .on(setIsAuth, (_, isAuth) => isAuth)
21 |
22 | export const $auth = auth
23 | .createStore({})
24 | .on(singUpFx.done, (_, { result }) => result)
25 | .on(singUpFx.fail, (_, { error }) => {
26 | toast.error(error.message)
27 | })
28 | .on(singInFx.done, (_, { result }) => result)
29 | .on(singInFx.fail, (_, { error }) => {
30 | toast.error(error.message)
31 | })
32 |
--------------------------------------------------------------------------------
/context/cart/init.ts:
--------------------------------------------------------------------------------
1 | import { sample } from 'effector'
2 | import {
3 | loadCartItems,
4 | getCartItemsFx,
5 | addProductToCart,
6 | addProductToCartFx,
7 | addProductsFromLSToCart,
8 | addProductsFromLSToCartFx,
9 | updateCartItemCount,
10 | updateCartItemCountFx,
11 | deleteProductFromCart,
12 | deleteCartItemFx,
13 | deleteAllFromCart,
14 | deleteAllFromCartFx,
15 | } from '.'
16 | import { $cart } from './state'
17 |
18 | sample({
19 | clock: loadCartItems,
20 | source: $cart,
21 | fn: (_, data) => data,
22 | target: getCartItemsFx,
23 | })
24 |
25 | sample({
26 | clock: addProductToCart,
27 | source: $cart,
28 | fn: (_, data) => data,
29 | target: addProductToCartFx,
30 | })
31 |
32 | sample({
33 | clock: addProductsFromLSToCart,
34 | source: $cart,
35 | fn: (_, data) => data,
36 | target: addProductsFromLSToCartFx,
37 | })
38 |
39 | sample({
40 | clock: updateCartItemCount,
41 | source: $cart,
42 | fn: (_, data) => data,
43 | target: updateCartItemCountFx,
44 | })
45 |
46 | sample({
47 | clock: deleteProductFromCart,
48 | source: $cart,
49 | fn: (_, data) => data,
50 | target: deleteCartItemFx,
51 | })
52 |
53 | sample({
54 | clock: deleteAllFromCart,
55 | source: {},
56 | fn: (_, data) => data,
57 | target: deleteAllFromCartFx,
58 | })
59 |
--------------------------------------------------------------------------------
/context/cart/state.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ICartItem } from '@/types/cart'
3 | import {
4 | cart,
5 | getCartItemsFx,
6 | addProductsFromLSToCartFx,
7 | addProductToCartFx,
8 | updateCartItemCountFx,
9 | deleteCartItemFx,
10 | setCartFromLS,
11 | setTotalPrice,
12 | setShouldShowEmpty,
13 | deleteAllFromCartFx,
14 | } from '.'
15 |
16 | export const $cart = cart
17 | .createStore([])
18 | .on(getCartItemsFx.done, (_, { result }) => result)
19 | .on(addProductsFromLSToCartFx.done, (_, { result }) => result.items)
20 | .on(addProductToCartFx.done, (cart, { result }) => [
21 | ...new Map(
22 | [...cart, result.newCartItem].map((item) => [item.clientId, item])
23 | ).values(),
24 | ])
25 | .on(updateCartItemCountFx.done, (cart, { result }) =>
26 | cart.map((item) =>
27 | item._id === result.id ? { ...item, count: result.count } : item
28 | )
29 | )
30 | .on(deleteCartItemFx.done, (cart, { result }) =>
31 | cart.filter((item) => item._id !== result.id)
32 | )
33 | .on(deleteAllFromCartFx.done, () => [])
34 |
35 | export const $cartFromLs = cart
36 | .createStore([])
37 | .on(setCartFromLS, (_, cart) => cart)
38 |
39 | export const $totalPrice = cart
40 | .createStore(0)
41 | .on(setTotalPrice, (_, value) => value)
42 |
43 | export const $shouldShowEmpty = cart
44 | .createStore(false)
45 | .on(setShouldShowEmpty, (_, value) => value)
46 |
--------------------------------------------------------------------------------
/context/catalog/index.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { createDomain } from 'effector'
3 | import {
4 | ICatalogCategoryOptions,
5 | ISizeOption,
6 | IColorOption,
7 | } from '@/types/catalog'
8 |
9 | export const catalog = createDomain()
10 |
11 | export const setCatalogCategoryOptions =
12 | catalog.createEvent>()
13 | export const setSizesOptions = catalog.createEvent()
14 | export const setColorsOptions = catalog.createEvent()
15 | export const updateSizesOptionBySize = catalog.createEvent()
16 | export const updateColorsOptionByCode = catalog.createEvent()
17 | export const setColors = catalog.createEvent()
18 | export const setSizes = catalog.createEvent()
19 | export const setFiltersPopup = catalog.createEvent()
20 |
--------------------------------------------------------------------------------
/context/comparison/init.ts:
--------------------------------------------------------------------------------
1 | import { sample } from 'effector'
2 | import {
3 | loadComparisonItems,
4 | getComparisonItemsFx,
5 | addProductToComparison,
6 | addProductToComparisonFx,
7 | addProductsFromLSToComparison,
8 | addProductsFromLSToComparisonFx,
9 | deleteProductFromComparison,
10 | deleteComparisonItemFx,
11 | } from '.'
12 | import { $comparison } from './state'
13 |
14 | sample({
15 | clock: loadComparisonItems,
16 | source: $comparison,
17 | fn: (_, data) => data,
18 | target: getComparisonItemsFx,
19 | })
20 |
21 | sample({
22 | clock: addProductToComparison,
23 | source: $comparison,
24 | fn: (_, data) => data,
25 | target: addProductToComparisonFx,
26 | })
27 |
28 | sample({
29 | clock: addProductsFromLSToComparison,
30 | source: $comparison,
31 | fn: (_, data) => data,
32 | target: addProductsFromLSToComparisonFx,
33 | })
34 |
35 | sample({
36 | clock: deleteProductFromComparison,
37 | source: $comparison,
38 | fn: (_, data) => data,
39 | target: deleteComparisonItemFx,
40 | })
41 |
--------------------------------------------------------------------------------
/context/comparison/state.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { IComparisonItem } from '@/types/comparison'
3 | import {
4 | comparison,
5 | getComparisonItemsFx,
6 | addProductToComparisonFx,
7 | addProductsFromLSToComparisonFx,
8 | deleteComparisonItemFx,
9 | setComparisonFromLS,
10 | setShouldShowEmptyComparison,
11 | } from '.'
12 |
13 | export const $comparison = comparison
14 | .createStore([])
15 | .on(getComparisonItemsFx.done, (_, { result }) => result)
16 | .on(addProductToComparisonFx.done, (state, { result }) => [
17 | ...state,
18 | result.newComparisonItem,
19 | ])
20 | .on(addProductsFromLSToComparisonFx.done, (_, { result }) => result.items)
21 | .on(deleteComparisonItemFx.done, (state, { result }) =>
22 | state.filter((item) => item._id !== result.id)
23 | )
24 |
25 | export const $comparisonFromLs = comparison
26 | .createStore([])
27 | .on(setComparisonFromLS, (_, comparison) => comparison)
28 |
29 | export const $shouldShowEmptyComparison = comparison
30 | .createStore(false)
31 | .on(setShouldShowEmptyComparison, (_, value) => value)
32 |
--------------------------------------------------------------------------------
/context/favorites/init.ts:
--------------------------------------------------------------------------------
1 | import { sample } from 'effector'
2 | import {
3 | addProductToFavorites,
4 | addProductToFavoriteFx,
5 | loadFavoriteItems,
6 | getFavoriteItemsFx,
7 | addProductsFromLSToFavorites,
8 | addProductsFromLSToFavoritesFx,
9 | deleteProductFromFavorites,
10 | deleteFavoriteItemFx,
11 | } from '.'
12 | import { $favorites } from './state'
13 |
14 | sample({
15 | clock: addProductToFavorites,
16 | source: $favorites,
17 | fn: (_, data) => data,
18 | target: addProductToFavoriteFx,
19 | })
20 |
21 | sample({
22 | clock: loadFavoriteItems,
23 | source: $favorites,
24 | fn: (_, data) => data,
25 | target: getFavoriteItemsFx,
26 | })
27 |
28 | sample({
29 | clock: addProductsFromLSToFavorites,
30 | source: $favorites,
31 | fn: (_, data) => data,
32 | target: addProductsFromLSToFavoritesFx,
33 | })
34 |
35 | sample({
36 | clock: deleteProductFromFavorites,
37 | source: $favorites,
38 | fn: (_, data) => data,
39 | target: deleteFavoriteItemFx,
40 | })
41 |
--------------------------------------------------------------------------------
/context/favorites/state.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { IFavoriteItem } from '@/types/favorites'
3 | import {
4 | favorites,
5 | getFavoriteItemsFx,
6 | addProductToFavoriteFx,
7 | addProductsFromLSToFavoritesFx,
8 | deleteFavoriteItemFx,
9 | setFavoritesFromLS,
10 | setIsAddToFavorites,
11 | setShouldShowEmptyFavorites,
12 | } from '.'
13 |
14 | export const $favorites = favorites
15 | .createStore([])
16 | .on(getFavoriteItemsFx.done, (_, { result }) => result)
17 | .on(addProductToFavoriteFx.done, (cart, { result }) => [
18 | ...new Map(
19 | [...cart, result.newFavoriteItem].map((item) => [item.clientId, item])
20 | ).values(),
21 | ])
22 | .on(addProductsFromLSToFavoritesFx.done, (_, { result }) => result.items)
23 | .on(deleteFavoriteItemFx.done, (state, { result }) =>
24 | state.filter((item) => item._id !== result.id)
25 | )
26 |
27 | export const $favoritesFromLS = favorites
28 | .createStore([])
29 | .on(setFavoritesFromLS, (_, favorites) => favorites)
30 |
31 | export const $isAddToFavorites = favorites
32 | .createStore(false)
33 | .on(setIsAddToFavorites, (_, value) => value)
34 |
35 | export const $shouldShowEmptyFavorites = favorites
36 | .createStore(false)
37 | .on(setShouldShowEmptyFavorites, (_, value) => value)
38 |
--------------------------------------------------------------------------------
/context/goods/init.ts:
--------------------------------------------------------------------------------
1 | import { Effect, sample } from 'effector'
2 | import { Gate } from 'effector-react'
3 | import {
4 | MainPageGate,
5 | getBestsellerProductsFx,
6 | getNewProductsFx,
7 | loadOneProduct,
8 | loadOneProductFx,
9 | loadProductBySearch,
10 | loadProductBySearchFx,
11 | loadProductsByFilter,
12 | loadProductsByFilterFx,
13 | loadWatchedProducts,
14 | loadWatchedProductsFx,
15 | } from '.'
16 | import {
17 | $currentProduct,
18 | $products,
19 | $productsBySearch,
20 | $watchedProducts,
21 | } from './state'
22 |
23 | const goodsSampleInstance = (
24 | effect: Effect,
25 | gate: Gate
26 | ) =>
27 | sample({
28 | clock: gate.open,
29 | target: effect,
30 | })
31 |
32 | goodsSampleInstance(getNewProductsFx, MainPageGate)
33 | goodsSampleInstance(getBestsellerProductsFx, MainPageGate)
34 |
35 | sample({
36 | clock: loadOneProduct,
37 | source: $currentProduct,
38 | fn: (_, data) => data,
39 | target: loadOneProductFx,
40 | })
41 |
42 | sample({
43 | clock: loadProductsByFilter,
44 | source: $products,
45 | fn: (_, data) => data,
46 | target: loadProductsByFilterFx,
47 | })
48 |
49 | sample({
50 | clock: loadWatchedProducts,
51 | source: $watchedProducts,
52 | fn: (_, data) => data,
53 | target: loadWatchedProductsFx,
54 | })
55 |
56 | sample({
57 | clock: loadProductBySearch,
58 | source: $productsBySearch,
59 | fn: (_, data) => data,
60 | target: loadProductBySearchFx,
61 | })
62 |
--------------------------------------------------------------------------------
/context/lang/index.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { createDomain } from 'effector'
3 | import { AllowedLangs } from '@/constants/lang'
4 |
5 | export const lang = createDomain()
6 |
7 | export const setLang = lang.createEvent()
8 |
--------------------------------------------------------------------------------
/context/lang/state.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { AllowedLangs } from '@/constants/lang'
3 | import { lang, setLang } from '.'
4 |
5 | export const $lang = lang
6 | .createStore(AllowedLangs.RU)
7 | .on(setLang, (_, lang) => lang)
8 |
--------------------------------------------------------------------------------
/context/modals/index.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { createDomain } from 'effector'
3 |
4 | export const modals = createDomain()
5 |
6 | export const openMenu = modals.createEvent()
7 | export const closeMenu = modals.createEvent()
8 | export const openCatalogMenu = modals.createEvent()
9 | export const closeCatalogMenu = modals.createEvent()
10 | export const openSearchModal = modals.createEvent()
11 | export const closeSearchModal = modals.createEvent()
12 | export const closeQuickViewModal = modals.createEvent()
13 | export const showQuickViewModal = modals.createEvent()
14 | export const closeSizeTable = modals.createEvent()
15 | export const showSizeTable = modals.createEvent()
16 | export const openShareModal = modals.createEvent()
17 | export const closeShareModal = modals.createEvent()
18 | export const openMapModal = modals.createEvent()
19 | export const closeMapModal = modals.createEvent()
20 |
--------------------------------------------------------------------------------
/context/modals/state.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import {
3 | modals,
4 | openMenu,
5 | closeMenu,
6 | openCatalogMenu,
7 | closeCatalogMenu,
8 | openSearchModal,
9 | closeSearchModal,
10 | showQuickViewModal,
11 | closeQuickViewModal,
12 | closeSizeTable,
13 | showSizeTable,
14 | closeShareModal,
15 | openShareModal,
16 | closeMapModal,
17 | openMapModal,
18 | } from '.'
19 |
20 | export const $menuIsOpen = modals
21 | .createStore(false)
22 | .on(openMenu, () => true)
23 | .on(closeMenu, () => false)
24 |
25 | export const $catalogMenuIsOpen = modals
26 | .createStore(false)
27 | .on(openCatalogMenu, () => true)
28 | .on(closeCatalogMenu, () => false)
29 |
30 | export const $searchModal = modals
31 | .createStore(false)
32 | .on(openSearchModal, () => true)
33 | .on(closeSearchModal, () => false)
34 |
35 | export const $showQuickViewModal = modals
36 | .createStore(false)
37 | .on(showQuickViewModal, () => true)
38 | .on(closeQuickViewModal, () => false)
39 |
40 | export const $showSizeTable = modals
41 | .createStore(false)
42 | .on(closeSizeTable, () => false)
43 | .on(showSizeTable, () => true)
44 |
45 | export const $shareModal = modals
46 | .createStore(false)
47 | .on(openShareModal, () => true)
48 | .on(closeShareModal, () => false)
49 |
50 | export const $mapModal = modals
51 | .createStore(false)
52 | .on(openMapModal, () => true)
53 | .on(closeMapModal, () => false)
54 |
--------------------------------------------------------------------------------
/context/order/init.ts:
--------------------------------------------------------------------------------
1 | import { sample } from 'effector'
2 | import {
3 | getRostelecomOfficesByCity,
4 | getRostelecomOfficesByCityFx,
5 | makePayment,
6 | makePaymentFx,
7 | } from '.'
8 |
9 | sample({
10 | clock: getRostelecomOfficesByCity,
11 | source: {},
12 | fn: (_, data) => data,
13 | target: getRostelecomOfficesByCityFx,
14 | })
15 |
16 | sample({
17 | clock: makePayment,
18 | source: {},
19 | fn: (_, data) => data,
20 | target: makePaymentFx,
21 | })
22 |
--------------------------------------------------------------------------------
/context/passwordRestore/init.ts:
--------------------------------------------------------------------------------
1 | import { sample } from 'effector'
2 | import { updateUserPassword, updateUserPasswordFx } from '.'
3 |
4 | sample({
5 | clock: updateUserPassword,
6 | source: {},
7 | fn: (_, data) => data,
8 | target: updateUserPasswordFx,
9 | })
10 |
--------------------------------------------------------------------------------
/context/profile/init.ts:
--------------------------------------------------------------------------------
1 | import { sample } from 'effector'
2 | import {
3 | deleteUser,
4 | deleteUserFx,
5 | editUsername,
6 | editUsernameFx,
7 | uploadAvatar,
8 | uploadUserAvatarFx,
9 | } from '.'
10 |
11 | sample({
12 | clock: uploadAvatar,
13 | source: {},
14 | fn: (_, data) => data,
15 | target: uploadUserAvatarFx,
16 | })
17 |
18 | sample({
19 | clock: editUsername,
20 | source: {},
21 | fn: (_, data) => data,
22 | target: editUsernameFx,
23 | })
24 |
25 | sample({
26 | clock: deleteUser,
27 | source: {},
28 | fn: (_, data) => data,
29 | target: deleteUserFx,
30 | })
31 |
--------------------------------------------------------------------------------
/context/profile/state.ts:
--------------------------------------------------------------------------------
1 | import { profile, uploadUserAvatarFx } from '.'
2 |
3 | export const $userImage = profile
4 | .createStore('')
5 | .on(uploadUserAvatarFx.done, (_, { result }) => result.image)
6 |
--------------------------------------------------------------------------------
/context/sizeTable/index.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { createDomain } from 'effector'
3 | import { ISelectedSizes } from '@/types/common'
4 |
5 | export const sizeTable = createDomain()
6 |
7 | export const setSizeTableSizes = sizeTable.createEvent()
8 |
--------------------------------------------------------------------------------
/context/sizeTable/state.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ISelectedSizes } from '@/types/common'
3 | import { sizeTable } from '.'
4 | import { setSizeTableSizes } from '../sizeTable'
5 |
6 | export const $sizeTableSizes = sizeTable
7 | .createStore({} as ISelectedSizes)
8 | .on(setSizeTableSizes, (_, sizes) => sizes)
9 |
--------------------------------------------------------------------------------
/context/user/init.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { sample } from 'effector'
3 | import { loginCheck, loginCheckFx } from '.'
4 | import { $user } from './state'
5 |
6 | sample({
7 | clock: loginCheck,
8 | source: $user,
9 | fn: (_, { jwt }) => ({
10 | jwt,
11 | }),
12 | target: loginCheckFx,
13 | })
14 |
--------------------------------------------------------------------------------
/context/user/state.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { IUser, IUserGeolocation } from '@/types/user'
3 | import {
4 | user,
5 | loginCheckFx,
6 | setUserGeolocation,
7 | updateUsername,
8 | updateUserImage,
9 | updateUserEmail,
10 | } from '.'
11 |
12 | export const $user = user
13 | .createStore({} as IUser)
14 | .on(loginCheckFx.done, (_, { result }) => result)
15 | .on(updateUsername, (state, name) => ({ ...state, name }))
16 | .on(updateUserImage, (state, image) => ({ ...state, image }))
17 | .on(updateUserEmail, (state, email) => ({ ...state, email }))
18 |
19 | export const $userGeolocation = user
20 | .createStore({} as IUserGeolocation)
21 | .on(setUserGeolocation, (_, data) => data)
22 |
--------------------------------------------------------------------------------
/hooks/useAuthForm.ts:
--------------------------------------------------------------------------------
1 | import { useEarthoOne } from '@eartho/one-client-react'
2 | import { EventCallable, Store } from 'effector'
3 | import { useUnit } from 'effector-react'
4 | import { useEffect } from 'react'
5 | import { useForm } from 'react-hook-form'
6 | import { IInputs, ISignUpFx } from '@/types/authPopup'
7 |
8 | export const useAuthForm = (
9 | initialSpinner: Store,
10 | isSideActive: boolean,
11 | event: EventCallable
12 | ) => {
13 | const spinner = useUnit(initialSpinner)
14 | const { isConnected, user, connectWithPopup } = useEarthoOne()
15 |
16 | const {
17 | register,
18 | formState: { errors },
19 | handleSubmit,
20 | } = useForm()
21 |
22 | useEffect(() => {
23 | if (isSideActive) {
24 | if (isConnected) {
25 | event({
26 | name: user?.displayName as string,
27 | email: user?.email as string,
28 | password: user?.uid as string,
29 | isOAuth: true,
30 | })
31 | }
32 | }
33 | }, [isConnected])
34 |
35 | const handleSignupWithOAuth = () =>
36 | connectWithPopup({
37 | accessId: `${process.env.NEXT_PUBLIC_OAUTH_ACCESS_ID}`,
38 | })
39 |
40 | return {
41 | spinner,
42 | register,
43 | errors,
44 | handleSubmit,
45 | handleSignupWithOAuth,
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/hooks/useCategoryFilter.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { useEffect, useState } from 'react'
3 | import { useLang } from './useLang'
4 | import { getSearchParamsUrl } from '@/lib/utils/common'
5 | import { $catalogCategoryOptions } from '@/context/catalog/state'
6 |
7 | export const useCategoryFilter = () => {
8 | const { lang, translations } = useLang()
9 | const catalogCategoryOptions = useUnit($catalogCategoryOptions)
10 | const [option, setOption] = useState('')
11 | const currentOptions = Object.values(catalogCategoryOptions)[0]
12 | const allCategoriesTitle = translations[lang].catalog.all_categories
13 |
14 | const handleSelectAllCategories = () => setOption(allCategoriesTitle)
15 |
16 | useEffect(() => {
17 | const urlParams = getSearchParamsUrl()
18 | const typeParam = urlParams.get('type')
19 |
20 | if (typeParam) {
21 | setOption(
22 | (translations[lang].comparison as { [index: string]: string })[
23 | typeParam
24 | ]
25 | )
26 | }
27 | }, [lang, translations])
28 |
29 | return {
30 | handleSelectAllCategories,
31 | currentOptions,
32 | option,
33 | setOption,
34 | catalogCategoryOptions,
35 | allCategoriesTitle,
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/hooks/useClickOutside.ts:
--------------------------------------------------------------------------------
1 | import { MutableRefObject, useEffect, useRef, useState } from 'react'
2 |
3 | export const useClickOutside = () => {
4 | const ref = useRef() as MutableRefObject
5 | const [open, setOpen] = useState(false)
6 |
7 | const toggle = () => setOpen(!open)
8 |
9 | useEffect(() => {
10 | const handleClickOutside = (e: MouseEvent) => {
11 | if (!ref.current.contains(e.target as HTMLDivElement)) {
12 | setOpen(false)
13 | }
14 | }
15 |
16 | document.addEventListener('mousedown', handleClickOutside)
17 |
18 | return () => document.removeEventListener('mousedown', handleClickOutside)
19 | }, [ref])
20 |
21 | return { open, setOpen, toggle, ref }
22 | }
23 |
--------------------------------------------------------------------------------
/hooks/useComparisonItems.ts:
--------------------------------------------------------------------------------
1 | import { $comparison, $comparisonFromLs } from '@/context/comparison/state'
2 | import { useGoodsByAuth } from './useGoodsByAuth'
3 |
4 | export const useComparisonItems = (type: string) => {
5 | const currentComparisonByAuth = useGoodsByAuth($comparison, $comparisonFromLs)
6 | const items = currentComparisonByAuth.filter(
7 | (item) => item.characteristics.type === type
8 | )
9 |
10 | return { items }
11 | }
12 |
--------------------------------------------------------------------------------
/hooks/useComparisonLinks.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { useUnit } from 'effector-react'
3 | import { usePathname } from 'next/navigation'
4 | import { getComparisonItemsFx } from '@/context/comparison'
5 | import { useGoodsByAuth } from './useGoodsByAuth'
6 | import { useLang } from './useLang'
7 | import { $comparison, $comparisonFromLs } from '@/context/comparison/state'
8 |
9 | export const useComparisonLinks = () => {
10 | const currentComparisonByAuth = useGoodsByAuth($comparison, $comparisonFromLs)
11 | const spinner = useUnit(getComparisonItemsFx.pending)
12 | const { lang, translations } = useLang()
13 | const pathname = usePathname()
14 |
15 | const availableProductLinks = useMemo(
16 | () =>
17 | [
18 | ...new Set(
19 | currentComparisonByAuth.map((item) => item.characteristics.type)
20 | ),
21 | ].map((type) => ({
22 | title: (translations[lang].comparison as { [index: string]: string })[
23 | type
24 | ],
25 | href: `/comparison/${type}`,
26 | itemsCount: currentComparisonByAuth.filter(
27 | (item) => item.characteristics.type === type
28 | ).length,
29 | isActive: pathname.split('/comparison/')[1] === type,
30 | })),
31 | [currentComparisonByAuth, lang, pathname, translations]
32 | )
33 |
34 | return { availableProductLinks, linksSpinner: spinner }
35 | }
36 |
--------------------------------------------------------------------------------
/hooks/useCrumbText.ts:
--------------------------------------------------------------------------------
1 | import { useLang } from './useLang'
2 |
3 | export const useCrumbText = (initialText: string) => {
4 | const { lang, translations } = useLang()
5 | const crumbText = (
6 | translations[lang].breadcrumbs as { [index: string]: string }
7 | )[initialText]
8 |
9 | return { crumbText }
10 | }
11 |
--------------------------------------------------------------------------------
/hooks/useDebounceCallback.ts:
--------------------------------------------------------------------------------
1 | import { MutableRefObject, useEffect, useRef } from 'react'
2 |
3 | export const useDebounceCallback = (delay = 100) => {
4 | const timerRef = useRef() as MutableRefObject
5 |
6 | useEffect(() => clearTimeout(timerRef.current), [])
7 |
8 | return (callback: VoidFunction) => {
9 | clearTimeout(timerRef.current)
10 | timerRef.current = setTimeout(callback, delay)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/hooks/useGoodsByAuth.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { UseGoodsByAuth } from '@/types/common'
3 | import { $isAuth } from '@/context/auth/state'
4 |
5 | export const useGoodsByAuth = (
6 | storeAsync: UseGoodsByAuth,
7 | storeSync: UseGoodsByAuth
8 | ) => {
9 | const goods = useUnit(storeAsync)
10 | const isAuth = useUnit($isAuth)
11 | const goodsFromLS = useUnit(storeSync)
12 | const currentFavoritesByAuth = isAuth ? goods : goodsFromLS
13 |
14 | return currentFavoritesByAuth
15 | }
16 |
--------------------------------------------------------------------------------
/hooks/useImagePreloader.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | const useImagePreloader = () => {
4 | const [imgSpinner, setImgSpinner] = useState(true)
5 |
6 | const handleLoadingImageComplete = async (
7 | img: React.SyntheticEvent
8 | ) => {
9 | img.currentTarget.classList.remove('opacity-0')
10 | setImgSpinner(false)
11 | }
12 |
13 | return { handleLoadingImageComplete, imgSpinner }
14 | }
15 |
16 | export default useImagePreloader
17 |
--------------------------------------------------------------------------------
/hooks/useLang.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useUnit } from 'effector-react'
3 | import translationsJson from '@/public/translations/translations.json'
4 | import { $lang } from '@/context/lang/state'
5 |
6 | export const useLang = () => {
7 | const lang = useUnit($lang)
8 | const translations = translationsJson
9 |
10 | return { lang, translations }
11 | }
12 |
--------------------------------------------------------------------------------
/hooks/useLogout.ts:
--------------------------------------------------------------------------------
1 | import { useEarthoOne } from '@eartho/one-client-react'
2 | import { useRouter } from 'next/navigation'
3 | import { setIsAuth } from '@/context/auth'
4 |
5 | export const useUserLogout = () => {
6 | const router = useRouter()
7 | const { logout } = useEarthoOne()
8 |
9 | return () => {
10 | logout({ clientId: `${process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID}` })
11 | localStorage.removeItem('auth')
12 | setIsAuth(false)
13 | router.push('/')
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/hooks/useMediaQuery.ts:
--------------------------------------------------------------------------------
1 | import { getWindowWidth } from '@/lib/utils/common'
2 | import { useEffect, useState } from 'react'
3 |
4 | const useWindowWidth = () => {
5 | const [windowWidth, setWindowWidth] = useState(getWindowWidth())
6 |
7 | const handleResize = () => setWindowWidth(getWindowWidth())
8 |
9 | useEffect(() => {
10 | window.addEventListener('resize', handleResize, true)
11 |
12 | return () => window.removeEventListener('resize', handleResize, true)
13 | }, [])
14 |
15 | return { windowWidth, handleResize }
16 | }
17 |
18 | export const useMediaQuery = (maxWidth: number) => {
19 | const {
20 | windowWidth: { windowWidth },
21 | handleResize,
22 | } = useWindowWidth()
23 | const [isMedia, setIsMedia] = useState(false)
24 |
25 | useEffect(() => {
26 | if (windowWidth <= maxWidth) {
27 | setIsMedia(true)
28 | } else {
29 | setIsMedia(false)
30 | }
31 | }, [handleResize, maxWidth, windowWidth])
32 |
33 | return isMedia
34 | }
35 |
--------------------------------------------------------------------------------
/hooks/useMenuAnimation.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const useMenuAnimation = (zIndex: number, popupIsOpen: boolean) => {
4 | const [popupZIndex, setPopupZIndex] = useState(0)
5 |
6 | const itemVariants = {
7 | closed: {
8 | opacity: 0,
9 | },
10 | open: { opacity: 1 },
11 | }
12 |
13 | const sideVariants = {
14 | closed: {
15 | transition: {
16 | staggerChildren: 0.05,
17 | staggerDirection: -1,
18 | },
19 | },
20 | open: {
21 | transition: {
22 | staggerChildren: 0.2,
23 | staggerDirection: 1,
24 | },
25 | },
26 | }
27 |
28 | useEffect(() => {
29 | if (popupIsOpen) {
30 | setPopupZIndex(zIndex)
31 | return
32 | }
33 |
34 | const timerId = setTimeout(() => setPopupZIndex('-1'), 1000)
35 |
36 | return () => clearTimeout(timerId)
37 | }, [popupIsOpen, zIndex])
38 |
39 | return { popupZIndex, itemVariants, sideVariants }
40 | }
41 |
--------------------------------------------------------------------------------
/hooks/usePageTitle.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useLang } from './useLang'
3 |
4 | export const usePageTitle = (page: string, additionalText = '') => {
5 | const { lang, translations } = useLang()
6 |
7 | useEffect(() => {
8 | document.title = `${lang === 'ru' ? 'Ростелеком' : 'Rostelecom'} | ${
9 | (translations[lang].breadcrumbs as { [index: string]: string })[page]
10 | }${additionalText ? ` - ${additionalText}` : ''}`
11 | }, [additionalText, lang, page, translations])
12 | }
13 |
--------------------------------------------------------------------------------
/hooks/usePricceAnimation.ts:
--------------------------------------------------------------------------------
1 | import { animate } from 'framer-motion'
2 | import { useEffect, useState } from 'react'
3 |
4 | export const usePriceAnimation = (initialFrom: number, initialTo: number) => {
5 | const [from, setFrom] = useState(initialFrom)
6 | const [to, setTo] = useState(initialTo)
7 | const [value, setValue] = useState(0)
8 |
9 | useEffect(() => {
10 | const controls = animate(from, to, {
11 | duration: 0.5,
12 | onUpdate(value) {
13 | setValue(+value.toFixed(0))
14 | },
15 | })
16 |
17 | return () => controls.stop()
18 | }, [from, to])
19 |
20 | return { setFrom, setTo, value }
21 | }
22 |
--------------------------------------------------------------------------------
/hooks/usePriceAction.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const usePriceAction = (count: number, initialPrice: number) => {
4 | const [price, setPrice] = useState(initialPrice)
5 |
6 | useEffect(() => {
7 | setPrice(price * count)
8 | }, [])
9 |
10 | const increasePrice = () => setPrice(price + initialPrice)
11 | const decreasePrice = () => setPrice(price - initialPrice)
12 |
13 | return { price, increasePrice, decreasePrice }
14 | }
15 |
--------------------------------------------------------------------------------
/hooks/useProductDelete.ts:
--------------------------------------------------------------------------------
1 | import { EventCallable } from 'effector'
2 | import { useState } from 'react'
3 | import { IBaseEffectProps } from '@/types/common'
4 |
5 | export const useProductDelete = (
6 | id: string,
7 | deleteEvent: EventCallable
8 | ) => {
9 | const [deleteSpinner, setDeleteSpinner] = useState(false)
10 |
11 | const handleDelete = () => {
12 | const auth = JSON.parse(localStorage.getItem('auth') as string)
13 |
14 | deleteEvent({
15 | setSpinner: setDeleteSpinner,
16 | jwt: auth.accessToken,
17 | id,
18 | })
19 | }
20 |
21 | return { handleDelete, deleteSpinner }
22 | }
23 |
--------------------------------------------------------------------------------
/hooks/useProductImages.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { idGenerator } from '@/lib/utils/common'
3 | import { IProduct } from '@/types/common'
4 |
5 | export const useProductImages = (product: IProduct) => {
6 | const images = useMemo(() => {
7 | const makeImagesObjects = (imagesArray: string[]) =>
8 | imagesArray.map((item) => ({
9 | src: item,
10 | alt: product.name,
11 | id: idGenerator(),
12 | }))
13 |
14 | if (product.images.length < 4) {
15 | const images = []
16 |
17 | for (let i = 0; i < 4; i++) {
18 | images.push(product.images[0])
19 | }
20 |
21 | return makeImagesObjects(images)
22 | }
23 |
24 | return makeImagesObjects(product.images)
25 | }, [product.images, product.name])
26 |
27 | return images
28 | }
29 |
--------------------------------------------------------------------------------
/hooks/useProductsByCollection.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable prettier/prettier */
2 | import { capitalizeFirstLetter } from '@/lib/utils/common'
3 | import { useLang } from './useLang'
4 | import { loadProductsByFilterFx } from '@/context/goods'
5 | import { $products } from '@/context/goods/state'
6 | import { useUnit } from 'effector-react'
7 |
8 | export const useProductsByCollection = (collection: string) => {
9 | const products = useUnit($products)
10 | const spinner = useUnit(loadProductsByFilterFx.pending)
11 | const { lang, translations } = useLang()
12 | const langText = translations[lang].product.collection_goods
13 | const capitalizedCollection = capitalizeFirstLetter(collection)
14 | const title =
15 | lang === 'ru'
16 | ? `${langText} «${capitalizedCollection}»`
17 | : [
18 | langText.slice(0, 17),
19 | ` «${capitalizedCollection}»`,
20 | langText.slice(17),
21 | ].join('')
22 |
23 | return { title, capitalizedCollection, products, spinner }
24 | }
25 |
--------------------------------------------------------------------------------
/hooks/useTTMap.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { $mapInstance } from '@/context/order/state'
3 | import { IAddressBBox, IAddressPosition } from '@/types/order'
4 |
5 | export const useTTMap = () => {
6 | const mapInstance = useUnit($mapInstance)
7 |
8 | const handleSelectAddress = async (
9 | { lon1, lat1, lon2, lat2 }: IAddressBBox,
10 | position: IAddressPosition,
11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
12 | initialMapInstance?: any
13 | ) => {
14 | const ttMaps = await import(`@tomtom-international/web-sdk-maps`)
15 | const currentMap = initialMapInstance || mapInstance
16 |
17 | const sw = new ttMaps.LngLat(lon1, lat1)
18 | const ne = new ttMaps.LngLat(lon2, lat2)
19 | const bounds = new ttMaps.LngLatBounds(sw, ne)
20 |
21 | currentMap.fitBounds(bounds, { padding: 130, linear: true })
22 |
23 | const element = document.createElement('div')
24 | element.classList.add('map-marker')
25 |
26 | new ttMaps.Marker({ element })
27 | .setLngLat([position.lon, position.lat])
28 | .addTo(currentMap)
29 | }
30 |
31 | return { handleSelectAddress }
32 | }
33 |
--------------------------------------------------------------------------------
/hooks/useTotalPrice.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useUnit } from 'effector-react'
3 | import { setTotalPrice } from '@/context/cart'
4 | import { usePriceAnimation } from './usePricceAnimation'
5 | import { useGoodsByAuth } from './useGoodsByAuth'
6 | import { $totalPrice, $cart, $cartFromLs } from '@/context/cart/state'
7 |
8 | export const useTotalPrice = () => {
9 | const totalPrice = useUnit($totalPrice)
10 | const currentCartByAuth = useGoodsByAuth($cart, $cartFromLs)
11 |
12 | const getNewTotal = () =>
13 | currentCartByAuth
14 | .map((item) => +item.price * +item.count)
15 | .reduce((defaultCount, item) => defaultCount + item, 0)
16 |
17 | const {
18 | value: animatedPrice,
19 | setFrom,
20 | setTo,
21 | } = usePriceAnimation(totalPrice, getNewTotal())
22 |
23 | useEffect(() => {
24 | setTotalPrice(getNewTotal())
25 | setFrom(totalPrice)
26 | setTo(getNewTotal())
27 | }, [currentCartByAuth])
28 |
29 | return { animatedPrice }
30 | }
31 |
--------------------------------------------------------------------------------
/hooks/useUserAvatar.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { useState, useEffect } from 'react'
3 | import { $user } from '@/context/user/state'
4 |
5 | export const useUserAvatar = () => {
6 | const user = useUnit($user)
7 | const [src, setSrc] = useState('')
8 |
9 | useEffect(() => {
10 | if (user.image) {
11 | setSrc(user.image)
12 | return
13 | }
14 |
15 | const oauthAvatar = JSON.parse(
16 | localStorage.getItem(
17 | '@@oneclientjs@@::l3Q4jO58IChQRwUkzkHI::@@user@@'
18 | ) as string
19 | )
20 |
21 | if (!oauthAvatar) {
22 | return
23 | }
24 |
25 | setSrc(oauthAvatar.decodedToken.user.photoURL)
26 | }, [user.image])
27 |
28 | return { src, alt: user.name }
29 | }
30 |
--------------------------------------------------------------------------------
/hooks/useWatchedProducts.ts:
--------------------------------------------------------------------------------
1 | import { useUnit } from 'effector-react'
2 | import { useEffect } from 'react'
3 | import { loadWatchedProducts } from '@/context/goods'
4 | import { $watchedProducts } from '@/context/goods/state'
5 | import { getWatchedProductFromLS } from '@/lib/utils/common'
6 |
7 | export const useWatchedProducts = (excludedProductId?: string) => {
8 | const watchedProducts = useUnit($watchedProducts)
9 |
10 | useEffect(() => {
11 | const watchedProducts = getWatchedProductFromLS()
12 |
13 | loadWatchedProducts({
14 | payload: excludedProductId
15 | ? watchedProducts.filter((item) => item._id !== excludedProductId)
16 | : watchedProducts,
17 | })
18 | }, [excludedProductId])
19 |
20 | return { watchedProducts }
21 | }
22 |
--------------------------------------------------------------------------------
/lib/mongodb/index.ts:
--------------------------------------------------------------------------------
1 | import { MongoClient } from 'mongodb'
2 |
3 | const clientPromise = MongoClient.connect(
4 | process.env.NEXT_PUBLIC_DB_URL as string,
5 | {
6 | maxPoolSize: 10,
7 | }
8 | )
9 |
10 | export default clientPromise
11 |
--------------------------------------------------------------------------------
/lib/utils/auth.ts:
--------------------------------------------------------------------------------
1 | import toast from 'react-hot-toast'
2 | import { handleCloseAuthPopup } from './common'
3 | import { setIsAuth } from '@/context/auth'
4 |
5 | export const onAuthSuccess = (message: string, data: T) => {
6 | localStorage.setItem('auth', JSON.stringify(data))
7 | toast.success(message)
8 | handleCloseAuthPopup()
9 | setIsAuth(true)
10 | }
11 |
12 | export const nameValidationRules = (
13 | message: string,
14 | requireMessage?: string
15 | ) => ({
16 | ...(requireMessage && { required: requireMessage }),
17 | pattern: {
18 | value: /^[а-яА-Яa-zA-ZёЁ]*$/,
19 | message,
20 | },
21 | minLength: 2,
22 | maxLength: 15,
23 | })
24 |
25 | export const emailValidationRules = (
26 | message: string,
27 | requireMessage?: string
28 | ) => ({
29 | ...(requireMessage && { required: requireMessage }),
30 | pattern: {
31 | value: /\S+@\S+\.\S+/,
32 | message,
33 | },
34 | })
35 |
36 | export const phoneValidationRules = (
37 | message: string,
38 | requireMessage?: string
39 | ) => ({
40 | ...(requireMessage && { required: requireMessage }),
41 | pattern: {
42 | value: /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/im,
43 | message,
44 | },
45 | })
46 |
--------------------------------------------------------------------------------
/lib/utils/catalog.ts:
--------------------------------------------------------------------------------
1 | export const getCheckedPriceFrom = (price: number) =>
2 | +price > 10000 ? '5000' : price
3 |
4 | export const getCheckedPriceTo = (price: number) =>
5 | +price > 10000 ? '10000' : price
6 |
--------------------------------------------------------------------------------
/migrations/20240127125424-users.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | async up(db) {
3 | db.createCollection('users')
4 | },
5 |
6 | async down(db) {
7 | db.collection('users').drop()
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/migrations/20240129162135-cart.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | async up(db) {
3 | db.createCollection('cart')
4 | },
5 |
6 | async down(db) {
7 | db.collection('cart').drop()
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/migrations/20240223160629-favorites.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | async up(db) {
3 | db.createCollection('favorites')
4 | },
5 |
6 | async down(db) {
7 | db.collection('favorites').drop()
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/migrations/20240313172721-comparison.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | async up(db) {
3 | db.createCollection('comparison')
4 | },
5 |
6 | async down(db) {
7 | db.collection('comparison').drop()
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/migrations/20240611155233-codes.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | async up(db) {
3 | db.createCollection('codes')
4 | },
5 |
6 | async down(db) {
7 | db.collection('codes').drop()
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | { protocol: 'https', hostname: 'lh3.googleusercontent.com' },
6 | ],
7 | },
8 | }
9 |
10 | module.exports = nextConfig
11 |
--------------------------------------------------------------------------------
/public/avatars/666c7b0c7bd6fa2fb8dc03e8__comet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/avatars/666c7b0c7bd6fa2fb8dc03e8__comet.png
--------------------------------------------------------------------------------
/public/avatars/6671bb69b58fa0e16fcf40e7__nestjs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/avatars/6671bb69b58fa0e16fcf40e7__nestjs.png
--------------------------------------------------------------------------------
/public/fonts/Rostelecom-Basis-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/fonts/Rostelecom-Basis-Bold.woff
--------------------------------------------------------------------------------
/public/fonts/Rostelecom-Basis-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/fonts/Rostelecom-Basis-Medium.woff
--------------------------------------------------------------------------------
/public/fonts/Rostelecom-Basis-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/fonts/Rostelecom-Basis-Regular.woff
--------------------------------------------------------------------------------
/public/img/accessories/accessories-bags-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/accessories/accessories-bags-1.png
--------------------------------------------------------------------------------
/public/img/accessories/accessories-bags-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/accessories/accessories-bags-2.png
--------------------------------------------------------------------------------
/public/img/accessories/accessories-bags-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/accessories/accessories-bags-3.png
--------------------------------------------------------------------------------
/public/img/accessories/accessories-bags-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/accessories/accessories-bags-4.png
--------------------------------------------------------------------------------
/public/img/accessories/accessories-headdress.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/accessories/accessories-headdress.png
--------------------------------------------------------------------------------
/public/img/accessories/accessories-umbrella.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/accessories/accessories-umbrella.png
--------------------------------------------------------------------------------
/public/img/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/black-t.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/black-t.png
--------------------------------------------------------------------------------
/public/img/brands-life.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/brands-life.png
--------------------------------------------------------------------------------
/public/img/burger.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/cart-checked.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/img/cart-plus.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/img/cart.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/catalog.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/categories-img-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/categories-img-1.png
--------------------------------------------------------------------------------
/public/img/categories-img-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/categories-img-2.png
--------------------------------------------------------------------------------
/public/img/categories-img-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/categories-img-3.png
--------------------------------------------------------------------------------
/public/img/categories-img-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/categories-img-4.png
--------------------------------------------------------------------------------
/public/img/category-filter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/checked-favorite.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/checked.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/close-small.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/clothes/cloth-hoodie-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/clothes/cloth-hoodie-1.png
--------------------------------------------------------------------------------
/public/img/clothes/cloth-long-sleeves-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/clothes/cloth-long-sleeves-1.png
--------------------------------------------------------------------------------
/public/img/clothes/cloth-long-sleeves-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/clothes/cloth-long-sleeves-2.png
--------------------------------------------------------------------------------
/public/img/clothes/cloth-outerwear-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/clothes/cloth-outerwear-1.png
--------------------------------------------------------------------------------
/public/img/clothes/cloth-outerwear-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/clothes/cloth-outerwear-2.png
--------------------------------------------------------------------------------
/public/img/clothes/cloth-t-shirts-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/clothes/cloth-t-shirts-1.png
--------------------------------------------------------------------------------
/public/img/clothes/cloth-t-shirts-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/clothes/cloth-t-shirts-2.png
--------------------------------------------------------------------------------
/public/img/comparison-checked.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/public/img/comparison-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/comparison-icon.png
--------------------------------------------------------------------------------
/public/img/comparison.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/public/img/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/empty-cart-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/empty-cart-page.png
--------------------------------------------------------------------------------
/public/img/empty-comparison-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/empty-comparison-page.png
--------------------------------------------------------------------------------
/public/img/favorites-empty-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/favorites-empty-page.png
--------------------------------------------------------------------------------
/public/img/favorites.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/filters.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/gray-ellipse.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/green-ellipse.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/home.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/info-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/map-marker.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/public/img/menu-bg-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/menu-bg-small.png
--------------------------------------------------------------------------------
/public/img/menu-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/menu-bg.png
--------------------------------------------------------------------------------
/public/img/menu-line-small.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/public/img/menu-line-tiny.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/img/menu-line.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/public/img/minus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/mir-logo.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/public/img/more.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/not-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/not-found.png
--------------------------------------------------------------------------------
/public/img/office/office-notebook-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/office/office-notebook-1.png
--------------------------------------------------------------------------------
/public/img/office/office-notebook-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/office/office-notebook-2.png
--------------------------------------------------------------------------------
/public/img/office/office-pen-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/office/office-pen-1.png
--------------------------------------------------------------------------------
/public/img/office/office-pen-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/office/office-pen-2.png
--------------------------------------------------------------------------------
/public/img/orange-t.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/orange-t.png
--------------------------------------------------------------------------------
/public/img/plus-big.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/plus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/popup-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/profile.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/quick-view.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/red-ellipse.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/save-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/search-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/search.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/select-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/simple-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/slider-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/sort.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/souvenirs/business-souvenirs-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/souvenirs/business-souvenirs-1.png
--------------------------------------------------------------------------------
/public/img/souvenirs/business-souvenirs-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/souvenirs/business-souvenirs-2.png
--------------------------------------------------------------------------------
/public/img/souvenirs/promotional-souvenirs-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/souvenirs/promotional-souvenirs-1.png
--------------------------------------------------------------------------------
/public/img/souvenirs/promotional-souvenirs-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/souvenirs/promotional-souvenirs-2.png
--------------------------------------------------------------------------------
/public/img/subtitle-rect.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/img/telegram.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/trash.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/violet-t.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/violet-t.png
--------------------------------------------------------------------------------
/public/img/white-ellipse.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/img/ytb.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/service/mailService.js:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer'
2 |
3 | export async function sendMail(subject, toEmail, otpText) {
4 | const transporter = nodemailer.createTransport({
5 | service: 'gmail',
6 | auth: {
7 | user: process.env.NEXT_PUBLIC_NODEMAILER_EMAIL,
8 | pass: process.env.NEXT_PUBLIC_NODEMAILER_PW,
9 | },
10 | })
11 |
12 | const mailOptions = {
13 | from: process.env.NEXT_PUBLIC_NODEMAILER_EMAIL,
14 | to: toEmail,
15 | subject: subject,
16 | text: otpText,
17 | }
18 |
19 | await new Promise((resolve, reject) => {
20 | transporter.sendMail(mailOptions, (err, response) => {
21 | if (err) {
22 | reject(err)
23 | } else {
24 | resolve(response)
25 | }
26 | })
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/styles/ad/index.module.scss:
--------------------------------------------------------------------------------
1 | .ad {
2 | position: absolute;
3 | top: -80px;
4 | right: 15px;
5 | padding: 2px 4px;
6 | padding-right: 27px;
7 | height: 28px;
8 | line-height: 24px;
9 | background-image: url('/img/info-icon.svg');
10 | background-repeat: no-repeat;
11 | background-position: right 2px center;
12 | color: #A5A8AD;
13 | font-size: 14px;
14 | font-weight: 500;
15 | border-radius: 30px;
16 | background-color: rgba(16, 24, 40, 0.40);
17 |
18 | @media (max-width: 760px) {
19 | top: -20px;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/styles/auth-popup/index.module.scss:
--------------------------------------------------------------------------------
1 | .error_alert {
2 | font-size: 12px;
3 | color: red !important;
4 | position: absolute;
5 | left: 10px;
6 | bottom: -23px;
7 | }
8 |
--------------------------------------------------------------------------------
/styles/cart-skeleton/index.module.scss:
--------------------------------------------------------------------------------
1 | .skeleton {
2 | width: 100%;
3 | list-style-type: none;
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | .skeleton__item {
9 | background-color: #2A323F;
10 | overflow: hidden !important;
11 | padding: 0;
12 | height: 185px;
13 | margin-bottom: 25px;
14 |
15 | @media (max-width: 530px) {
16 | height: 145px;
17 | margin-bottom: 12px;
18 | }
19 | }
20 |
21 | .skeleton__item__light {
22 | height: 100%;
23 | animation-duration: 1.6s;
24 | background: linear-gradient(90deg, #333a47 25%, #464f61 50%, #333A47 75%);
25 | background-size: 200% 100%;
26 | animation: skeleton 2s infinite ease-in-out;
27 | }
28 |
29 |
30 | @keyframes skeleton {
31 | 0% {
32 | background-position: 200% 0;
33 | }
34 | 100% {
35 | background-position: -200% 0;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/styles/comparison-links-skeleton/index.module.scss:
--------------------------------------------------------------------------------
1 | .skeleton {
2 | display: flex;
3 | align-items: start;
4 | margin-bottom: 40px;
5 | overflow-x: auto;
6 | padding-bottom: 10px;
7 | }
8 |
9 | .skeleton::-webkit-scrollbar {
10 | height: 8px;
11 | }
12 |
13 | .skeleton::-webkit-scrollbar-track {
14 | background-color: transparent;
15 | }
16 |
17 | .skeleton::-webkit-scrollbar-thumb {
18 | background-color: #9466FF;
19 | border-radius: 3px;
20 | }
21 |
22 | .skeleton::-webkit-scrollbar-thumb:hover {
23 | background-color: #70F;
24 | }
25 |
26 | .skeleton__item {
27 | background-color: #2A323F;
28 | overflow: hidden !important;
29 | padding: 0;
30 | height: 48px;
31 | min-width: 150px;
32 | }
33 |
34 | .skeleton__item:not(:last-child) {
35 | margin-right: 10px;
36 | }
37 |
38 | .skeleton__item__light {
39 | height: 100%;
40 | animation-duration: 1.6s;
41 | background: linear-gradient(270deg, #ffffff0c 34.78%, rgba(255, 255, 255, 0.06) 60.33%, #ffffff0c 84.84%);
42 | animation-name: skeleton;
43 | animation-timing-function: linear;
44 | animation-iteration-count: infinite;
45 | }
46 |
47 |
48 | @keyframes skeleton {
49 | 0% {transform: translateX(-600px)}
50 | 100% {transform: translateX(700px)}
51 | }
52 |
--------------------------------------------------------------------------------
/styles/comparison-list-skeleton/index.module.scss:
--------------------------------------------------------------------------------
1 | .skeleton {
2 | display: flex;
3 | align-items: start;
4 | margin-bottom: 40px;
5 | overflow-x: auto;
6 | padding-bottom: 10px;
7 | }
8 |
9 | .skeleton::-webkit-scrollbar {
10 | height: 8px;
11 | }
12 |
13 | .skeleton::-webkit-scrollbar-track {
14 | background-color: transparent;
15 | }
16 |
17 | .skeleton::-webkit-scrollbar-thumb {
18 | background-color: #9466FF;
19 | border-radius: 3px;
20 | }
21 |
22 | .skeleton::-webkit-scrollbar-thumb:hover {
23 | background-color: #70F;
24 | }
25 |
26 | .skeleton__item {
27 | background-color: #2A323F;
28 | overflow: hidden !important;
29 | padding: 0;
30 | height: 600px;
31 | min-width: 478px;
32 |
33 | @media (max-width: 730px) {
34 | min-width: 289px;
35 | width: 289px;
36 | }
37 | }
38 |
39 | .skeleton__item:not(:last-child) {
40 | margin-right: 32px;
41 |
42 | @media (max-width: 730px) {
43 | margin-right: 16px;
44 | }
45 | }
46 |
47 | .skeleton__item__light {
48 | height: 100%;
49 | animation-duration: 1.6s;
50 | background: linear-gradient(270deg, #ffffff0c 34.78%, rgba(255, 255, 255, 0.06) 60.33%, #ffffff0c 84.84%);
51 | animation-name: skeleton;
52 | animation-timing-function: linear;
53 | animation-iteration-count: infinite;
54 | }
55 |
56 |
57 | @keyframes skeleton {
58 | 0% {transform: translateX(-600px)}
59 | 100% {transform: translateX(700px)}
60 | }
61 |
--------------------------------------------------------------------------------
/styles/comparison-skeleton/index.module.scss:
--------------------------------------------------------------------------------
1 | .skeleton {
2 | width: 100%;
3 | list-style-type: none;
4 | margin: 0;
5 | padding: 0;
6 | display: grid;
7 | grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
8 | grid-gap: 32px;
9 | }
10 |
11 | .skeleton__item {
12 | background-color: #2A323F;
13 | overflow: hidden !important;
14 | padding: 0;
15 | height: 300px;
16 | }
17 |
18 | .skeleton__item__light {
19 | height: 100%;
20 | animation-duration: 1.6s;
21 | background: linear-gradient(270deg, #ffffff0c 34.78%, rgba(255, 255, 255, 0.06) 60.33%, #ffffff0c 84.84%);
22 | animation-name: skeleton;
23 | animation-timing-function: linear;
24 | animation-iteration-count: infinite;
25 | }
26 |
27 |
28 | @keyframes skeleton {
29 | 0% {transform: translateX(-600px)}
30 | 100% {transform: translateX(700px)}
31 | }
32 |
--------------------------------------------------------------------------------
/styles/heading-with-count/index.module.scss:
--------------------------------------------------------------------------------
1 | .title {
2 | position: relative;
3 | display: inline-block;
4 | color: #F1F3F5;
5 |
6 | @media (max-width: 530px) {
7 | font-size: 24px;
8 | margin-bottom: 32px;
9 | }
10 |
11 | &__count {
12 | position: absolute;
13 | top: 0;
14 | right: -80px;
15 | color: #A5A8AD;
16 | font-size: 14px;
17 | font-weight: 400;
18 |
19 | @media (max-width: 560px) {
20 | top: -10px;
21 | }
22 |
23 | @media (max-width: 530px) {
24 | font-size: 13px;
25 | right: -73px;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/styles/not-found/index.module.scss:
--------------------------------------------------------------------------------
1 | .not_found {
2 | padding-top: 80px;
3 | padding-bottom: 80px;
4 |
5 | @media (max-width: 950px) {
6 | padding-top: 5px;
7 | }
8 |
9 | &_bg {
10 | text-align: center;
11 | left: 74% !important;
12 | line-height: 87%;
13 |
14 | @media (max-width: 1660px) {
15 | left: 74% !important;
16 | font-size: 300px !important;
17 | }
18 |
19 | @media (max-width: 1365px) {
20 | left: 65% !important;
21 | font-size: 280px !important;
22 | }
23 |
24 | @media (max-width: 950px) {
25 | left: 50% !important;
26 | font-size: 160px !important;
27 | top: 90% !important;
28 | }
29 |
30 | @media (max-width: 390px) {
31 | font-size: 130px !important;
32 | top: 95% !important;
33 | }
34 | }
35 | }
36 |
37 | .empty_bg {
38 | background-image: url('/img/not-found.png');
39 | }
40 |
--------------------------------------------------------------------------------
/styles/policy/index.module.scss:
--------------------------------------------------------------------------------
1 | .policy {
2 | h1, h3 {
3 | color: #9466ff;
4 | }
5 |
6 | a {
7 | color: #9466ff;
8 | text-decoration: none;
9 | }
10 |
11 | p {
12 | font-size: 18px;
13 | }
14 |
15 | strong {
16 | color: #9466ff;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/styles/product-count-indicator/index.module.scss:
--------------------------------------------------------------------------------
1 | .count {
2 | position: absolute;
3 | top: -6px;
4 | right: -4px;
5 | width: 20px;
6 | text-align: center;
7 | height: 20px;
8 | background-color: #FF4747;
9 | border-radius: 50%;
10 | color: #E8E9EA;
11 | font-size: 14px;
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 |
16 | &.with_icon {
17 | width: 50px;
18 | height: 25px;
19 | padding-right: 8px;
20 | padding-left: 5px;
21 | border-radius: 4px;
22 | top: 7px;
23 | right: -20px;
24 | }
25 |
26 | &.with_icon span {
27 | width: 100%;
28 | height: 100%;
29 | display: flex;
30 | align-items: center;
31 | justify-content: end;
32 | background-image: url('/img/cart.svg');
33 | background-repeat: no-repeat;
34 | background-position: left center;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/styles/share-modal/index.module.scss:
--------------------------------------------------------------------------------
1 | .share_modal {
2 | background: #333A47;
3 | border-radius: 4px;
4 | -webkit-box-shadow: 0 2px 8px 1px rgba(0, 42, 81, 0.1);
5 | box-shadow: 0 2px 8px 1px rgba(0, 42, 81, 0.1);
6 | left: 50%;
7 | max-width: calc(100% - 30px);
8 | padding: 15px;
9 | position: fixed;
10 | top: 50%;
11 | -webkit-transform: translateX(-50%) translateY(-50%);
12 | -ms-transform: translateX(-50%) translateY(-50%);
13 | transform: translateX(-50%) translateY(-50%);
14 | width: 616px;
15 | z-index: 102;
16 |
17 | &__close {
18 | height: 15px;
19 | width: 15px;
20 | z-index: 1;
21 | position: absolute;
22 | right: 15px;
23 | top: 15px;
24 |
25 | &::before {
26 | background-color: #E8E9EA;
27 | content: '';
28 | height: 100%;
29 | width: 100%;
30 | left: 0;
31 | position: absolute;
32 | top: 0;
33 | transition: background-color 0.2s ease-in-out;
34 | -webkit-mask: url(/img/close-small.svg) no-repeat 50% 50%;
35 | mask: url(/img/close-small.svg) no-repeat 50% 50%;
36 | }
37 |
38 | &:hover::before {
39 | transition: background-color 0.2s ease-in-out;
40 | background-color: #9466FF;
41 | }
42 | }
43 |
44 | &__title {
45 | margin: 0;
46 | margin-bottom: 10px;
47 | color: #E8E9EA;
48 | font-size: 1.125rem;
49 | line-height: 30px;
50 | text-align: left;
51 | text-transform: uppercase;
52 | font-weight: 600;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/styles/skeleton/index.module.scss:
--------------------------------------------------------------------------------
1 | .skeleton {
2 | width: 100%;
3 | list-style-type: none;
4 | margin: 0;
5 | padding: 0;
6 | display: grid;
7 | grid-template-columns: repeat(4, 1fr);
8 | grid-gap: 32px;
9 |
10 | @media (max-width: 1150px) {
11 | grid-template-columns: 1fr 1fr;
12 | grid-template-rows: 1fr 1fr;
13 | }
14 |
15 | @media (max-width: 800px) {
16 | grid-gap: 24px;
17 | }
18 |
19 | @media (max-width: 620px) {
20 | grid-gap: 18px;
21 | }
22 |
23 | @media (max-width: 460px) {
24 | grid-template-columns: 1fr;
25 | }
26 | }
27 |
28 | .skeleton__item {
29 | background-color: #2A323F;
30 | overflow: hidden !important;
31 | padding: 0;
32 | height: 485px;
33 | border-radius: 4px;
34 |
35 | @media (max-width: 800px) {
36 | height: 450px;
37 | }
38 |
39 | @media (max-width: 460px) {
40 | height: 350px;
41 | }
42 | }
43 |
44 | .skeleton__item__light {
45 | height: 100%;
46 | animation-duration: 1.6s;
47 | background: linear-gradient(270deg, #ffffff0c 34.78%, rgba(255, 255, 255, 0.06) 60.33%, #ffffff0c 84.84%);
48 | animation-name: skeleton;
49 | animation-timing-function: linear;
50 | animation-iteration-count: infinite;
51 | }
52 |
53 |
54 | @keyframes skeleton {
55 | 0% {transform: translateX(-600px)}
56 | 100% {transform: translateX(700px)}
57 | }
58 |
--------------------------------------------------------------------------------
/styles/tooltip/index.module.scss:
--------------------------------------------------------------------------------
1 | .tooltip {
2 | position: absolute;
3 | top: -6px;
4 | width: max-content;
5 | padding: 10px 12px;
6 | border-radius: 12px;
7 | background-color: #3D4555;
8 | z-index: 10;
9 |
10 | &__inner {
11 | position: relative;
12 |
13 | &::before {
14 | content: '';
15 | position: absolute;
16 | top: 5px;
17 | right: -30px;
18 | transform: rotate(90deg);
19 | height: 8px;
20 | width: 30px;
21 | background-image: url(/img/popup-arrow.svg);
22 | background-repeat: no-repeat;
23 | background-position: center center;
24 | }
25 | }
26 |
27 | &__text {
28 | color: rgba(255, 255, 255, 0.90);
29 | font-size: 14px;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/styles/watched-products-page/index.module.scss:
--------------------------------------------------------------------------------
1 | .watched_products {
2 | padding-top: 40px;
3 |
4 | &__list {
5 | display: grid;
6 | grid-template-columns: 1fr 1fr 1fr 1fr;
7 | grid-gap: 32px;
8 | width: 100%;
9 | margin-bottom: 60px;
10 |
11 | @media (max-width: 1150px) {
12 | grid-template-columns: 1fr 1fr;
13 | grid-template-rows: 1fr 1fr;
14 | }
15 |
16 | @media (max-width: 800px) {
17 | grid-gap: 24px;
18 | }
19 |
20 | @media (max-width: 620px) {
21 | grid-gap: 18px;
22 | }
23 |
24 | @media (max-width: 460px) {
25 | grid-template-columns: 1fr;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/styles/watched-products/index.module.scss:
--------------------------------------------------------------------------------
1 | .watched {
2 | padding-top: 40px;
3 | margin-bottom: 40px;
4 | position: relative;
5 |
6 | &__title {
7 | margin: 0;
8 | color: #F1F3F5;
9 | font-size: 42px;
10 | font-weight: 700;
11 | margin-bottom: 40px;
12 |
13 | @media (max-width: 650px) {
14 | font-size: 32px;
15 | }
16 |
17 | @media (max-width: 590px) {
18 | margin-bottom: 20px;
19 | font-size: 24px;
20 | max-width: 200px;
21 | }
22 | }
23 |
24 | &__inner {
25 | position: relative;
26 | }
27 |
28 | &__inner > a:first-child {
29 | right: 10px;
30 | }
31 |
32 | &__slider {
33 | margin-right: -15px;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "downlevelIteration": true,
11 | "module": "esnext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/types/authPopup.ts:
--------------------------------------------------------------------------------
1 | import { FieldErrors, FieldErrorsImpl, UseFormRegister } from 'react-hook-form'
2 |
3 | export interface IInputs {
4 | name: string
5 | email: string
6 | password: string
7 | }
8 |
9 | export interface ISignUpFx {
10 | password: string
11 | email: string
12 | isOAuth?: boolean
13 | name?: string
14 | }
15 |
16 | export interface IAuthSideProps {
17 | toggleAuth: VoidFunction
18 | isSideActive: boolean
19 | }
20 |
21 | export interface IAuthInput {
22 | register: UseFormRegister
23 | errors: Partial>
24 | }
25 |
26 | export interface INameErrorMessageProps {
27 | errors: FieldErrors
28 | fieldName: string
29 | className?: string
30 | }
31 |
--------------------------------------------------------------------------------
/types/cart.ts:
--------------------------------------------------------------------------------
1 | import { IBaseEffectProps } from './common'
2 |
3 | export interface ICartItem {
4 | _id: string
5 | clientId: string
6 | userId: string
7 | productId: string
8 | image: string
9 | name: string
10 | size: string
11 | count: string | number
12 | price: string
13 | totalPrice: string
14 | inStock: string
15 | color: string
16 | category: string
17 | }
18 |
19 | export interface IAddProductToCartFx {
20 | productId: string
21 | category: string
22 | size: string
23 | count: number
24 | jwt: string
25 | clientId: string
26 | setSpinner: (arg0: boolean) => void
27 | }
28 |
29 | export interface IAddProductsFromLSToCartFx {
30 | jwt: string
31 | cartItems: ICartItem[]
32 | }
33 |
34 | export interface IUpdateCartItemCountFx extends IBaseEffectProps {
35 | count: number
36 | }
37 |
38 | export interface IDeleteCartItemBtnProps {
39 | btnDisabled: boolean
40 | callback: VoidFunction
41 | className?: string
42 | }
43 |
44 | export type IDeleteCartItemsFx = IBaseEffectProps
45 |
--------------------------------------------------------------------------------
/types/common.ts:
--------------------------------------------------------------------------------
1 | import { StoreWritable } from 'effector'
2 |
3 | export interface IProduct {
4 | _id: string
5 | type: string
6 | category: string
7 | collection: string
8 | price: number
9 | name: string
10 | description: string
11 | characteristics: { [index: string]: string }
12 | images: string[]
13 | vendorCode: string
14 | inStock: string
15 | isBestseller: boolean
16 | isNew: boolean
17 | sizes: ISizes
18 | popularity: number
19 | errorMessage?: string
20 | }
21 |
22 | export interface ISizes {
23 | s: boolean
24 | l: boolean
25 | m: boolean
26 | xl: boolean
27 | xxl: boolean
28 | }
29 |
30 | export interface ISelectedSizes {
31 | sizes: ISizes
32 | type: string
33 | className?: string
34 | }
35 |
36 | export interface IBaseEffectProps {
37 | jwt: string
38 | id: string
39 | setSpinner: (arg0: boolean) => void
40 | }
41 |
42 | export type UseGoodsByAuth = StoreWritable
43 |
44 | export interface IGetGeolocationFx {
45 | lat: number
46 | lon: number
47 | }
48 |
--------------------------------------------------------------------------------
/types/comparison.ts:
--------------------------------------------------------------------------------
1 | import { IBaseEffectProps, ISizes } from './common'
2 |
3 | export interface IAddProductToComparisonFx {
4 | productId: string
5 | category: string
6 | jwt: string
7 | clientId: string
8 | setSpinner: (arg0: boolean) => void
9 | }
10 |
11 | export interface IComparisonItem {
12 | _id: string
13 | userId: string
14 | clientId: string
15 | productId: string
16 | image: string
17 | name: string
18 | sizes: ISizes
19 | size: string
20 | price: string
21 | category: string
22 | inStock: string
23 | characteristics: { [index: string]: string }
24 | }
25 |
26 | export interface IAddProductsFromLSToComparisonFx {
27 | jwt: string
28 | comparisonItems: IComparisonItem[]
29 | }
30 |
31 | export interface IComparisonLinksListProps {
32 | links: {
33 | href: string
34 | title: string
35 | itemsCount: number
36 | isActive: boolean
37 | }[]
38 | className?: string
39 | }
40 |
41 | export type IDeleteComparisonItemsFx = IBaseEffectProps
42 |
--------------------------------------------------------------------------------
/types/elements.ts:
--------------------------------------------------------------------------------
1 | import { CustomArrowProps } from 'react-slick'
2 |
3 | export interface IProductSubtitleProps {
4 | subtitleClassName?: string
5 | subtitleRectClassName?: string
6 | }
7 |
8 | export interface IProductItemActionBtnProps {
9 | text: string
10 | iconClass: string
11 | spinner?: boolean
12 | callback?: VoidFunction
13 | withTooltip?: boolean
14 | marginBottom?: number
15 | }
16 |
17 | export interface IProductAvailableProps {
18 | vendorCode: string
19 | inStock: number
20 | }
21 |
22 | export interface IQuickViewModalSliderArrowProps extends CustomArrowProps {
23 | directionClassName: string
24 | }
25 |
26 | export interface IHeadingWithCountProps {
27 | count: number
28 | title: string
29 | spinner?: boolean
30 | }
31 |
32 | export interface IAddToCartIconProps {
33 | isProductInCart: boolean
34 | addedClassName: string
35 | className: string
36 | addToCartSpinner: boolean
37 | callback: VoidFunction
38 | }
39 |
40 | export interface ISkeletonProps {
41 | styles: {
42 | readonly [key: string]: string
43 | }
44 | count?: number
45 | }
46 |
--------------------------------------------------------------------------------
/types/favorites.ts:
--------------------------------------------------------------------------------
1 | import { IBaseEffectProps } from './common'
2 |
3 | export interface IFavoriteItem {
4 | _id: string
5 | clientId: string
6 | userId: string
7 | productId: string
8 | image: string
9 | name: string
10 | size: string
11 | price: string
12 | vendorCode: string
13 | category: string
14 | inStock: string
15 | color: string
16 | }
17 |
18 | export interface IAddProductsFromLSToFavoriteFx {
19 | jwt: string
20 | favoriteItems: IFavoriteItem[]
21 | }
22 |
23 | export type IDeleteFavoriteItemsFx = IBaseEffectProps
24 |
--------------------------------------------------------------------------------
/types/hocs.ts:
--------------------------------------------------------------------------------
1 | export interface IWrappedComponentProps {
2 | open: boolean
3 | setOpen: (arg0: boolean) => void
4 | }
5 |
--------------------------------------------------------------------------------
/types/main-page.ts:
--------------------------------------------------------------------------------
1 | import { StaticImageData } from 'next/image'
2 | import { IProduct } from './common'
3 |
4 | export interface IHeroSlide {
5 | id?: number
6 | image: StaticImageData
7 | title: string
8 | }
9 |
10 | export type IHeroSlideTooltip = IHeroSlide
11 |
12 | export interface IMainPageSectionProps {
13 | title: string
14 | goods: IProduct[]
15 | spinner: boolean
16 | }
17 |
--------------------------------------------------------------------------------
/types/passwordRestore.ts:
--------------------------------------------------------------------------------
1 | export interface IUpdateUserPasswordFx {
2 | password: string
3 | email: string
4 | }
5 |
6 | export interface IVerifyCodeFx {
7 | code: number
8 | codeId: string
9 | }
10 |
11 | export interface IPasswordRestoreInputs {
12 | password: string
13 | passwordRepeat: string
14 | }
15 |
--------------------------------------------------------------------------------
/types/product.ts:
--------------------------------------------------------------------------------
1 | export interface IProductPageProps {
2 | productId: string
3 | category: string
4 | }
5 |
6 | export interface IProductImagesItemProps {
7 | image: {
8 | src: string
9 | alt: string
10 | id: string
11 | }
12 | imgSize: number
13 | }
14 |
15 | export interface IProductInfoAccordionProps {
16 | children: React.ReactNode
17 | title: string
18 | }
19 |
--------------------------------------------------------------------------------
/types/profile.ts:
--------------------------------------------------------------------------------
1 | export interface IUploadUserAvatarFx {
2 | jwt: string
3 | formData: FormData
4 | }
5 |
6 | interface IBaseEditUser {
7 | jwt: string
8 | setEdit: (arg0: boolean) => void
9 | }
10 |
11 | export interface IEditUsernameFx extends IBaseEditUser {
12 | name: string
13 | }
14 |
15 | export interface IVerifyEmailFx {
16 | jwt: string
17 | email: string
18 | }
19 |
20 | export interface IEditUserEmailFx extends IBaseEditUser {
21 | code: number
22 | email: string
23 | codeId: string
24 | }
25 |
26 | export interface IVerifyCodeFx {
27 | code: number
28 | jwt: string
29 | codeId: string
30 | }
31 |
32 | export interface IProfileInfoActionsProps {
33 | spinner: boolean
34 | handleSaveInfo: VoidFunction
35 | disabled: boolean
36 | handleCancelEdit: VoidFunction
37 | }
38 |
39 | export interface IProfileInfoBlockProps {
40 | allowEdit: VoidFunction
41 | text: string
42 | }
43 |
44 | export interface ICodeInputProps {
45 | processInput: (
46 | arg0: React.ChangeEvent,
47 | arg1: number
48 | ) => void
49 | onKeyUp: (arg0: React.KeyboardEvent, arg1: number) => void
50 | index: number
51 | handlePushCurrentInput: (arg0: HTMLInputElement) => void
52 | num: string
53 | autoFocus: boolean
54 | }
55 |
56 | export interface IDeleteUserFx {
57 | jwt: string
58 | id: string
59 | handleLogout: VoidFunction
60 | }
61 |
--------------------------------------------------------------------------------
/types/user.ts:
--------------------------------------------------------------------------------
1 | export interface IUser {
2 | _id: string
3 | name: string
4 | password: string
5 | email: string
6 | image: string
7 | role: string
8 | }
9 |
10 | export interface IUserGeolocation {
11 | features: [
12 | {
13 | properties: {
14 | city: string
15 | lon: number
16 | lat: number
17 | }
18 | bbox: [number, number, number, number]
19 | },
20 | ]
21 | }
22 |
23 | export interface ILoginCheckFx {
24 | jwt: string
25 | setShouldShowContent?: (arg0: boolean) => void
26 | }
27 |
--------------------------------------------------------------------------------