├── .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 | 2 | 3 | 4 | 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 |
12 |
13 | 23 |
24 |
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 | Rostelecom Logo 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 | 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 |
  • 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 | 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 |
    3 | 4 | vc.ru 5 | 6 | 7 | habr.comu 8 | 9 |
    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 | {slide.title} 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 | {title} 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 | {image.alt} 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 |
    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 |
    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 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/cart-checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/img/cart-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | {' '} 4 | 5 | -------------------------------------------------------------------------------- /public/img/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/catalog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/checked-favorite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/close-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 8 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /public/img/comparison-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skillBlogWebDev/rostelecom-shop/5fb43da3601cb232354feced9642041cfe4ff747/public/img/comparison-icon.png -------------------------------------------------------------------------------- /public/img/comparison.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/filters.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/gray-ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/green-ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/info-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/map-marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/img/menu-line-tiny.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/img/menu-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/img/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/mir-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/img/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/popup-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/quick-view.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/red-ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/save-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/search-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/select-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/simple-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/slider-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/sort.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/img/telegram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/img/ytb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | --------------------------------------------------------------------------------