From 124aa64d9062028cdf052a697eff78a08e913d16 Mon Sep 17 00:00:00 2001 From: AlexandrValgamov Date: Tue, 4 Jun 2024 18:29:49 +0500 Subject: [PATCH 1/9] Catalog | feat: add catalog page --- src/app/catalog/[shop]/[category]/loading.tsx | 9 ++ src/app/catalog/[shop]/[category]/page.tsx | 30 +++++++ src/components/catalog/catalog.tsx | 39 +++++++++ src/components/catalog/index.ts | 1 + src/components/catalog/styled.tsx | 20 +++++ .../catalog-categories-list.tsx | 39 +++++++++ .../ui/catalog-categories-list/index.ts | 1 + .../ui/catalog-categories-list/styled.tsx | 11 +++ .../ui/catalog-category/catalog-category.tsx | 82 +++++++++++++++++++ .../catalog/ui/catalog-category/index.ts | 1 + .../catalog/ui/catalog-category/styled.tsx | 70 ++++++++++++++++ .../catalog/ui/catalog-tabs/catalog-tabs.tsx | 39 +++++++++ .../catalog/ui/catalog-tabs/index.ts | 1 + .../catalog/ui/catalog-tabs/styled.tsx | 12 +++ src/components/catalog/ui/index.ts | 4 + .../catalog/ui/product-card-button/index.ts | 1 + .../product-card-button.tsx | 20 +++++ .../catalog/ui/product-card-button/styled.tsx | 5 ++ src/page/catalog-page/catalog-page.tsx | 60 ++++++++++++++ src/page/catalog-page/index.ts | 1 + .../utils/helpers/get-tab-index.ts | 13 +++ src/page/catalog-page/utils/helpers/index.ts | 1 + src/page/catalog-page/utils/index.ts | 1 + 23 files changed, 461 insertions(+) create mode 100644 src/app/catalog/[shop]/[category]/loading.tsx create mode 100644 src/app/catalog/[shop]/[category]/page.tsx create mode 100644 src/components/catalog/catalog.tsx create mode 100644 src/components/catalog/index.ts create mode 100644 src/components/catalog/styled.tsx create mode 100644 src/components/catalog/ui/catalog-categories-list/catalog-categories-list.tsx create mode 100644 src/components/catalog/ui/catalog-categories-list/index.ts create mode 100644 src/components/catalog/ui/catalog-categories-list/styled.tsx create mode 100644 src/components/catalog/ui/catalog-category/catalog-category.tsx create mode 100644 src/components/catalog/ui/catalog-category/index.ts create mode 100644 src/components/catalog/ui/catalog-category/styled.tsx create mode 100644 src/components/catalog/ui/catalog-tabs/catalog-tabs.tsx create mode 100644 src/components/catalog/ui/catalog-tabs/index.ts create mode 100644 src/components/catalog/ui/catalog-tabs/styled.tsx create mode 100644 src/components/catalog/ui/index.ts create mode 100644 src/components/catalog/ui/product-card-button/index.ts create mode 100644 src/components/catalog/ui/product-card-button/product-card-button.tsx create mode 100644 src/components/catalog/ui/product-card-button/styled.tsx create mode 100644 src/page/catalog-page/catalog-page.tsx create mode 100644 src/page/catalog-page/index.ts create mode 100644 src/page/catalog-page/utils/helpers/get-tab-index.ts create mode 100644 src/page/catalog-page/utils/helpers/index.ts create mode 100644 src/page/catalog-page/utils/index.ts diff --git a/src/app/catalog/[shop]/[category]/loading.tsx b/src/app/catalog/[shop]/[category]/loading.tsx new file mode 100644 index 0000000..e8b963e --- /dev/null +++ b/src/app/catalog/[shop]/[category]/loading.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { Preloader } from '@/vendor'; + +const Loading = () => { + return ; +}; + +export default Loading; diff --git a/src/app/catalog/[shop]/[category]/page.tsx b/src/app/catalog/[shop]/[category]/page.tsx new file mode 100644 index 0000000..380dd6e --- /dev/null +++ b/src/app/catalog/[shop]/[category]/page.tsx @@ -0,0 +1,30 @@ +import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; +import { Suspense } from 'react'; + +import { CatalogPage } from '@/page'; +import { getQueryClient } from '@/utils'; +import { getCatalog } from '@/api'; + +import Loading from './loading'; + +const Catalog = async ({ + params, +}: { + params: { shop: string; category: string }; +}) => { + const { shop, category } = params; + const queryClient = getQueryClient(); + await queryClient.prefetchQuery({ + queryKey: ['catalog', shop], + queryFn: async () => getCatalog(shop), + }); + const dehydratedState = dehydrate(queryClient); + return ( + + }> + + + + ); +}; +export default Catalog; diff --git a/src/components/catalog/catalog.tsx b/src/components/catalog/catalog.tsx new file mode 100644 index 0000000..f2e288d --- /dev/null +++ b/src/components/catalog/catalog.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { FC } from 'react'; + +import { MenuCategory } from '@/interfaces'; + +import { CatalogTabs, CatalogCategoriesList } from './ui'; +import * as Styled from './styled'; + +interface ICatalogProps { + shopName: string; + categories: MenuCategory[]; + colorItem?: string; + selectedTab: number; + handleTabClick: (id: number) => void; +} + +export const Catalog: FC = ({ + shopName, + categories, + colorItem, + selectedTab, + handleTabClick, +}) => ( + + + + +); diff --git a/src/components/catalog/index.ts b/src/components/catalog/index.ts new file mode 100644 index 0000000..d435521 --- /dev/null +++ b/src/components/catalog/index.ts @@ -0,0 +1 @@ +export * from './catalog'; diff --git a/src/components/catalog/styled.tsx b/src/components/catalog/styled.tsx new file mode 100644 index 0000000..22fdc41 --- /dev/null +++ b/src/components/catalog/styled.tsx @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const Catalog = styled.div` + position: sticky; + height: 100%; + padding-top: 12px; + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + @media ${devices.tablet} { + position: static; + min-height: 100vh; + } +`; diff --git a/src/components/catalog/ui/catalog-categories-list/catalog-categories-list.tsx b/src/components/catalog/ui/catalog-categories-list/catalog-categories-list.tsx new file mode 100644 index 0000000..e912530 --- /dev/null +++ b/src/components/catalog/ui/catalog-categories-list/catalog-categories-list.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { FC } from 'react'; + +import { MenuCategory } from '@/interfaces'; + +import { CatalogCategory } from '../catalog-category'; +import * as Styled from './styled'; + +interface CatalogCategoriesListProps { + categories: MenuCategory[]; + selectedTab: number | null; + colorItem?: string; + shopName: string; +} + +export const CatalogCategoriesList: FC = ({ + categories, + selectedTab, + colorItem = 'var(--green-minus)', + shopName, +}) => ( + + {categories.map( + (category, index) => + selectedTab === index && + category.lists.map((menuList, categoryIndex) => ( + + )), + )} + +); diff --git a/src/components/catalog/ui/catalog-categories-list/index.ts b/src/components/catalog/ui/catalog-categories-list/index.ts new file mode 100644 index 0000000..1467f80 --- /dev/null +++ b/src/components/catalog/ui/catalog-categories-list/index.ts @@ -0,0 +1 @@ +export * from './catalog-categories-list'; diff --git a/src/components/catalog/ui/catalog-categories-list/styled.tsx b/src/components/catalog/ui/catalog-categories-list/styled.tsx new file mode 100644 index 0000000..c7ef2b4 --- /dev/null +++ b/src/components/catalog/ui/catalog-categories-list/styled.tsx @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +export const CatalogCategoryList = styled.ul` + top: 46px; + display: flex; + flex-direction: column; + gap: 12px; + height: calc(100vh - 200px); + padding-top: 14px; + overflow-y: auto; +`; diff --git a/src/components/catalog/ui/catalog-category/catalog-category.tsx b/src/components/catalog/ui/catalog-category/catalog-category.tsx new file mode 100644 index 0000000..8272ba1 --- /dev/null +++ b/src/components/catalog/ui/catalog-category/catalog-category.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { FC, useState } from 'react'; + +import { Icon } from '@/shared'; +import { MenuList } from '@/interfaces'; + +import { ProductCardLink } from '../../../product-card-link'; +import { ProductCardTopContent } from '../../../product-card-top-content'; +import { ProductCardBottomContent } from '../../../product-card-bottom-content'; +import { ProductCardButton } from '../product-card-button'; +import * as Styled from './styled'; + +interface CatalogCategoryProps { + menuList: MenuList; + colorItem?: string; + selectedTab: number | null; + categoryIndex: number; + shopName: string; +} + +export const CatalogCategory: FC = ({ + menuList, + colorItem = 'var(--green-minus)', + selectedTab, + categoryIndex, + shopName, +}) => { + const [showAllProducts, setShowAllProducts] = useState( + menuList.items.length < 5, + ); + const handleShowAllProducts = (): void => { + setShowAllProducts((prev) => !prev); + }; + const itemsToDisplay = showAllProducts + ? menuList.items + : menuList.items.slice(0, 3); + + return ( + + + {menuList.name} + + + + + + {itemsToDisplay.map((item) => ( + } + to={`/catalog/${shopName}/product/${item.slug}`} + topContent={ + + } + /> + ))} + {!showAllProducts && ( + + 6 из {menuList.items.length} + + } + topContent={ + + Посмотреть все + + } + onClick={handleShowAllProducts} + /> + )} + + + ); +}; diff --git a/src/components/catalog/ui/catalog-category/index.ts b/src/components/catalog/ui/catalog-category/index.ts new file mode 100644 index 0000000..7201fb9 --- /dev/null +++ b/src/components/catalog/ui/catalog-category/index.ts @@ -0,0 +1 @@ +export * from './catalog-category'; diff --git a/src/components/catalog/ui/catalog-category/styled.tsx b/src/components/catalog/ui/catalog-category/styled.tsx new file mode 100644 index 0000000..af2975f --- /dev/null +++ b/src/components/catalog/ui/catalog-category/styled.tsx @@ -0,0 +1,70 @@ +import Link from 'next/link'; +import ScrollContainer from 'react-indiana-drag-scroll'; +import styled from 'styled-components'; + +export const CatalogCategory = styled.li` + display: flex; + flex-direction: column; + width: 100%; +`; + +export const IconWrapper = styled.div` + width: 24px; + transform: rotate(-90deg); + + &:hover { + opacity: 0.7; + } +`; + +export const CategoryDetails = styled(Link)` + display: flex; + flex-direction: row; + justify-content: space-between; + padding-right: 20px; + padding-bottom: 12px; + padding-left: 20px; + cursor: pointer; + + &:hover { + ${IconWrapper} { + opacity: 0.5; + } + } +`; + +export const CategoryTitle = styled.h3` + font-family: ${(props) => props.theme.fonts}; + font-size: 14px; + font-weight: 800; + line-height: 20px; + color: var(--black); +`; + +export const CategoryProducts = styled(ScrollContainer)` + display: flex; + flex-direction: row; + gap: 12px; + padding-right: 20px; + padding-left: 20px; +`; + +export const CategoryProductMoreTop = styled.p` + padding: 10px 8px; + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--gray); + white-space: nowrap; +`; + +export const CategoryProductMoreBottom = styled.p` + padding: 4px 10px 2px; + font-family: ${(props) => props.theme.fonts}; + font-size: 10px; + font-weight: 600; + line-height: 14px; + color: var(--white); + text-align: left; +`; diff --git a/src/components/catalog/ui/catalog-tabs/catalog-tabs.tsx b/src/components/catalog/ui/catalog-tabs/catalog-tabs.tsx new file mode 100644 index 0000000..182bfc6 --- /dev/null +++ b/src/components/catalog/ui/catalog-tabs/catalog-tabs.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { FC } from 'react'; + +import { Tab } from '@/shared'; +import { MenuCategory } from '@/interfaces'; + +import * as Styled from './styled'; + +interface CatalogTabProps { + categories: MenuCategory[]; + selectedTab: number; + onClick: (id: number) => void; + colorItem?: string; +} + +export const CatalogTabs: FC = ({ + categories, + selectedTab, + onClick, + colorItem, +}) => ( + + {categories.map((category, index) => ( + onClick(index)} + /> + ))} + +); diff --git a/src/components/catalog/ui/catalog-tabs/index.ts b/src/components/catalog/ui/catalog-tabs/index.ts new file mode 100644 index 0000000..1397848 --- /dev/null +++ b/src/components/catalog/ui/catalog-tabs/index.ts @@ -0,0 +1 @@ +export * from './catalog-tabs'; diff --git a/src/components/catalog/ui/catalog-tabs/styled.tsx b/src/components/catalog/ui/catalog-tabs/styled.tsx new file mode 100644 index 0000000..490fe5d --- /dev/null +++ b/src/components/catalog/ui/catalog-tabs/styled.tsx @@ -0,0 +1,12 @@ +import ScrollContainer from 'react-indiana-drag-scroll'; +import 'react-indiana-drag-scroll/dist/style.css'; +import styled from 'styled-components'; + +export const CatalogTabs = styled(ScrollContainer)` + display: flex; + flex-direction: row; + gap: 20px; + padding-right: 20px; + padding-left: 20px; + border-bottom: 1px solid var(--gray); +`; diff --git a/src/components/catalog/ui/index.ts b/src/components/catalog/ui/index.ts new file mode 100644 index 0000000..6e957f4 --- /dev/null +++ b/src/components/catalog/ui/index.ts @@ -0,0 +1,4 @@ +export * from './catalog-categories-list'; +export * from './catalog-category'; +export * from './catalog-tabs'; +export * from './product-card-button'; diff --git a/src/components/catalog/ui/product-card-button/index.ts b/src/components/catalog/ui/product-card-button/index.ts new file mode 100644 index 0000000..ccfe853 --- /dev/null +++ b/src/components/catalog/ui/product-card-button/index.ts @@ -0,0 +1 @@ +export * from './product-card-button'; diff --git a/src/components/catalog/ui/product-card-button/product-card-button.tsx b/src/components/catalog/ui/product-card-button/product-card-button.tsx new file mode 100644 index 0000000..95aa97c --- /dev/null +++ b/src/components/catalog/ui/product-card-button/product-card-button.tsx @@ -0,0 +1,20 @@ +import { FC, ReactNode } from 'react'; + +import { Card } from '@/shared'; + +import * as Styled from './styled'; + +interface ProductCardButtonProps { + topContent?: ReactNode; + bottomContent?: ReactNode; + onClick: () => void; +} +export const ProductCardButton: FC = ({ + topContent, + bottomContent, + onClick, +}) => ( + + + +); diff --git a/src/components/catalog/ui/product-card-button/styled.tsx b/src/components/catalog/ui/product-card-button/styled.tsx new file mode 100644 index 0000000..4a24f06 --- /dev/null +++ b/src/components/catalog/ui/product-card-button/styled.tsx @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const ProductCardButton = styled.button` + background-color: transparent; +`; diff --git a/src/page/catalog-page/catalog-page.tsx b/src/page/catalog-page/catalog-page.tsx new file mode 100644 index 0000000..507fd12 --- /dev/null +++ b/src/page/catalog-page/catalog-page.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useContext, useEffect, useState } from 'react'; + +import { useCatalog } from '@/api-hooks'; +import { CurrentStylebookContext, ICurrentStylebookContext } from '@/contexts'; +import { getCategories, getErrorMessage } from '@/utils'; +import { Preloader } from '@/vendor'; +import { Catalog } from '@/components'; + +import { getTabIndex } from './utils'; + +export const CatalogPage = ({ + shop, + category, +}: { + shop: string; + category: string; +}) => { + const [selectedTab, setSelectedTab] = useState(0); + const { stylebook, setStylebook } = useContext( + CurrentStylebookContext, + ) as ICurrentStylebookContext; + + const { data, isError, error, isLoading, isSuccess } = useCatalog(shop || ''); + + const categories = isSuccess && data ? getCategories(data) : []; + const index = + isSuccess && categories ? getTabIndex(categories, category || '') : 0; + + useEffect(() => { + if (isSuccess && data) { + setStylebook(data.shop?.stylebook || null); + } + }, [isSuccess, data, setStylebook]); + + useEffect(() => { + if (isSuccess && index) { + setSelectedTab(index); + } + }, [isSuccess, index, setSelectedTab]); + + const handleTabClick = (targetId: number): void => { + setSelectedTab(targetId); + }; + + if (isLoading) return ; + + if (isError) return
Произошла ошибка: {getErrorMessage(error)}
; + + return ( + + ); +}; diff --git a/src/page/catalog-page/index.ts b/src/page/catalog-page/index.ts new file mode 100644 index 0000000..49d18ce --- /dev/null +++ b/src/page/catalog-page/index.ts @@ -0,0 +1 @@ +export * from './catalog-page'; diff --git a/src/page/catalog-page/utils/helpers/get-tab-index.ts b/src/page/catalog-page/utils/helpers/get-tab-index.ts new file mode 100644 index 0000000..93fc9bc --- /dev/null +++ b/src/page/catalog-page/utils/helpers/get-tab-index.ts @@ -0,0 +1,13 @@ +import { MenuCategory } from '@/interfaces'; + +export const getTabIndex = ( + categories: MenuCategory[], + categoryName: string, +) => { + for (let i = 0; i < categories.length; i += 1) { + if (categories[i].category === categoryName) { + return i; + } + } + return 0; +}; diff --git a/src/page/catalog-page/utils/helpers/index.ts b/src/page/catalog-page/utils/helpers/index.ts new file mode 100644 index 0000000..8008390 --- /dev/null +++ b/src/page/catalog-page/utils/helpers/index.ts @@ -0,0 +1 @@ +export * from './get-tab-index'; diff --git a/src/page/catalog-page/utils/index.ts b/src/page/catalog-page/utils/index.ts new file mode 100644 index 0000000..c5f595c --- /dev/null +++ b/src/page/catalog-page/utils/index.ts @@ -0,0 +1 @@ +export * from './helpers'; -- GitLab From 78df5d6e5b2f3b2acec2eb03b9bc2e517d4aefa3 Mon Sep 17 00:00:00 2001 From: AlexandrValgamov Date: Tue, 4 Jun 2024 18:32:57 +0500 Subject: [PATCH 2/9] Catalog | feat: add category page --- .../[indexCategoryParam]/loading.tsx | 9 ++++ .../[category]/[indexCategoryParam]/page.tsx | 35 +++++++++++++ src/components/category/categoty.tsx | 33 ++++++++++++ src/components/category/index.ts | 1 + src/components/category/styled.tsx | 45 ++++++++++++++++ .../CategoryProductImage.tsx | 13 +++++ .../category/ui/CategoryProductImage/index.ts | 1 + .../ui/CategoryProductImage/styled.tsx | 8 +++ .../CategoryProductsList.tsx | 36 +++++++++++++ .../category/ui/CategoryProductsList/index.ts | 1 + .../ui/CategoryProductsList/styled.tsx | 24 +++++++++ .../ProductCardCategoryBottomContent.tsx | 17 +++++++ .../ProductCardCategoryBottomContent/index.ts | 1 + .../styled.tsx | 33 ++++++++++++ src/components/category/ui/index.ts | 1 + src/page/category-page/category-page.tsx | 51 +++++++++++++++++++ src/page/category-page/index.ts | 1 + 17 files changed, 310 insertions(+) create mode 100644 src/app/catalog/[shop]/[category]/[indexCategoryParam]/loading.tsx create mode 100644 src/app/catalog/[shop]/[category]/[indexCategoryParam]/page.tsx create mode 100644 src/components/category/categoty.tsx create mode 100644 src/components/category/index.ts create mode 100644 src/components/category/styled.tsx create mode 100644 src/components/category/ui/CategoryProductImage/CategoryProductImage.tsx create mode 100644 src/components/category/ui/CategoryProductImage/index.ts create mode 100644 src/components/category/ui/CategoryProductImage/styled.tsx create mode 100644 src/components/category/ui/CategoryProductsList/CategoryProductsList.tsx create mode 100644 src/components/category/ui/CategoryProductsList/index.ts create mode 100644 src/components/category/ui/CategoryProductsList/styled.tsx create mode 100644 src/components/category/ui/ProductCardCategoryBottomContent/ProductCardCategoryBottomContent.tsx create mode 100644 src/components/category/ui/ProductCardCategoryBottomContent/index.ts create mode 100644 src/components/category/ui/ProductCardCategoryBottomContent/styled.tsx create mode 100644 src/components/category/ui/index.ts create mode 100644 src/page/category-page/category-page.tsx create mode 100644 src/page/category-page/index.ts diff --git a/src/app/catalog/[shop]/[category]/[indexCategoryParam]/loading.tsx b/src/app/catalog/[shop]/[category]/[indexCategoryParam]/loading.tsx new file mode 100644 index 0000000..e8b963e --- /dev/null +++ b/src/app/catalog/[shop]/[category]/[indexCategoryParam]/loading.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { Preloader } from '@/vendor'; + +const Loading = () => { + return ; +}; + +export default Loading; diff --git a/src/app/catalog/[shop]/[category]/[indexCategoryParam]/page.tsx b/src/app/catalog/[shop]/[category]/[indexCategoryParam]/page.tsx new file mode 100644 index 0000000..6940509 --- /dev/null +++ b/src/app/catalog/[shop]/[category]/[indexCategoryParam]/page.tsx @@ -0,0 +1,35 @@ +import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; +import { Suspense } from 'react'; + +import { CategoryPage } from '@/page'; +import { getQueryClient } from '@/utils'; +import { getCatalog } from '@/api'; + +import Loading from './loading'; + +const Category = ({ + params, +}: { + params: { shop: string; category: string; indexCategoryParam: string }; +}) => { + const { shop, category, indexCategoryParam } = params; + + const queryClient = getQueryClient(); + queryClient.prefetchQuery({ + queryKey: ['catalog', shop], + queryFn: async () => getCatalog(shop), + }); + const dehydratedState = dehydrate(queryClient); + return ( + }> + + + + + ); +}; +export default Category; diff --git a/src/components/category/categoty.tsx b/src/components/category/categoty.tsx new file mode 100644 index 0000000..21413eb --- /dev/null +++ b/src/components/category/categoty.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { FC } from 'react'; + +import { MenuList } from '@/interfaces'; +import { BackGroup } from '@/shared'; + +import * as Styled from './styled'; +import { CategoryProductsList } from './ui'; + +interface ICategoryProps { + category: MenuList | null; + shop: string; + handleBackClick: () => void; + colorItem: string; +} + +export const Category: FC = ({ + category, + shop, + handleBackClick, + colorItem, +}) => ( + + + + + + +); diff --git a/src/components/category/index.ts b/src/components/category/index.ts new file mode 100644 index 0000000..70da778 --- /dev/null +++ b/src/components/category/index.ts @@ -0,0 +1 @@ +export * from './categoty'; diff --git a/src/components/category/styled.tsx b/src/components/category/styled.tsx new file mode 100644 index 0000000..3ff300e --- /dev/null +++ b/src/components/category/styled.tsx @@ -0,0 +1,45 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +interface CategoryPageProps { + $colorItem?: string; +} + +export const CategoryPage = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + height: 897px; + overflow-y: auto; + background-color: ${({ $colorItem }) => $colorItem || 'var(--black)'}; + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + @media ${devices.desktopMd} { + height: 789px; + } + + @media ${devices.desktop} { + width: 361px; + height: 656px; + } + + @media ${devices.tablet} { + width: 100%; + height: 100vh; + } +`; + +export const CategoryHeaderWrapper = styled.div` + display: flex; + flex-direction: row; + gap: 12px; + align-items: center; + justify-content: flex-start; + margin: 20px; +`; diff --git a/src/components/category/ui/CategoryProductImage/CategoryProductImage.tsx b/src/components/category/ui/CategoryProductImage/CategoryProductImage.tsx new file mode 100644 index 0000000..8b92e8e --- /dev/null +++ b/src/components/category/ui/CategoryProductImage/CategoryProductImage.tsx @@ -0,0 +1,13 @@ +import { FC } from 'react'; + +import * as Styled from './styled'; + +interface CategoryProductImageProps { + src: string; + alt: string; +} + +export const CategoryProductImage: FC = ({ + src, + alt, +}) => ; diff --git a/src/components/category/ui/CategoryProductImage/index.ts b/src/components/category/ui/CategoryProductImage/index.ts new file mode 100644 index 0000000..b8c6524 --- /dev/null +++ b/src/components/category/ui/CategoryProductImage/index.ts @@ -0,0 +1 @@ +export * from './CategoryProductImage'; diff --git a/src/components/category/ui/CategoryProductImage/styled.tsx b/src/components/category/ui/CategoryProductImage/styled.tsx new file mode 100644 index 0000000..92cd3b8 --- /dev/null +++ b/src/components/category/ui/CategoryProductImage/styled.tsx @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const CategoryProductImage = styled.img` + width: 100%; + height: auto; + object-fit: cover; + border-radius: 15px; +`; diff --git a/src/components/category/ui/CategoryProductsList/CategoryProductsList.tsx b/src/components/category/ui/CategoryProductsList/CategoryProductsList.tsx new file mode 100644 index 0000000..357cbe1 --- /dev/null +++ b/src/components/category/ui/CategoryProductsList/CategoryProductsList.tsx @@ -0,0 +1,36 @@ +import { FC } from 'react'; + +import { MenuItem } from '@/interfaces'; + +import { ProductCardCategoryBottomContent } from '../ProductCardCategoryBottomContent'; +import { CategoryProductImage } from '../CategoryProductImage'; +import * as Styled from './styled'; +import { ProductCardLink } from '../../../product-card-link'; + +const coffee = '/coffee.jpg'; + +interface ICategoryProductsListProps { + products: MenuItem[]; + shop: string; +} + +export const CategoryProductsList: FC = ({ + products, + shop, +}) => ( + + {products.map((product) => ( + } + bottomContent={ + + } + /> + ))} + +); diff --git a/src/components/category/ui/CategoryProductsList/index.ts b/src/components/category/ui/CategoryProductsList/index.ts new file mode 100644 index 0000000..9541c89 --- /dev/null +++ b/src/components/category/ui/CategoryProductsList/index.ts @@ -0,0 +1 @@ +export * from './CategoryProductsList'; diff --git a/src/components/category/ui/CategoryProductsList/styled.tsx b/src/components/category/ui/CategoryProductsList/styled.tsx new file mode 100644 index 0000000..c75cab9 --- /dev/null +++ b/src/components/category/ui/CategoryProductsList/styled.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const CategoryProductsList = styled.div` + box-sizing: border-box; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(130px, 47%)); + grid-auto-rows: minmax(140px, auto); + gap: 12px; + justify-content: flex-start; + width: 100%; + padding-right: 20px; + padding-bottom: 110px; + padding-left: 20px; + + @media ${devices.tablet} { + grid-template-columns: repeat(auto-fit, minmax(130px, 32%)); + } + + @media ${devices.mobile} { + grid-template-columns: repeat(auto-fit, minmax(130px, 47%)); + } +`; diff --git a/src/components/category/ui/ProductCardCategoryBottomContent/ProductCardCategoryBottomContent.tsx b/src/components/category/ui/ProductCardCategoryBottomContent/ProductCardCategoryBottomContent.tsx new file mode 100644 index 0000000..01da663 --- /dev/null +++ b/src/components/category/ui/ProductCardCategoryBottomContent/ProductCardCategoryBottomContent.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react'; + +import * as Styled from './styled'; + +interface ProductCardCategoryBottomContentProps { + name: string; + price: string; +} + +export const ProductCardCategoryBottomContent: FC< + ProductCardCategoryBottomContentProps +> = ({ name, price }) => ( + + {name} + {price} + +); diff --git a/src/components/category/ui/ProductCardCategoryBottomContent/index.ts b/src/components/category/ui/ProductCardCategoryBottomContent/index.ts new file mode 100644 index 0000000..737bc06 --- /dev/null +++ b/src/components/category/ui/ProductCardCategoryBottomContent/index.ts @@ -0,0 +1 @@ +export * from './ProductCardCategoryBottomContent'; diff --git a/src/components/category/ui/ProductCardCategoryBottomContent/styled.tsx b/src/components/category/ui/ProductCardCategoryBottomContent/styled.tsx new file mode 100644 index 0000000..a1e31ed --- /dev/null +++ b/src/components/category/ui/ProductCardCategoryBottomContent/styled.tsx @@ -0,0 +1,33 @@ +import styled from 'styled-components'; + +export const ProductCardCategoryBottomContent = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; + padding: 2px 12px 6px; +`; + +export const ProductTitle = styled.p` + display: -webkit-box; + overflow: hidden; + font-family: Gilroy, sans-serif; + font-size: 10px; + font-weight: 800; + line-height: 14px; + color: var(--white); + text-align: left; + text-overflow: ellipsis; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; +`; + +export const ProductPrice = styled.p` + font-family: Gilroy, sans-serif; + font-size: 9px; + font-weight: 800; + line-height: 12px; + color: var(--white); + text-align: right; +`; diff --git a/src/components/category/ui/index.ts b/src/components/category/ui/index.ts new file mode 100644 index 0000000..9541c89 --- /dev/null +++ b/src/components/category/ui/index.ts @@ -0,0 +1 @@ +export * from './CategoryProductsList'; diff --git a/src/page/category-page/category-page.tsx b/src/page/category-page/category-page.tsx new file mode 100644 index 0000000..6ab5a83 --- /dev/null +++ b/src/page/category-page/category-page.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { useContext, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +import { CurrentStylebookContext, ICurrentStylebookContext } from '@/contexts'; +import { useCategory } from '@/api-hooks'; +import { parseToInt, getErrorMessage } from '@/utils'; +import { Preloader } from '@/vendor'; +import { Category } from '@/components'; + +export const CategoryPage = ({ + shop, + indexTabParam, + indexCategoryParam, +}: { + shop: string; + indexTabParam: string; + indexCategoryParam: string; +}) => { + const router = useRouter(); + const { stylebook, setStylebook } = useContext( + CurrentStylebookContext, + ) as ICurrentStylebookContext; + const categoryIndex = parseToInt(indexCategoryParam); + const tabIndex = parseToInt(indexTabParam); + const { newStylebook, category, isError, error, isLoading, isSuccess } = + useCategory(shop || '', tabIndex, categoryIndex); + const handleBackClick = () => { + router.push(`/catalog/${shop}/0`, { scroll: false }); + }; + + useEffect(() => { + if (isSuccess && newStylebook) { + setStylebook(newStylebook || null); + } + }, [isSuccess, newStylebook, setStylebook]); + + if (isLoading) return ; + + if (isError) return
Произошла ошибка: {getErrorMessage(error)}
; + + return ( + + ); +}; diff --git a/src/page/category-page/index.ts b/src/page/category-page/index.ts new file mode 100644 index 0000000..2551448 --- /dev/null +++ b/src/page/category-page/index.ts @@ -0,0 +1 @@ +export * from './category-page'; -- GitLab From 319728f648aff5a589e549c7bc66eec88311eeb1 Mon Sep 17 00:00:00 2001 From: AlexandrValgamov Date: Tue, 4 Jun 2024 18:36:19 +0500 Subject: [PATCH 3/9] Catalog | feat: add product page --- .../catalog/[shop]/product/[item]/loading.tsx | 9 ++ .../catalog/[shop]/product/[item]/page.tsx | 46 ++++++ .../product-card-bottom-content/index.ts | 1 + .../product-card-bottom-content.tsx | 18 +++ .../product-card-bottom-content/styled.tsx | 15 ++ src/components/product-card-link/index.ts | 1 + .../product-card-link/product-card-link.tsx | 23 +++ src/components/product-card-link/styled.tsx | 4 + .../product-card-top-content/index.ts | 1 + .../product-card-top-content.tsx | 25 ++++ .../product-card-top-content/styled.tsx | 32 +++++ src/components/product/index.ts | 1 + src/components/product/product.tsx | 53 +++++++ src/components/product/styled.tsx | 7 + src/components/product/ui/index.ts | 6 + .../product/ui/product-add-button/index.ts | 1 + .../product-add-button/product-add-button.tsx | 47 ++++++ .../product/ui/product-add-button/styled.tsx | 55 +++++++ .../product/ui/product-checkbox/index.ts | 1 + .../ui/product-checkbox/product-checkbox.tsx | 39 +++++ .../product/ui/product-checkbox/styled.tsx | 24 ++++ .../product/ui/product-control/index.ts | 1 + .../ui/product-control/product-control.tsx | 38 +++++ .../product/ui/product-control/styled.tsx | 40 ++++++ .../product/ui/product-details/index.ts | 1 + .../ui/product-details/product-details.tsx | 73 ++++++++++ .../product/ui/product-details/styled.tsx | 33 +++++ .../product/ui/product-footer/index.ts | 1 + .../ui/product-footer/product-footer.tsx | 83 +++++++++++ .../product/ui/product-footer/styled.tsx | 84 +++++++++++ .../product/ui/product-header/index.ts | 1 + .../ui/product-header/product-header.tsx | 30 ++++ .../product/ui/product-header/styled.tsx | 28 ++++ .../product/ui/product-image/index.ts | 1 + .../ui/product-image/product-image.tsx | 12 ++ .../product/ui/product-image/styled.tsx | 11 ++ .../product/ui/product-info/index.ts | 1 + .../product/ui/product-info/product-info.tsx | 41 ++++++ .../product/ui/product-info/styled.tsx | 60 ++++++++ .../product/ui/product-modifications/index.ts | 1 + .../product-modifications.tsx | 69 +++++++++ .../ui/product-modifications/styled.tsx | 49 +++++++ .../product/ui/product-page-view/index.ts | 1 + .../product-page-view/product-page-view.tsx | 135 ++++++++++++++++++ .../product/ui/product-page-view/styled.tsx | 39 +++++ .../product/ui/product-radio/index.ts | 1 + .../ui/product-radio/product-radio.tsx | 35 +++++ .../product/ui/product-radio/styled.tsx | 24 ++++ .../product/ui/recommendations/index.ts | 1 + .../ui/recommendations/recommendations.tsx | 37 +++++ .../product/ui/recommendations/styled.tsx | 12 ++ src/components/slider/index.ts | 1 + src/components/slider/slider.tsx | 24 ++++ src/components/slider/styled.tsx | 13 ++ src/page/product-page/index.ts | 1 + src/page/product-page/product-page.tsx | 51 +++++++ 56 files changed, 1442 insertions(+) create mode 100644 src/app/catalog/[shop]/product/[item]/loading.tsx create mode 100644 src/app/catalog/[shop]/product/[item]/page.tsx create mode 100644 src/components/product-card-bottom-content/index.ts create mode 100644 src/components/product-card-bottom-content/product-card-bottom-content.tsx create mode 100644 src/components/product-card-bottom-content/styled.tsx create mode 100644 src/components/product-card-link/index.ts create mode 100644 src/components/product-card-link/product-card-link.tsx create mode 100644 src/components/product-card-link/styled.tsx create mode 100644 src/components/product-card-top-content/index.ts create mode 100644 src/components/product-card-top-content/product-card-top-content.tsx create mode 100644 src/components/product-card-top-content/styled.tsx create mode 100644 src/components/product/index.ts create mode 100644 src/components/product/product.tsx create mode 100644 src/components/product/styled.tsx create mode 100644 src/components/product/ui/index.ts create mode 100644 src/components/product/ui/product-add-button/index.ts create mode 100644 src/components/product/ui/product-add-button/product-add-button.tsx create mode 100644 src/components/product/ui/product-add-button/styled.tsx create mode 100644 src/components/product/ui/product-checkbox/index.ts create mode 100644 src/components/product/ui/product-checkbox/product-checkbox.tsx create mode 100644 src/components/product/ui/product-checkbox/styled.tsx create mode 100644 src/components/product/ui/product-control/index.ts create mode 100644 src/components/product/ui/product-control/product-control.tsx create mode 100644 src/components/product/ui/product-control/styled.tsx create mode 100644 src/components/product/ui/product-details/index.ts create mode 100644 src/components/product/ui/product-details/product-details.tsx create mode 100644 src/components/product/ui/product-details/styled.tsx create mode 100644 src/components/product/ui/product-footer/index.ts create mode 100644 src/components/product/ui/product-footer/product-footer.tsx create mode 100644 src/components/product/ui/product-footer/styled.tsx create mode 100644 src/components/product/ui/product-header/index.ts create mode 100644 src/components/product/ui/product-header/product-header.tsx create mode 100644 src/components/product/ui/product-header/styled.tsx create mode 100644 src/components/product/ui/product-image/index.ts create mode 100644 src/components/product/ui/product-image/product-image.tsx create mode 100644 src/components/product/ui/product-image/styled.tsx create mode 100644 src/components/product/ui/product-info/index.ts create mode 100644 src/components/product/ui/product-info/product-info.tsx create mode 100644 src/components/product/ui/product-info/styled.tsx create mode 100644 src/components/product/ui/product-modifications/index.ts create mode 100644 src/components/product/ui/product-modifications/product-modifications.tsx create mode 100644 src/components/product/ui/product-modifications/styled.tsx create mode 100644 src/components/product/ui/product-page-view/index.ts create mode 100644 src/components/product/ui/product-page-view/product-page-view.tsx create mode 100644 src/components/product/ui/product-page-view/styled.tsx create mode 100644 src/components/product/ui/product-radio/index.ts create mode 100644 src/components/product/ui/product-radio/product-radio.tsx create mode 100644 src/components/product/ui/product-radio/styled.tsx create mode 100644 src/components/product/ui/recommendations/index.ts create mode 100644 src/components/product/ui/recommendations/recommendations.tsx create mode 100644 src/components/product/ui/recommendations/styled.tsx create mode 100644 src/components/slider/index.ts create mode 100644 src/components/slider/slider.tsx create mode 100644 src/components/slider/styled.tsx create mode 100644 src/page/product-page/index.ts create mode 100644 src/page/product-page/product-page.tsx diff --git a/src/app/catalog/[shop]/product/[item]/loading.tsx b/src/app/catalog/[shop]/product/[item]/loading.tsx new file mode 100644 index 0000000..e8b963e --- /dev/null +++ b/src/app/catalog/[shop]/product/[item]/loading.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { Preloader } from '@/vendor'; + +const Loading = () => { + return ; +}; + +export default Loading; diff --git a/src/app/catalog/[shop]/product/[item]/page.tsx b/src/app/catalog/[shop]/product/[item]/page.tsx new file mode 100644 index 0000000..afcd220 --- /dev/null +++ b/src/app/catalog/[shop]/product/[item]/page.tsx @@ -0,0 +1,46 @@ +import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; +import { Suspense } from 'react'; + +import { ProductPage } from '@/page'; +import { getQueryClient } from '@/utils'; +import { getProduct, postRecommendations } from '@/api'; + +import Loading from './loading'; + +const Product = async ({ + params, +}: { + params: { shop: string; item: string }; +}) => { + const { shop, item } = params; + const queryClient = getQueryClient(); + + try { + await Promise.all([ + queryClient.prefetchQuery({ + queryKey: ['catalog', shop, item], + queryFn: () => getProduct(shop, item), + }), + queryClient.prefetchQuery({ + queryKey: ['promo', shop, item], + queryFn: () => + postRecommendations({ + shop: shop || '', + currentItem: item || '', + }), + }), + ]); + } catch (error) { + console.error('Error prefetching data:', error); + } + + const dehydratedState = dehydrate(queryClient); + return ( + }> + + + + + ); +}; +export default Product; diff --git a/src/components/product-card-bottom-content/index.ts b/src/components/product-card-bottom-content/index.ts new file mode 100644 index 0000000..dd57dc5 --- /dev/null +++ b/src/components/product-card-bottom-content/index.ts @@ -0,0 +1 @@ +export * from './product-card-bottom-content'; diff --git a/src/components/product-card-bottom-content/product-card-bottom-content.tsx b/src/components/product-card-bottom-content/product-card-bottom-content.tsx new file mode 100644 index 0000000..64d1347 --- /dev/null +++ b/src/components/product-card-bottom-content/product-card-bottom-content.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { FC } from 'react'; + +import { formatPrice, parseToInt } from '@/utils'; + +import * as Styled from './styled'; + +interface ProductCardBottomContentProps { + price: string; +} +export const ProductCardBottomContent: FC = ({ + price, +}) => ( + + {formatPrice(parseToInt(price))} + +); diff --git a/src/components/product-card-bottom-content/styled.tsx b/src/components/product-card-bottom-content/styled.tsx new file mode 100644 index 0000000..d8b5fa3 --- /dev/null +++ b/src/components/product-card-bottom-content/styled.tsx @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +export const ProductCardBottomContent = styled.div` + display: flex; + justify-content: flex-end; + padding: 4px 10px 2px; +`; + +export const Price = styled.span` + font-family: ${(props) => props.theme.fonts}; + font-size: 10px; + font-weight: 600; + line-height: 14px; + color: var(--white); +`; diff --git a/src/components/product-card-link/index.ts b/src/components/product-card-link/index.ts new file mode 100644 index 0000000..7c0f7f4 --- /dev/null +++ b/src/components/product-card-link/index.ts @@ -0,0 +1 @@ +export * from './product-card-link'; diff --git a/src/components/product-card-link/product-card-link.tsx b/src/components/product-card-link/product-card-link.tsx new file mode 100644 index 0000000..d82cd7e --- /dev/null +++ b/src/components/product-card-link/product-card-link.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { FC, ReactNode } from 'react'; + +import { Card } from '@/shared'; + +import * as Styled from './styled'; + +interface IProductCardLinkProps { + to: string; + topContent?: ReactNode; + bottomContent?: ReactNode; +} + +export const ProductCardLink: FC = ({ + to, + topContent, + bottomContent, +}) => ( + + + +); diff --git a/src/components/product-card-link/styled.tsx b/src/components/product-card-link/styled.tsx new file mode 100644 index 0000000..ff43ae9 --- /dev/null +++ b/src/components/product-card-link/styled.tsx @@ -0,0 +1,4 @@ +import Link from 'next/link'; +import styled from 'styled-components'; + +export const ProductCardLink = styled(Link)``; diff --git a/src/components/product-card-top-content/index.ts b/src/components/product-card-top-content/index.ts new file mode 100644 index 0000000..ee5e1aa --- /dev/null +++ b/src/components/product-card-top-content/index.ts @@ -0,0 +1 @@ +export * from './product-card-top-content'; diff --git a/src/components/product-card-top-content/product-card-top-content.tsx b/src/components/product-card-top-content/product-card-top-content.tsx new file mode 100644 index 0000000..460199f --- /dev/null +++ b/src/components/product-card-top-content/product-card-top-content.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { FC } from 'react'; + +import { Icon } from '@/shared'; + +import * as Styled from './styled'; + +interface ProductCardTopContentProps { + name: string; + icon: string; + colorItem?: string; +} +export const ProductCardTopContent: FC = ({ + name, + icon, + colorItem, +}) => ( + + + + + {name} + +); diff --git a/src/components/product-card-top-content/styled.tsx b/src/components/product-card-top-content/styled.tsx new file mode 100644 index 0000000..96d0fc5 --- /dev/null +++ b/src/components/product-card-top-content/styled.tsx @@ -0,0 +1,32 @@ +import styled from 'styled-components'; + +export const ProductCardTopContent = styled.div` + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + width: max-content; + padding-right: 8px; + padding-left: 8px; +`; + +export const IconWrapper = styled.div` + padding-top: 6px; + padding-bottom: 6px; +`; + +export const Title = styled.p` + display: -webkit-box; + max-width: 183px; + padding-top: 2px; + padding-bottom: 2px; + overflow: hidden; + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--black); + text-overflow: ellipsis; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +`; diff --git a/src/components/product/index.ts b/src/components/product/index.ts new file mode 100644 index 0000000..81cb0b0 --- /dev/null +++ b/src/components/product/index.ts @@ -0,0 +1 @@ +export * from './product'; diff --git a/src/components/product/product.tsx b/src/components/product/product.tsx new file mode 100644 index 0000000..d22fe2e --- /dev/null +++ b/src/components/product/product.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { FC } from 'react'; +import { useRouter } from 'next/navigation'; + +import { + OperatingHours, + Stylebook, + IProduct, + IProductRecommendations, +} from '@/interfaces'; + +import { ProductHeader, ProductImage, ProductPageView } from './ui'; +import * as Styled from './styled'; + +interface IProductProps { + product: IProduct | null; + recommendations: IProductRecommendations[]; + shop: string; + stylebook: Stylebook | null; + mode: OperatingHours | null; +} + +export const Product: FC = ({ + product, + recommendations, + shop, + stylebook, + mode, +}) => { + const router = useRouter(); + const handleBackClick = () => { + router.push(`/catalog/${shop}/0`, { scroll: false }); + }; + + return ( + + + + + + ); +}; diff --git a/src/components/product/styled.tsx b/src/components/product/styled.tsx new file mode 100644 index 0000000..c69fe37 --- /dev/null +++ b/src/components/product/styled.tsx @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +export const ProductPage = styled.div` + display: flex; + flex-direction: column; + background-color: var(--white); +`; diff --git a/src/components/product/ui/index.ts b/src/components/product/ui/index.ts new file mode 100644 index 0000000..4a864c3 --- /dev/null +++ b/src/components/product/ui/index.ts @@ -0,0 +1,6 @@ +export * from './product-header'; +export * from './product-page-view'; +export * from './product-image'; +export * from './product-footer'; +export * from './product-details'; +export * from './recommendations'; diff --git a/src/components/product/ui/product-add-button/index.ts b/src/components/product/ui/product-add-button/index.ts new file mode 100644 index 0000000..7ba640c --- /dev/null +++ b/src/components/product/ui/product-add-button/index.ts @@ -0,0 +1 @@ +export * from './product-add-button'; diff --git a/src/components/product/ui/product-add-button/product-add-button.tsx b/src/components/product/ui/product-add-button/product-add-button.tsx new file mode 100644 index 0000000..ed9cc2f --- /dev/null +++ b/src/components/product/ui/product-add-button/product-add-button.tsx @@ -0,0 +1,47 @@ +import { FC } from 'react'; + +import { Icon } from '@/shared'; + +import * as Styled from './styled'; + +interface AddButtonProps { + onIncrease: () => void; + onDecrease: () => void; + value: number; + colorItem?: string; + isOpen?: boolean; +} + +export const AddButton: FC = ({ + value, + onIncrease, + onDecrease, + colorItem, + isOpen, +}) => { + const title = isOpen ? 'Добавить' : 'Закрыто'; + + if (!value || !isOpen) { + return ( + + {title} + + ); + } + + return ( + + + + + {value} + + + + + ); +}; diff --git a/src/components/product/ui/product-add-button/styled.tsx b/src/components/product/ui/product-add-button/styled.tsx new file mode 100644 index 0000000..bd61189 --- /dev/null +++ b/src/components/product/ui/product-add-button/styled.tsx @@ -0,0 +1,55 @@ +import styled from 'styled-components'; + +interface AddButtonProps { + $colorItem?: string; +} + +export const AddButton = styled.button` + display: block; + width: 100%; + height: 100%; + font-size: 13px; + font-weight: 800; + line-height: 16px; + color: ${({ $colorItem }) => $colorItem || 'black'}; + letter-spacing: 0; + cursor: pointer; + user-select: none; + background-color: var(--white); + border: 2px solid var(--black-plus); + border-radius: 10px; + + &:disabled { + cursor: default; + } +`; + +export const Counter = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + height: 100%; + color: var(--green-minus); + user-select: none; + background-color: var(--white); + border: 2px solid var(--black-plus); + border-radius: 10px; +`; + +export const CounterButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + padding: 0 14px; + color: ${({ $colorItem }) => $colorItem || 'black'}; + cursor: pointer; +`; + +export const Count = styled.p` + font-family: Gilroy, sans-serif; + font-size: 12px; + font-weight: 800; + color: ${({ $colorItem }) => $colorItem || 'black'}; +`; diff --git a/src/components/product/ui/product-checkbox/index.ts b/src/components/product/ui/product-checkbox/index.ts new file mode 100644 index 0000000..f61635e --- /dev/null +++ b/src/components/product/ui/product-checkbox/index.ts @@ -0,0 +1 @@ +export * from './product-checkbox'; diff --git a/src/components/product/ui/product-checkbox/product-checkbox.tsx b/src/components/product/ui/product-checkbox/product-checkbox.tsx new file mode 100644 index 0000000..1d3bd41 --- /dev/null +++ b/src/components/product/ui/product-checkbox/product-checkbox.tsx @@ -0,0 +1,39 @@ +import { FC, HTMLProps } from 'react'; + +import { Radio, RadioActive } from '@/shared'; + +import * as Styled from './styled'; + +interface CheckboxProps extends HTMLProps { + label: string; + value: string; + price: string; + name: string; + activeValues: string[]; + handleAddonChange: (value: string, price: string, checked: boolean) => void; + colorItem?: string; +} + +export const ProductCheckbox: FC = ({ + label, + value, + price, + name, + activeValues, + handleAddonChange, + colorItem, +}) => ( + + {activeValues.includes(value) ? : } + + handleAddonChange(value, price, event.target.checked) + } + /> + {label} + +); diff --git a/src/components/product/ui/product-checkbox/styled.tsx b/src/components/product/ui/product-checkbox/styled.tsx new file mode 100644 index 0000000..0b9c113 --- /dev/null +++ b/src/components/product/ui/product-checkbox/styled.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components'; + +interface StyledRadioProps { + $colorItem?: string; +} + +export const CheckboxLabel = styled.label` + display: inline-flex; + gap: 6px; + align-items: center; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: ${({ $colorItem }) => $colorItem || 'black'}; + cursor: pointer; +`; + +export const Checkbox = styled.input` + display: none; +`; + +export const StyledSpan = styled.span` + color: var(--black-plus); +`; diff --git a/src/components/product/ui/product-control/index.ts b/src/components/product/ui/product-control/index.ts new file mode 100644 index 0000000..a214e6e --- /dev/null +++ b/src/components/product/ui/product-control/index.ts @@ -0,0 +1 @@ +export * from './product-control'; diff --git a/src/components/product/ui/product-control/product-control.tsx b/src/components/product/ui/product-control/product-control.tsx new file mode 100644 index 0000000..217273e --- /dev/null +++ b/src/components/product/ui/product-control/product-control.tsx @@ -0,0 +1,38 @@ +import { formatPrice } from '@/utils'; + +import { AddButton } from '../product-add-button'; +import * as Styled from './styled'; + +export interface IProductControlProps { + isOpen: boolean; + onIncrease: () => void; + onDecrease: () => void; + count: number; + price: number; + colorItem: string; +} + +export const ProductControl = ({ + isOpen, + onIncrease, + onDecrease, + count, + price, + colorItem, +}: IProductControlProps) => ( + + + + + + + {formatPrice(count === 0 ? price : price * count)} + + +); diff --git a/src/components/product/ui/product-control/styled.tsx b/src/components/product/ui/product-control/styled.tsx new file mode 100644 index 0000000..4b797e8 --- /dev/null +++ b/src/components/product/ui/product-control/styled.tsx @@ -0,0 +1,40 @@ +import styled from 'styled-components'; + +export const CartItemControl = styled.div` + display: grid; + grid-template-rows: 44px; + grid-template-columns: 71.5px 71.5px 67px; + border-radius: 10px; +`; + +export const AddButtonWrapper = styled.div` + z-index: 1; + grid-row: 1/2; + grid-column: 1/3; + border-radius: 10px; +`; + +export const PriceBackground = styled.div` + box-sizing: border-box; + display: flex; + grid-row: 1/2; + grid-column: 2/4; + align-items: center; + justify-content: center; + background-color: var(--black-plus); + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; +`; + +export const Price = styled.div` + display: flex; + grid-row: 1/2; + grid-column: 3/4; + align-items: center; + justify-content: center; + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--white); +`; diff --git a/src/components/product/ui/product-details/index.ts b/src/components/product/ui/product-details/index.ts new file mode 100644 index 0000000..d415f29 --- /dev/null +++ b/src/components/product/ui/product-details/index.ts @@ -0,0 +1 @@ +export * from './product-details'; diff --git a/src/components/product/ui/product-details/product-details.tsx b/src/components/product/ui/product-details/product-details.tsx new file mode 100644 index 0000000..de25e8f --- /dev/null +++ b/src/components/product/ui/product-details/product-details.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { FC, useState } from 'react'; + +import { + IFormState, + IModifications, + INutritionalValue, + IProductInfo, + Stylebook, +} from '@/interfaces'; +import { Tab } from '@/shared'; + +import { ProductModifications } from '../product-modifications'; +import { ProductInfo } from '../product-info'; +import * as Styled from './styled'; + +interface ProductDetailsProps { + modifications: IModifications | null; + info: IProductInfo | null; + nutritionalValue: INutritionalValue[] | null; + stylebook: Stylebook | null; + formState: IFormState; + handleAddonChange: (value: string, price: string, isChecked: boolean) => void; + handleModificationChange: (value: string) => void; +} + +export const ProductDetails: FC = ({ + modifications, + info, + nutritionalValue, + stylebook, + formState, + handleAddonChange, + handleModificationChange, +}) => { + const [selectedTab, setSelectedTab] = useState(0); + + return ( + + + setSelectedTab(0)} + /> + setSelectedTab(1)} + /> + + + {selectedTab === 0 && ( + + )} + {selectedTab === 1 && ( + + )} + + + ); +}; diff --git a/src/components/product/ui/product-details/styled.tsx b/src/components/product/ui/product-details/styled.tsx new file mode 100644 index 0000000..e020523 --- /dev/null +++ b/src/components/product/ui/product-details/styled.tsx @@ -0,0 +1,33 @@ +import styled from 'styled-components'; + +import { Stylebook } from '@/interfaces'; + +interface ProductDetailsProps { + $stylebook: Stylebook | null; +} + +export const ProductDetails = styled.div` + display: flex; + flex-direction: column; + padding: 20px; +`; + +export const Tabs = styled.div` + z-index: 1; + display: flex; + flex-direction: row; + gap: 20px; + justify-content: space-evenly; + border-bottom: 1px solid var(--black); +`; + +export const ProductDetailsContent = styled.div` + z-index: 1; + display: flex; + flex-direction: column; + padding: 22px; + margin-top: 24px; + background-color: var(--white); + border-radius: 15px; + transition: max-height 1s ease; +`; diff --git a/src/components/product/ui/product-footer/index.ts b/src/components/product/ui/product-footer/index.ts new file mode 100644 index 0000000..f500a3b --- /dev/null +++ b/src/components/product/ui/product-footer/index.ts @@ -0,0 +1 @@ +export * from './product-footer'; diff --git a/src/components/product/ui/product-footer/product-footer.tsx b/src/components/product/ui/product-footer/product-footer.tsx new file mode 100644 index 0000000..18013e1 --- /dev/null +++ b/src/components/product/ui/product-footer/product-footer.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; +import { AnimatePresence } from 'framer-motion'; +import { useRouter } from 'next/navigation'; + +import { Portal, Icon, Overlay, LayoutModal } from '@/shared'; +import { Stylebook } from '@/interfaces'; +import { useAppSelector } from '@/hooks'; +import { basketSelector, loggedInSelector } from '@/store'; + +import { ProductControl, IProductControlProps } from '../product-control'; +import * as Styled from './styled'; +import { Cart } from '../../../cart'; + +interface IProductFooterProps extends IProductControlProps { + shop: string; + stylebook: Stylebook | null; +} +export const ProductFooter = ({ + isOpen, + onIncrease, + onDecrease, + count, + stylebook, + price, + shop, + colorItem, +}: IProductFooterProps) => { + const basket = useAppSelector(basketSelector); + const loggedIn = useAppSelector(loggedInSelector); + const router = useRouter(); + const isEmpty = !(basket[shop]?.items?.length > 0); + + const [modalIsOpen, setModalIsOpen] = useState(false); + const handleToPayClick = () => { + if (!loggedIn) { + router.push('/profile'); + return; + } + setModalIsOpen(true); + }; + + const handleCloseCart = () => { + setModalIsOpen(false); + }; + + return ( + + + + + К оплате + + + {modalIsOpen && ( + + + + + + + + )} + + + ); +}; diff --git a/src/components/product/ui/product-footer/styled.tsx b/src/components/product/ui/product-footer/styled.tsx new file mode 100644 index 0000000..2b4332b --- /dev/null +++ b/src/components/product/ui/product-footer/styled.tsx @@ -0,0 +1,84 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const ProductFooter = styled.div` + position: absolute; + bottom: 0; + z-index: 2; + box-sizing: border-box; + display: flex; + flex-direction: row; + justify-content: space-evenly; + width: 100%; + height: 66px; + padding-top: 12px; + padding-bottom: 10px; + padding-left: 20px; + background-color: var(--black); + border-top-left-radius: 15px; + border-top-right-radius: 15px; + + &::after { + position: absolute; + inset: 0; + pointer-events: none; + content: ''; + background: url('/patterns/noise.png'); + } + + @media ${devices.tablet} { + height: 70px; + } + + @media ${devices.mobile} { + height: 66px; + } +`; + +export const PayButton = styled.button` + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 60px; + padding: 0 5px 0 4px; + color: var(--white); + cursor: pointer; + user-select: none; + background-color: transparent; + + &:hover { + opacity: 0.8; + } + + &:disabled { + color: var(--gray); + cursor: default; + opacity: 0.5; + } +`; + +export const Title = styled.p` + font-family: ${(props) => props.theme.fonts}; + font-size: 16px; + font-weight: 800; + line-height: 22px; + color: currentcolor; + white-space: nowrap; + + @media ${devices.mobile} { + font-size: 10px; + } + + @media ${devices.tablet} { + font-size: 12px; + line-height: 16px; + } + + @media ${devices.desktop} { + font-size: 14px; + line-height: 20px; + } +`; diff --git a/src/components/product/ui/product-header/index.ts b/src/components/product/ui/product-header/index.ts new file mode 100644 index 0000000..6124820 --- /dev/null +++ b/src/components/product/ui/product-header/index.ts @@ -0,0 +1 @@ +export * from './product-header'; diff --git a/src/components/product/ui/product-header/product-header.tsx b/src/components/product/ui/product-header/product-header.tsx new file mode 100644 index 0000000..59d9bf0 --- /dev/null +++ b/src/components/product/ui/product-header/product-header.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; + +import { BackGroup, Icon } from '@/shared'; + +import * as Styled from './styled'; + +interface ProductHeaderProps { + name: string; + size: string; + handleBackClick: () => void; +} +export const ProductHeader: FC = ({ + name, + size, + handleBackClick, +}) => ( + + + + {size} + + + + + +); diff --git a/src/components/product/ui/product-header/styled.tsx b/src/components/product/ui/product-header/styled.tsx new file mode 100644 index 0000000..c051112 --- /dev/null +++ b/src/components/product/ui/product-header/styled.tsx @@ -0,0 +1,28 @@ +import styled from 'styled-components'; + +export const ProductHeader = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + margin: 20px 20px 0; +`; + +export const LikeGroup = styled.div` + display: flex; + flex-direction: row; + gap: 20px; + align-items: center; +`; +export const SizeText = styled.p` + font-family: ${(props) => props.theme.fonts}; + font-size: 10px; + font-weight: 800; + line-height: 14px; + color: var(--black); + white-space: nowrap; +`; + +export const FavoriteButton = styled.button` + cursor: pointer; + background-color: transparent; +`; diff --git a/src/components/product/ui/product-image/index.ts b/src/components/product/ui/product-image/index.ts new file mode 100644 index 0000000..485544c --- /dev/null +++ b/src/components/product/ui/product-image/index.ts @@ -0,0 +1 @@ +export * from './product-image'; diff --git a/src/components/product/ui/product-image/product-image.tsx b/src/components/product/ui/product-image/product-image.tsx new file mode 100644 index 0000000..e2f7caf --- /dev/null +++ b/src/components/product/ui/product-image/product-image.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react'; + +import * as Styled from './styled'; + +interface ProductImageProps { + src: string; +} +export const ProductImage: FC = ({ src }) => ( + + + +); diff --git a/src/components/product/ui/product-image/styled.tsx b/src/components/product/ui/product-image/styled.tsx new file mode 100644 index 0000000..7f0cb3f --- /dev/null +++ b/src/components/product/ui/product-image/styled.tsx @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +export const ImageContainer = styled.div` + margin: 0 auto; +`; + +export const Image = styled.img` + width: 100%; + height: auto; + object-fit: cover; +`; diff --git a/src/components/product/ui/product-info/index.ts b/src/components/product/ui/product-info/index.ts new file mode 100644 index 0000000..703cb53 --- /dev/null +++ b/src/components/product/ui/product-info/index.ts @@ -0,0 +1 @@ +export * from './product-info'; diff --git a/src/components/product/ui/product-info/product-info.tsx b/src/components/product/ui/product-info/product-info.tsx new file mode 100644 index 0000000..7cd9919 --- /dev/null +++ b/src/components/product/ui/product-info/product-info.tsx @@ -0,0 +1,41 @@ +import { FC } from 'react'; + +import { INutritionalValue, IProductInfo } from '@/interfaces'; + +import * as Styled from './styled'; + +interface ProductInfoProps { + info: IProductInfo | null; + nutritionalValue: INutritionalValue[] | null; +} +export const ProductInfo: FC = ({ + info, + nutritionalValue, +}) => ( + + {info?.desc} + + {nutritionalValue && ( + + + + {nutritionalValue?.map((value) => ( + + {value.name}, {value.unit} + + ))} + + + + + {nutritionalValue?.map((value) => ( + + {value.value} + + ))} + + + + )} + +); diff --git a/src/components/product/ui/product-info/styled.tsx b/src/components/product/ui/product-info/styled.tsx new file mode 100644 index 0000000..3d15e78 --- /dev/null +++ b/src/components/product/ui/product-info/styled.tsx @@ -0,0 +1,60 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const ProductInfo = styled.div` + display: flex; + flex-direction: column; + gap: 32px; + align-items: center; +`; + +export const ProductDesription = styled.p` + width: 100%; + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--black); + text-align: left; +`; + +export const Table = styled.table` + max-width: 350px; + margin: 0 -20px; + border-spacing: 20px 6px; + border-collapse: separate; +`; + +export const TableHead = styled.thead``; + +export const TableRow = styled.tr``; + +export const TableBody = styled.tbody``; + +export const TableHeader = styled.th` + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--gray-plus); + text-align: left; + letter-spacing: 0; + white-space: nowrap; + + @media ${devices.mobile} { + font-size: 10px; + } +`; + +export const TableData = styled.td` + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--black); + + @media ${devices.mobile} { + font-size: 10px; + } +`; diff --git a/src/components/product/ui/product-modifications/index.ts b/src/components/product/ui/product-modifications/index.ts new file mode 100644 index 0000000..3f60642 --- /dev/null +++ b/src/components/product/ui/product-modifications/index.ts @@ -0,0 +1 @@ +export * from './product-modifications'; diff --git a/src/components/product/ui/product-modifications/product-modifications.tsx b/src/components/product/ui/product-modifications/product-modifications.tsx new file mode 100644 index 0000000..590aba9 --- /dev/null +++ b/src/components/product/ui/product-modifications/product-modifications.tsx @@ -0,0 +1,69 @@ +import { FC } from 'react'; + +import { IFormState, IModifications, Stylebook } from '@/interfaces'; +import { formatPrice, parseToInt } from '@/utils'; + +import * as Styled from './styled'; +import { ProductCheckbox } from '../product-checkbox'; +import { ProductRadio } from '../product-radio'; + +interface ProductModificationsProps { + modifications: IModifications | null; + stylebook: Stylebook | null; + formState: IFormState; + handleAddonChange: (value: string, price: string, isChecked: boolean) => void; + handleModificationChange: (value: string) => void; +} +export const ProductModifications: FC = ({ + modifications, + stylebook, + formState, + handleAddonChange, + handleModificationChange, +}) => ( + + + + Добавки + {modifications?.addons?.map((addon) => ( + + + {addon.price ? ( + + {formatPrice(parseToInt(addon.price))} + + ) : null} + + ))} + + + Модификации + {modifications?.options?.map((option) => ( + + + {option.price ? ( + + {formatPrice(parseToInt(option.price))} + + ) : null} + + ))} + + + +); diff --git a/src/components/product/ui/product-modifications/styled.tsx b/src/components/product/ui/product-modifications/styled.tsx new file mode 100644 index 0000000..33bfd9a --- /dev/null +++ b/src/components/product/ui/product-modifications/styled.tsx @@ -0,0 +1,49 @@ +import styled from 'styled-components'; + +interface ProductModificationsProps { + $colorItem?: string; +} + +export const ProductModifications = styled.div` + display: flex; + flex-direction: column; +`; + +export const Form = styled.form` + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 32px; + width: 100%; +`; + +export const Fieldset = styled.fieldset` + display: flex; + flex-direction: column; + gap: 16px; + border: none; +`; + +export const Legend = styled.legend` + margin-bottom: 10px; + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--gray); +`; + +export const RadioWrapper = styled.div` + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; +`; + +export const ModPrice = styled.span` + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: ${({ $colorItem }) => $colorItem || 'black'}; +`; diff --git a/src/components/product/ui/product-page-view/index.ts b/src/components/product/ui/product-page-view/index.ts new file mode 100644 index 0000000..ed96066 --- /dev/null +++ b/src/components/product/ui/product-page-view/index.ts @@ -0,0 +1 @@ +export * from './product-page-view'; diff --git a/src/components/product/ui/product-page-view/product-page-view.tsx b/src/components/product/ui/product-page-view/product-page-view.tsx new file mode 100644 index 0000000..b48362d --- /dev/null +++ b/src/components/product/ui/product-page-view/product-page-view.tsx @@ -0,0 +1,135 @@ +import { FC, useEffect } from 'react'; + +import { + IProduct, + IProductRecommendations, + OperatingHours, + Stylebook, +} from '@/interfaces'; +import { + addAddon, + changeModification, + initializeProduct, + removeAddon, + updateCount, + addItem, + removeItem, + currentProductSelector, + basketSelector, +} from '@/store'; +import { updateOpenMode, isObjectsEqual } from '@/utils'; +import { useAppDispatch, useAppSelector } from '@/hooks'; + +import { ProductDetails } from '../product-details'; +import { ProductFooter } from '../product-footer'; +import { Recommendations } from '../recommendations'; +import * as Styled from './styled'; + +interface IProductPageViewProps { + mode: OperatingHours | null; + product: IProduct | null; + recommendations: IProductRecommendations[] | null; + stylebook: Stylebook | null; + shop: string | null; +} + +export const ProductPageView: FC = ({ + mode, + product, + recommendations, + stylebook, + shop, +}) => { + const isOpen = updateOpenMode(mode); + const basket = useAppSelector(basketSelector); + const currentProduct = useAppSelector(currentProductSelector); + const dispatch = useAppDispatch(); + + useEffect(() => { + if (!product || currentProduct.productSlug === product?.slug) { + return; + } + dispatch( + initializeProduct({ + icon: product.icon || 'cake', + name: product.name || '', + productSlug: product?.slug || '', + cost: Number(product?.price), + modification: product?.modifications?.options[0]?.name || '', + addons: [], + count: 0, + }), + ); + }, [product, dispatch, currentProduct.productSlug]); + + useEffect(() => { + if (!shop || !basket[shop]) { + return; + } + let countToUpdate = 0; + basket[shop].items.forEach((item) => { + if (isObjectsEqual(item, currentProduct)) countToUpdate = item.count; + }); + dispatch(updateCount(countToUpdate)); + }, [shop, dispatch, basket, currentProduct]); + + const handleAddonChange = ( + value: string, + price: string, + isChecked: boolean, + ) => { + if (isChecked) { + dispatch(addAddon({ value, price })); + } else { + dispatch(removeAddon({ value, price })); + } + }; + + const options = product?.modifications?.options; + const handleModificationChange = (value: string) => { + if (!options) return; + dispatch(changeModification({ value, options })); + }; + + const handleDecreaseCount = () => { + if (shop) dispatch(removeItem({ shop, item: currentProduct })); + }; + + const handleIncreaseCount = () => { + if (shop) dispatch(addItem({ shop, item: currentProduct })); + }; + + return ( + + + + + + + + ); +}; diff --git a/src/components/product/ui/product-page-view/styled.tsx b/src/components/product/ui/product-page-view/styled.tsx new file mode 100644 index 0000000..15e2dc2 --- /dev/null +++ b/src/components/product/ui/product-page-view/styled.tsx @@ -0,0 +1,39 @@ +import styled from 'styled-components'; + +import { Stylebook } from '@/interfaces'; + +interface ProductWrapperDetailsProps { + $stylebook: Stylebook | null; +} + +const patternUrl = (props: ProductWrapperDetailsProps) => + `/patterns/${props.$stylebook?.pattern}.png`; + +export const ProductContentWrapper = styled.div` + display: flex; + flex-direction: column; + background-color: var(--white); +`; + +export const ProductContent = styled.div` + position: absolute; + right: 0; + bottom: 41px; + left: 0; + display: flex; + flex-direction: column; + min-height: 58%; + padding-bottom: 41px; + background-color: ${(props) => props.$stylebook?.mainColor}; + border-top-left-radius: 15px; + border-top-right-radius: 15px; + + &::before { + position: absolute; + inset: 0; + pointer-events: none; + content: ''; + background-image: url(${patternUrl}); + opacity: ${(props) => props.$stylebook?.opacity}; + } +`; diff --git a/src/components/product/ui/product-radio/index.ts b/src/components/product/ui/product-radio/index.ts new file mode 100644 index 0000000..40d0ae0 --- /dev/null +++ b/src/components/product/ui/product-radio/index.ts @@ -0,0 +1 @@ +export * from './product-radio'; diff --git a/src/components/product/ui/product-radio/product-radio.tsx b/src/components/product/ui/product-radio/product-radio.tsx new file mode 100644 index 0000000..09d917e --- /dev/null +++ b/src/components/product/ui/product-radio/product-radio.tsx @@ -0,0 +1,35 @@ +import { FC, HTMLProps } from 'react'; + +import { Radio, RadioActive } from '@/shared'; + +import * as Styled from './styled'; + +interface RadioProps extends HTMLProps { + label: string; + value: string; + name: string; + activeValue: string; + handleModificationChange: (value: string) => void; + colorItem?: string; +} + +export const ProductRadio: FC = ({ + label, + value, + name, + activeValue, + handleModificationChange, + colorItem, +}) => ( + + {activeValue === value ? : } + handleModificationChange(value)} + /> + {label} + +); diff --git a/src/components/product/ui/product-radio/styled.tsx b/src/components/product/ui/product-radio/styled.tsx new file mode 100644 index 0000000..802973f --- /dev/null +++ b/src/components/product/ui/product-radio/styled.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components'; + +interface StyledRadioProps { + $colorItem?: string; +} + +export const StyledRadioLabel = styled.label` + display: inline-flex; + gap: 6px; + align-items: center; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: ${({ $colorItem }) => $colorItem || 'black'}; + cursor: pointer; +`; + +export const StyledRadio = styled.input` + display: none; +`; + +export const StyledSpan = styled.span` + color: var(--black-plus); +`; diff --git a/src/components/product/ui/recommendations/index.ts b/src/components/product/ui/recommendations/index.ts new file mode 100644 index 0000000..aa1cf72 --- /dev/null +++ b/src/components/product/ui/recommendations/index.ts @@ -0,0 +1 @@ +export * from './recommendations'; diff --git a/src/components/product/ui/recommendations/recommendations.tsx b/src/components/product/ui/recommendations/recommendations.tsx new file mode 100644 index 0000000..c3849c9 --- /dev/null +++ b/src/components/product/ui/recommendations/recommendations.tsx @@ -0,0 +1,37 @@ +import { FC } from 'react'; + +import { IProductRecommendations, Stylebook } from '@/interfaces'; + +import { ProductCardTopContent } from '../../../product-card-top-content'; +import { ProductCardBottomContent } from '../../../product-card-bottom-content'; +import { ProductCardLink } from '../../../product-card-link'; +import * as Styled from './styled'; + +interface IRecommendationsProps { + recommendations: IProductRecommendations[]; + shop?: string; + stylebook?: Stylebook | null; +} + +export const Recommendations: FC = ({ + recommendations, + shop, + stylebook, +}) => ( + + {recommendations.map((item) => ( + } + to={`/catalog/${shop}/product/${item.slug}`} + topContent={ + + } + /> + ))} + +); diff --git a/src/components/product/ui/recommendations/styled.tsx b/src/components/product/ui/recommendations/styled.tsx new file mode 100644 index 0000000..c104773 --- /dev/null +++ b/src/components/product/ui/recommendations/styled.tsx @@ -0,0 +1,12 @@ +import ScrollContainer from 'react-indiana-drag-scroll'; +import styled from 'styled-components'; + +export const Recommendations = styled(ScrollContainer)` + z-index: 1; + display: flex; + flex-direction: row; + gap: 12px; + justify-content: flex-start; + padding-right: 20px; + padding-left: 20px; +`; diff --git a/src/components/slider/index.ts b/src/components/slider/index.ts new file mode 100644 index 0000000..eb0742f --- /dev/null +++ b/src/components/slider/index.ts @@ -0,0 +1 @@ +export * from './slider'; diff --git a/src/components/slider/slider.tsx b/src/components/slider/slider.tsx new file mode 100644 index 0000000..5fb7718 --- /dev/null +++ b/src/components/slider/slider.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { FC, ReactNode } from 'react'; + +import * as Styled from './styled'; + +interface SliderProps { + items: ReactNode[]; + spaceBetween: number; +} + +export const Slider: FC = ({ items, spaceBetween }) => { + return ( + + {items.map((item, index) => + item ? {item} : null, + )} + + ); +}; diff --git a/src/components/slider/styled.tsx b/src/components/slider/styled.tsx new file mode 100644 index 0000000..3060519 --- /dev/null +++ b/src/components/slider/styled.tsx @@ -0,0 +1,13 @@ +import styled from 'styled-components'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; + +export const Slider = styled(Swiper)` + padding: 0 20px; + margin: 0; + cursor: pointer; +`; + +export const SliderItem = styled(SwiperSlide)` + width: fit-content; +`; diff --git a/src/page/product-page/index.ts b/src/page/product-page/index.ts new file mode 100644 index 0000000..79ef336 --- /dev/null +++ b/src/page/product-page/index.ts @@ -0,0 +1 @@ +export * from './product-page'; diff --git a/src/page/product-page/product-page.tsx b/src/page/product-page/product-page.tsx new file mode 100644 index 0000000..5c889fe --- /dev/null +++ b/src/page/product-page/product-page.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { FC } from 'react'; + +import { useProduct, useRecommendations } from '@/api-hooks'; +import { getErrorMessage } from '@/utils'; +import { Preloader } from '@/vendor'; +import { Product } from '@/components'; + +interface IProductPage { + shop: string; + item: string; +} +export const ProductPage: FC = ({ shop, item }) => { + const { + mode, + stylebook, + product, + isError: isProductError, + error: productError, + isLoading: isProductLoading, + } = useProduct(shop || '', item || ''); + + const { + data: recommendations, + isError: isRecommendationsError, + error: recommendationsError, + isLoading: isRecommendationsLoading, + } = useRecommendations({ + shop: shop || '', + currentItem: item || '', + }); + + const isLoading = isProductLoading || isRecommendationsLoading; + const isError = isProductError || isRecommendationsError; + const error = productError || recommendationsError; + + if (isLoading) return ; + + if (isError) return
Произошла ошибка: {getErrorMessage(error)}
; + + return ( + + ); +}; -- GitLab From 783d30fb0a0cd27c7913c199c14767bac874e801 Mon Sep 17 00:00:00 2001 From: AlexandrValgamov Date: Tue, 4 Jun 2024 18:37:43 +0500 Subject: [PATCH 4/9] Catalog | feat: add profile page --- src/app/profile/loading.tsx | 9 ++ src/app/profile/page.tsx | 14 +++ src/components/profile/index.ts | 1 + src/components/profile/profile.tsx | 86 +++++++++++++ src/components/profile/schema/index.ts | 1 + .../profile/schema/schema-profile-form.ts | 61 +++++++++ src/components/profile/styled.tsx | 30 +++++ src/components/profile/ui/index.ts | 3 + .../profile/ui/modal-avatar/index.ts | 1 + .../profile/ui/modal-avatar/modal-avatar.tsx | 42 +++++++ .../profile/ui/modal-avatar/styled.tsx | 63 ++++++++++ .../profile/ui/phone-input/index.ts | 1 + .../profile/ui/phone-input/phone-input.tsx | 42 +++++++ .../profile/ui/phone-input/styled.tsx | 38 ++++++ .../profile/ui/profile-button/index.ts | 1 + .../ui/profile-button/profile-button.tsx | 17 +++ .../profile/ui/profile-button/styled.tsx | 18 +++ .../profile/ui/profile-form/index.ts | 1 + .../profile/ui/profile-form/profile-form.tsx | 118 ++++++++++++++++++ .../profile/ui/profile-form/styled.tsx | 75 +++++++++++ .../profile/ui/profile-group/index.ts | 1 + .../ui/profile-group/profile-group.tsx | 48 +++++++ .../profile/ui/profile-group/styled.tsx | 81 ++++++++++++ .../profile/ui/styles/input.module.css | 22 ++++ src/page/profile-page/index.ts | 1 + src/page/profile-page/profile-page.tsx | 3 + 26 files changed, 778 insertions(+) create mode 100644 src/app/profile/loading.tsx create mode 100644 src/app/profile/page.tsx create mode 100644 src/components/profile/index.ts create mode 100644 src/components/profile/profile.tsx create mode 100644 src/components/profile/schema/index.ts create mode 100644 src/components/profile/schema/schema-profile-form.ts create mode 100644 src/components/profile/styled.tsx create mode 100644 src/components/profile/ui/index.ts create mode 100644 src/components/profile/ui/modal-avatar/index.ts create mode 100644 src/components/profile/ui/modal-avatar/modal-avatar.tsx create mode 100644 src/components/profile/ui/modal-avatar/styled.tsx create mode 100644 src/components/profile/ui/phone-input/index.ts create mode 100644 src/components/profile/ui/phone-input/phone-input.tsx create mode 100644 src/components/profile/ui/phone-input/styled.tsx create mode 100644 src/components/profile/ui/profile-button/index.ts create mode 100644 src/components/profile/ui/profile-button/profile-button.tsx create mode 100644 src/components/profile/ui/profile-button/styled.tsx create mode 100644 src/components/profile/ui/profile-form/index.ts create mode 100644 src/components/profile/ui/profile-form/profile-form.tsx create mode 100644 src/components/profile/ui/profile-form/styled.tsx create mode 100644 src/components/profile/ui/profile-group/index.ts create mode 100644 src/components/profile/ui/profile-group/profile-group.tsx create mode 100644 src/components/profile/ui/profile-group/styled.tsx create mode 100644 src/components/profile/ui/styles/input.module.css create mode 100644 src/page/profile-page/index.ts create mode 100644 src/page/profile-page/profile-page.tsx diff --git a/src/app/profile/loading.tsx b/src/app/profile/loading.tsx new file mode 100644 index 0000000..e8b963e --- /dev/null +++ b/src/app/profile/loading.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { Preloader } from '@/vendor'; + +const Loading = () => { + return ; +}; + +export default Loading; diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 0000000..3c636d0 --- /dev/null +++ b/src/app/profile/page.tsx @@ -0,0 +1,14 @@ +import { Suspense } from 'react'; + +import { ProfilePage } from '@/page'; + +import Loading from './loading'; + +const Profile = () => { + return ( + }> + + + ); +}; +export default Profile; diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts new file mode 100644 index 0000000..671f425 --- /dev/null +++ b/src/components/profile/index.ts @@ -0,0 +1 @@ +export * from './profile'; diff --git a/src/components/profile/profile.tsx b/src/components/profile/profile.tsx new file mode 100644 index 0000000..9acd173 --- /dev/null +++ b/src/components/profile/profile.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; + +import { BackGroup } from '@/shared'; +import { useAppDispatch, useAppSelector } from '@/hooks'; +import { useLogout, useUser } from '@/api-hooks'; +import { loggedInSelector, setUser, userSelector } from '@/store'; + +import * as Styled from './styled'; +import { ProfileGroup, ProfileButton, ProfileForm } from './ui'; + +export const Profile = () => { + const dispatch = useAppDispatch(); + const loggedIn = useAppSelector(loggedInSelector); + const user = useAppSelector(userSelector); + const router = useRouter(); + const [isEditing, setIsEditing] = useState(false); + + const { data, refetch: refetchUser } = useUser(user.id?.toString() || ''); + if (loggedIn) refetchUser(); + useEffect(() => { + if (data?.success) { + dispatch(setUser({ data: data.user })); + } + }, [data, dispatch]); + + const handleBackClick = () => { + router.push('/'); + }; + + const handleLogout = useLogout(); + + const handleLoginClick = () => { + router.push('/login'); + }; + + const handleRegisterClick = () => { + router.push('/signup'); + }; + + const handleEdit = () => { + setIsEditing(true); + }; + + const handleCloseEdit = () => { + setIsEditing(false); + }; + + if (!loggedIn) { + return ( + + + + + + + + ); + } + + return ( + + + {isEditing ? ( + + ) : ( + + )} + + ); +}; diff --git a/src/components/profile/schema/index.ts b/src/components/profile/schema/index.ts new file mode 100644 index 0000000..4860529 --- /dev/null +++ b/src/components/profile/schema/index.ts @@ -0,0 +1 @@ +export * from './schema-profile-form'; diff --git a/src/components/profile/schema/schema-profile-form.ts b/src/components/profile/schema/schema-profile-form.ts new file mode 100644 index 0000000..f2afc5e --- /dev/null +++ b/src/components/profile/schema/schema-profile-form.ts @@ -0,0 +1,61 @@ +import { isValidPhoneNumber } from 'libphonenumber-js'; +import { ZodType, z } from 'zod'; + +export const ProfileSchema: ZodType = z + .object({ + avatar: z.string().optional(), + firstName: z + .string() + .optional() + .transform((value) => (value === '' ? undefined : value)), + lastName: z + .string() + .optional() + .transform((value) => (value === '' ? undefined : value)), + surname: z + .string() + .optional() + .transform((value) => (value === '' ? undefined : value)), + phone: z + .string() + .transform((val) => val.replace(/[\s()-]/g, '')) + .refine((val) => isValidPhoneNumber(val, 'RU'), { + message: 'Неверный номер телефона (RU).', + }), + email: z + .string() + .email({ message: 'Неверный адрес электронной почты.' }) + .refine( + (val) => /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(val), + { + message: 'Неверный адрес электронной почты.', + }, + ), + isIncognita: z.boolean(), + }) + .refine( + (data) => { + if (!data.isIncognita) { + return !!data.firstName; + } + return true; + }, + { + message: 'Имя обязательное поле.', + path: ['firstName'], + }, + ) + .refine( + (data) => { + if (!data.isIncognita) { + return !!data.lastName; + } + return true; + }, + { + message: 'Фамилия обязательное поле.', + path: ['lastName'], + }, + ); + +export type TProfileSchema = z.infer; diff --git a/src/components/profile/styled.tsx b/src/components/profile/styled.tsx new file mode 100644 index 0000000..9aeb125 --- /dev/null +++ b/src/components/profile/styled.tsx @@ -0,0 +1,30 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const Profile = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + padding: 20px; + background-color: var(--blue); +`; + +export const ProfileAuthButtons = styled.div` + display: flex; + flex-direction: column; + gap: 20px; + margin: auto 0; + + @media ${devices.tablet} { + margin-right: 40px; + margin-left: 40px; + } + + @media ${devices.mobile} { + margin-right: 0; + margin-left: 0; + } +`; diff --git a/src/components/profile/ui/index.ts b/src/components/profile/ui/index.ts new file mode 100644 index 0000000..0951af1 --- /dev/null +++ b/src/components/profile/ui/index.ts @@ -0,0 +1,3 @@ +export * from './profile-group'; +export * from './profile-button'; +export * from './profile-form'; diff --git a/src/components/profile/ui/modal-avatar/index.ts b/src/components/profile/ui/modal-avatar/index.ts new file mode 100644 index 0000000..d61a937 --- /dev/null +++ b/src/components/profile/ui/modal-avatar/index.ts @@ -0,0 +1 @@ +export * from './modal-avatar'; diff --git a/src/components/profile/ui/modal-avatar/modal-avatar.tsx b/src/components/profile/ui/modal-avatar/modal-avatar.tsx new file mode 100644 index 0000000..087f3d6 --- /dev/null +++ b/src/components/profile/ui/modal-avatar/modal-avatar.tsx @@ -0,0 +1,42 @@ +import { FC } from 'react'; +import { Control, Controller } from 'react-hook-form'; + +import { BackGroup } from '@/shared'; +import { avatars } from '@/utils'; + +import * as Styled from './styled'; + +interface IModalAvatar { + handleClose: () => void; + control: Control; +} +export const ModalAvatar: FC = ({ handleClose, control }) => ( + + + ( + + {avatars.map((avatar) => ( + { + onChange(avatar); + handleClose(); + }} + > + + + + + ))} + + )} + /> + +); diff --git a/src/components/profile/ui/modal-avatar/styled.tsx b/src/components/profile/ui/modal-avatar/styled.tsx new file mode 100644 index 0000000..1433a35 --- /dev/null +++ b/src/components/profile/ui/modal-avatar/styled.tsx @@ -0,0 +1,63 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const ModalAvatar = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + height: 100%; + padding: 40px 20px 0; + background-color: var(--green); + + @media ${devices.tablet} { + padding-right: 40px; + padding-left: 40px; + } + + @media ${devices.mobile} { + padding-right: 20px; + padding-left: 20px; + } +`; + +export const AvatarsGroup = styled.div` + box-sizing: border-box; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(38px, 15%)); + grid-auto-rows: repeat(auto-fit, minmax(38px, auto)); + gap: 15px; + justify-content: center; + width: 100%; + height: 100%; + padding: 20px 20px 40px; + overflow-y: auto; + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +`; + +export const AvatarButton = styled.button` + box-sizing: border-box; + width: 100%; + height: 100%; + margin-bottom: 15px; + cursor: pointer; + filter: drop-shadow(2px 2px 2px rgb(0 0 0 / 50%)); + transition: 0.2s ease; + + &:hover { + filter: drop-shadow(3px 3px 8px rgb(0 0 0 / 80%)); + transform: translateY(-1px); + } + + &:active { + filter: drop-shadow(2px 2px 2px rgb(0 0 0 / 50%)); + transform: translateY(0); + } +`; diff --git a/src/components/profile/ui/phone-input/index.ts b/src/components/profile/ui/phone-input/index.ts new file mode 100644 index 0000000..426d04b --- /dev/null +++ b/src/components/profile/ui/phone-input/index.ts @@ -0,0 +1 @@ +export * from './phone-input'; diff --git a/src/components/profile/ui/phone-input/phone-input.tsx b/src/components/profile/ui/phone-input/phone-input.tsx new file mode 100644 index 0000000..a021316 --- /dev/null +++ b/src/components/profile/ui/phone-input/phone-input.tsx @@ -0,0 +1,42 @@ +import { PatternFormat } from 'react-number-format'; +import { FC } from 'react'; +import { Control, Controller } from 'react-hook-form'; +import classNames from 'classnames'; + +import * as Styled from './styled'; +import styled from '../styles/input.module.css'; + +interface IPhoneInput { + control: Control; +} +export const PhoneInput: FC = ({ control }) => ( + + ( + + + {error?.message && ( + {error.message} + )} + + )} + /> + +); diff --git a/src/components/profile/ui/phone-input/styled.tsx b/src/components/profile/ui/phone-input/styled.tsx new file mode 100644 index 0000000..37c8316 --- /dev/null +++ b/src/components/profile/ui/phone-input/styled.tsx @@ -0,0 +1,38 @@ +import styled from 'styled-components'; + +export const PhoneInput = styled.div` + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; +`; + +export const AuthInputWrapper = styled.div` + position: relative; + margin-bottom: 18px; +`; + +export const UserPhoneInput = styled.input` + width: 100%; + height: 40px; + font-family: ${(props) => props.theme.fonts}; + font-size: 14px; + font-weight: 800; + background-color: transparent; + border: none; +`; + +export const InputError = styled.span` + position: absolute; + top: 26px; + left: 0; + display: block; + width: 300px; + overflow: hidden; + font-size: 12px; + font-weight: 400; + line-height: 12px; + color: var(--red); + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/src/components/profile/ui/profile-button/index.ts b/src/components/profile/ui/profile-button/index.ts new file mode 100644 index 0000000..9e17903 --- /dev/null +++ b/src/components/profile/ui/profile-button/index.ts @@ -0,0 +1 @@ +export * from './profile-button'; diff --git a/src/components/profile/ui/profile-button/profile-button.tsx b/src/components/profile/ui/profile-button/profile-button.tsx new file mode 100644 index 0000000..9902b58 --- /dev/null +++ b/src/components/profile/ui/profile-button/profile-button.tsx @@ -0,0 +1,17 @@ +import { FC, HTMLProps } from 'react'; + +import * as Styled from './styled'; + +interface IProfileButton extends HTMLProps { + title: string; + colorItem?: string; +} +export const ProfileButton: FC = ({ + title, + onClick, + colorItem, +}) => ( + + {title} + +); diff --git a/src/components/profile/ui/profile-button/styled.tsx b/src/components/profile/ui/profile-button/styled.tsx new file mode 100644 index 0000000..fcb526a --- /dev/null +++ b/src/components/profile/ui/profile-button/styled.tsx @@ -0,0 +1,18 @@ +import styled from 'styled-components'; + +export const Button = styled.button<{ $colorItem?: string }>` + width: 100%; + padding: 16px 0 14px; + font-size: 14px; + font-weight: 800; + line-height: 20px; + color: ${(props) => props.$colorItem || 'var(--black)'}; + text-align: center; + letter-spacing: 0; + cursor: pointer; + user-select: none; + background-color: var(--white); + border: 2px solid var(--black-plus); + border-radius: 10px; + box-shadow: 2px 2px 2px rgb(0 0 0 / 25%); +`; diff --git a/src/components/profile/ui/profile-form/index.ts b/src/components/profile/ui/profile-form/index.ts new file mode 100644 index 0000000..f21606f --- /dev/null +++ b/src/components/profile/ui/profile-form/index.ts @@ -0,0 +1 @@ +export * from './profile-form'; diff --git a/src/components/profile/ui/profile-form/profile-form.tsx b/src/components/profile/ui/profile-form/profile-form.tsx new file mode 100644 index 0000000..60fe7a1 --- /dev/null +++ b/src/components/profile/ui/profile-form/profile-form.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { FC, useEffect, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { AnimatePresence } from 'framer-motion'; + +import { + ActionAuthButton, + AuthCheckbox, + AuthInput, + LayoutModal, + Portal, +} from '@/shared'; +import { useAppDispatch, useAppSelector } from '@/hooks'; +import { useUserMutation } from '@/api-hooks'; +import { setUser, userSelector } from '@/store'; + +import * as Styled from './styled'; +import { PhoneInput } from '../phone-input'; +import { ProfileSchema, TProfileSchema } from '../../schema'; +import { ModalAvatar } from '../modal-avatar'; + +interface IProfileForm { + handleCloseEdit: () => void; +} +export const ProfileForm: FC = ({ handleCloseEdit }) => { + const [isModal, setIsModal] = useState(false); + const handleCloseModal = () => setIsModal(false); + const handleAvatarClick = () => setIsModal(true); + const user = useAppSelector(userSelector); + const dispatch = useAppDispatch(); + + const { mutate, data, isPending } = useUserMutation( + user.id?.toString() || '', + ); + + useEffect(() => { + if (data?.success) { + dispatch(setUser({ data: data.user })); + handleCloseEdit(); + } + }, [data, dispatch, handleCloseEdit]); + + const { + watch, + control, + handleSubmit, + formState: { isValid, isDirty }, + } = useForm({ + resolver: zodResolver(ProfileSchema), + defaultValues: { + avatar: user.avatar, + firstName: user.firstName, + lastName: user.lastName, + surname: user.surname, + phone: user.phone, + email: user.email, + isIncognita: JSON.parse(user.isIncognita || 'false'), + }, + mode: 'onTouched', + }); + const isIncognitaValue = watch('isIncognita'); + + const selectedAvatar = watch('avatar'); + const onSubmit: SubmitHandler = (dataForm) => { + mutate(dataForm); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + {isModal && ( + + + + + + )} + + + ); +}; diff --git a/src/components/profile/ui/profile-form/styled.tsx b/src/components/profile/ui/profile-form/styled.tsx new file mode 100644 index 0000000..5c7e1e1 --- /dev/null +++ b/src/components/profile/ui/profile-form/styled.tsx @@ -0,0 +1,75 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const ProfileForm = styled.form` + position: relative; + box-sizing: border-box; + display: grid; + flex-direction: column; + grid-template-columns: 1fr; + grid-auto-rows: auto; + column-gap: 32px; + padding: 20px 20px 30px; + margin-top: 32px; + background-color: var(--white); + border-radius: 15px; + + & :first-child { + grid-column: 1 / -1; + } + + @media ${devices.tablet} { + grid-template-columns: 1fr 1fr; + padding-right: 40px; + padding-left: 40px; + margin-right: 60px; + margin-left: 60px; + } + + @media ${devices.mobile} { + grid-template-columns: 1fr; + padding-right: 20px; + padding-left: 20px; + margin-right: 0; + margin-left: 0; + } +`; + +export const Fieldset = styled.fieldset``; + +export const ButtonWrapper = styled.div` + position: absolute; + right: 22px; + bottom: -22px; + box-shadow: 0 4px 4px rgb(0 0 0 / 25%); +`; + +export const Avatar = styled.button` + width: 48px; + height: 48px; + margin-bottom: 20px; + cursor: pointer; + filter: drop-shadow(2px 2px 2px rgb(0 0 0 / 50%)); + transition: 0.2s ease; + + &:hover { + filter: drop-shadow(3px 3px 8px rgb(0 0 0 / 80%)); + transform: translateY(-1px); + } + + &:active { + filter: drop-shadow(2px 2px 2px rgb(0 0 0 / 50%)); + transform: translateY(0); + } + + @media ${devices.tablet} { + width: 78px; + height: 78px; + } + + @media ${devices.mobile} { + width: 48px; + height: 48px; + } +`; diff --git a/src/components/profile/ui/profile-group/index.ts b/src/components/profile/ui/profile-group/index.ts new file mode 100644 index 0000000..e0cfe69 --- /dev/null +++ b/src/components/profile/ui/profile-group/index.ts @@ -0,0 +1 @@ +export * from './profile-group'; diff --git a/src/components/profile/ui/profile-group/profile-group.tsx b/src/components/profile/ui/profile-group/profile-group.tsx new file mode 100644 index 0000000..d30fd73 --- /dev/null +++ b/src/components/profile/ui/profile-group/profile-group.tsx @@ -0,0 +1,48 @@ +import { FC } from 'react'; +import { PatternFormat } from 'react-number-format'; + +import { ActionButton } from '@/shared'; + +import * as Styled from './styled'; +import { ProfileButton } from '../profile-button'; + +interface IProfileGroup { + handleLogout: () => void; + handleEdit: () => void; + name: string; + phone: string; + avatar: string; +} + +export const ProfileGroup: FC = ({ + handleEdit, + handleLogout, + name, + phone, + avatar, +}) => ( + + + + + + + + + {name} + + + {value}} + value={phone} + /> + + + +); diff --git a/src/components/profile/ui/profile-group/styled.tsx b/src/components/profile/ui/profile-group/styled.tsx new file mode 100644 index 0000000..b9316b4 --- /dev/null +++ b/src/components/profile/ui/profile-group/styled.tsx @@ -0,0 +1,81 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const ProfileGroup = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 40px; + height: 100%; + margin-top: 32px; +`; + +export const ProfileContent = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: column; + padding: 20px; + background-color: var(--white); + border-radius: 15px; + + @media ${devices.tablet} { + margin-right: 60px; + margin-left: 60px; + } + + @media ${devices.mobile} { + margin-right: 0; + margin-left: 0; + } +`; + +export const AvatarWrapper = styled.div` + width: 48px; + height: 48px; +`; + +export const UserEditInfo = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-top: 15px; +`; + +export const UserName = styled.p` + font-size: 16px; + font-weight: 800; + line-height: 22px; + color: var(--black-plus); + text-align: left; + + @media ${devices.tablet} { + font-size: 20px; + line-height: 28px; + } + + @media ${devices.mobile} { + font-size: 16px; + line-height: 22px; + } +`; + +export const UserPhone = styled.p` + margin-top: 6px; + font-size: 13px; + font-weight: 800; + line-height: 16px; + color: var(--yellow); + text-align: left; + + @media ${devices.tablet} { + font-size: 18px; + line-height: 26px; + } + + @media ${devices.mobile} { + font-size: 13px; + line-height: 16px; + } +`; diff --git a/src/components/profile/ui/styles/input.module.css b/src/components/profile/ui/styles/input.module.css new file mode 100644 index 0000000..f91550c --- /dev/null +++ b/src/components/profile/ui/styles/input.module.css @@ -0,0 +1,22 @@ +.input { + color: var(--black-plus); + border-bottom: 2px solid var(--black-plus); + font-size: 14px; + font-weight: 800; + line-height: 20px; + padding-bottom: 2px; + width: 100%; + background-color: transparent; + + &:focus { + outline: none; + } + + &::placeholder { + color: var(--gray-plus); + } +} + +.inputError { + border-bottom: 2px solid var(--red); +} diff --git a/src/page/profile-page/index.ts b/src/page/profile-page/index.ts new file mode 100644 index 0000000..61fc685 --- /dev/null +++ b/src/page/profile-page/index.ts @@ -0,0 +1 @@ +export * from './profile-page'; diff --git a/src/page/profile-page/profile-page.tsx b/src/page/profile-page/profile-page.tsx new file mode 100644 index 0000000..045e6e4 --- /dev/null +++ b/src/page/profile-page/profile-page.tsx @@ -0,0 +1,3 @@ +import { Profile } from '@/components'; + +export const ProfilePage = () => ; -- GitLab From e36f16745297cb275efe534c6c47f90803bd0d2f Mon Sep 17 00:00:00 2001 From: AlexandrValgamov Date: Tue, 4 Jun 2024 18:39:04 +0500 Subject: [PATCH 5/9] Catalog | feat: add register page --- src/app/signup/loading.tsx | 9 + src/app/signup/page.tsx | 14 ++ src/components/register/index.ts | 1 + src/components/register/register.tsx | 159 ++++++++++++++++++ src/components/register/schema/index.ts | 1 + .../register/schema/schema-register-form.ts | 95 +++++++++++ src/components/register/styled.tsx | 75 +++++++++ .../ui/auth-error-modal/auth-error-modal.tsx | 27 +++ .../register/ui/auth-error-modal/index.ts | 1 + .../ui/birth-date-step/birth-date-step.tsx | 76 +++++++++ .../register/ui/birth-date-step/index.ts | 1 + .../register/ui/birth-date-step/styled.tsx | 48 ++++++ .../contact-info-step/contact-info-step.tsx | 55 ++++++ .../register/ui/contact-info-step/index.ts | 1 + .../register/ui/contact-info-step/styled.tsx | 38 +++++ src/components/register/ui/index.ts | 6 + .../register/ui/modal-content/index.ts | 1 + .../ui/modal-content/modal-content.tsx | 25 +++ .../register/ui/modal-content/styled.tsx | 70 ++++++++ .../register/ui/payment-info-step/index.ts | 1 + .../payment-info-step/payment-info-step.tsx | 93 ++++++++++ .../register/ui/payment-info-step/styled.tsx | 44 +++++ .../register/ui/personal-info-step/index.ts | 1 + .../personal-info-step/personal-info-step.tsx | 45 +++++ .../register/ui/personal-info-step/styled.tsx | 52 ++++++ src/components/register/ui/steps/index.ts | 1 + src/components/register/ui/steps/steps.tsx | 36 ++++ .../register/ui/styles/input.module.css | 22 +++ src/page/register-page/index.ts | 1 + src/page/register-page/register-page.tsx | 3 + 30 files changed, 1002 insertions(+) create mode 100644 src/app/signup/loading.tsx create mode 100644 src/app/signup/page.tsx create mode 100644 src/components/register/index.ts create mode 100644 src/components/register/register.tsx create mode 100644 src/components/register/schema/index.ts create mode 100644 src/components/register/schema/schema-register-form.ts create mode 100644 src/components/register/styled.tsx create mode 100644 src/components/register/ui/auth-error-modal/auth-error-modal.tsx create mode 100644 src/components/register/ui/auth-error-modal/index.ts create mode 100644 src/components/register/ui/birth-date-step/birth-date-step.tsx create mode 100644 src/components/register/ui/birth-date-step/index.ts create mode 100644 src/components/register/ui/birth-date-step/styled.tsx create mode 100644 src/components/register/ui/contact-info-step/contact-info-step.tsx create mode 100644 src/components/register/ui/contact-info-step/index.ts create mode 100644 src/components/register/ui/contact-info-step/styled.tsx create mode 100644 src/components/register/ui/index.ts create mode 100644 src/components/register/ui/modal-content/index.ts create mode 100644 src/components/register/ui/modal-content/modal-content.tsx create mode 100644 src/components/register/ui/modal-content/styled.tsx create mode 100644 src/components/register/ui/payment-info-step/index.ts create mode 100644 src/components/register/ui/payment-info-step/payment-info-step.tsx create mode 100644 src/components/register/ui/payment-info-step/styled.tsx create mode 100644 src/components/register/ui/personal-info-step/index.ts create mode 100644 src/components/register/ui/personal-info-step/personal-info-step.tsx create mode 100644 src/components/register/ui/personal-info-step/styled.tsx create mode 100644 src/components/register/ui/steps/index.ts create mode 100644 src/components/register/ui/steps/steps.tsx create mode 100644 src/components/register/ui/styles/input.module.css create mode 100644 src/page/register-page/index.ts create mode 100644 src/page/register-page/register-page.tsx diff --git a/src/app/signup/loading.tsx b/src/app/signup/loading.tsx new file mode 100644 index 0000000..e8b963e --- /dev/null +++ b/src/app/signup/loading.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { Preloader } from '@/vendor'; + +const Loading = () => { + return ; +}; + +export default Loading; diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx new file mode 100644 index 0000000..5cff167 --- /dev/null +++ b/src/app/signup/page.tsx @@ -0,0 +1,14 @@ +import { Suspense } from 'react'; + +import { RegisterPage } from '@/page'; + +import Loading from './loading'; + +const Register = () => { + return ( + }> + + + ); +}; +export default Register; diff --git a/src/components/register/index.ts b/src/components/register/index.ts new file mode 100644 index 0000000..412f070 --- /dev/null +++ b/src/components/register/index.ts @@ -0,0 +1 @@ +export * from './register'; diff --git a/src/components/register/register.tsx b/src/components/register/register.tsx new file mode 100644 index 0000000..dfa58d6 --- /dev/null +++ b/src/components/register/register.tsx @@ -0,0 +1,159 @@ +'use client'; + +import { useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { AnimatePresence } from 'framer-motion'; +import { isAxiosError } from 'axios'; +import { useRouter } from 'next/navigation'; + +import { ActionAuthButton, BackGroup, LayoutModal, Portal } from '@/shared'; +import { AUTH_TITLES } from '@/utils'; +import { useRegisterMutation } from '@/api-hooks'; +import { IGetUserResponse } from '@/interfaces'; + +import * as Styled from './styled'; +import { RegisterSchema, TRegisterSchema } from './schema'; +import { Steps, AuthErrorModal } from './ui'; + +export type TStepName = 1 | 2 | 3 | 4; + +export const Register = () => { + const router = useRouter(); + const handleBackClick = () => { + router.push('/'); + }; + const [step, setStep] = useState(1); + const [isModalError, setIsModalError] = useState(false); + const handleCloseModal = () => setIsModalError(false); + + const handleStepBack = () => + setStep((prev) => { + const count = prev > 1 ? prev - 1 : prev; + return count as TStepName; + }); + + const { + control, + watch, + handleSubmit, + formState: { isSubmitting }, + trigger, + } = useForm({ + resolver: zodResolver(RegisterSchema), + defaultValues: { + step1: { + firstName: '', + lastName: '', + surname: '', + isIncognita: false, + }, + step2: { + birthday: '', + }, + step3: { + typeCard: '', + numberCard: '', + codeCard: '', + }, + step4: { + phone: '', + email: '', + }, + }, + mode: 'onTouched', + }); + + const isIncognitaValue = watch('step1.isIncognita'); + const birthdayValue = watch('step2.birthday'); + const CardTypeValue = watch('step3.typeCard'); + + const handleSuccess = (data: IGetUserResponse) => { + if (data.success) router.replace('/login'); + }; + + const handleError = (error: Error) => { + if (isAxiosError(error) && error.code === '404') router.replace('/404'); + if (isAxiosError(error) && error.code === '500') + router.replace('/server-error'); + setIsModalError(true); + }; + + const { mutate } = useRegisterMutation(handleError, handleSuccess); + + const handleNext = async () => { + const isValid = await trigger(`step${step}`); + if (isValid) { + setStep((prev) => { + const count = prev < 4 ? prev + 1 : prev; + return count as TStepName; + }); + } + }; + + const onSubmit: SubmitHandler = (dataForm) => { + mutate({ + ...dataForm.step1, + ...dataForm.step2, + ...dataForm.step3, + ...dataForm.step4, + }); + }; + + return ( + + + + + + {AUTH_TITLES[step]} + + + + {step !== 1 ? ( + + + + ) : null} + + + {step < 4 ? ( + + ) : ( + + )} + + + + + {isModalError && ( + + + + + + )} + + + ); +}; diff --git a/src/components/register/schema/index.ts b/src/components/register/schema/index.ts new file mode 100644 index 0000000..4bf811b --- /dev/null +++ b/src/components/register/schema/index.ts @@ -0,0 +1 @@ +export * from './schema-register-form'; diff --git a/src/components/register/schema/schema-register-form.ts b/src/components/register/schema/schema-register-form.ts new file mode 100644 index 0000000..1f8bab3 --- /dev/null +++ b/src/components/register/schema/schema-register-form.ts @@ -0,0 +1,95 @@ +import { isValidPhoneNumber } from 'libphonenumber-js'; +import { ZodType, z } from 'zod'; + +const step1Schema = z + .object({ + firstName: z.string().optional(), + lastName: z.string().optional(), + surname: z.string().optional(), + isIncognita: z.boolean(), + }) + .refine( + (data) => { + if (!data.isIncognita) { + return !!data.firstName; + } + return true; + }, + { + message: 'Имя обязательное поле.', + path: ['firstName'], + }, + ) + .refine( + (data) => { + if (!data.isIncognita) { + return !!data.lastName; + } + return true; + }, + { + message: 'Фамилия обязательное поле.', + path: ['lastName'], + }, + ); + +const step2Schema = z.object({ + birthday: z.string().refine((val) => !Number.isNaN(Date.parse(val)), { + message: 'Необходимо заполнить дату рождения.', + }), +}); + +const step3Schema = z + .object({ + typeCard: z.string().transform((val) => val.toUpperCase()), + numberCard: z + .string() + .refine((val) => val.replace(/\D/g, '').length >= 16, { + message: 'Номер карты должен содержать минимум 16 цифр.', + }), + codeCard: z + .string() + .optional() + .refine((val) => (val ? val.replace(/\D/g, '').length === 3 : true), { + message: 'CVC должен содержать 3 цифры.', + }), + }) + .refine( + (data) => { + if (data.typeCard === 'PLASTIC') { + return !!data.codeCard; + } + return true; + }, + { + message: 'CVC обязательное поле.', + path: ['codeCard'], + }, + ); + +const step4Schema = z.object({ + phone: z + .string() + .transform((val) => val.replace(/[\s()-]/g, '')) + .refine((val) => isValidPhoneNumber(val, 'RU'), { + message: 'Неверный номер телефона (RU).', + }), + email: z + .string() + .email({ message: 'Неверный адрес электронной почты.' }) + .refine( + (val) => /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(val), + { + message: 'Неверный адрес электронной почты.', + }, + ), +}); + +export const RegisterSchema: ZodType = z.object({ + step1: step1Schema, + step2: step2Schema, + step3: step3Schema, + step4: step4Schema, +}); + +export type TRegisterSchema = z.infer; diff --git a/src/components/register/styled.tsx b/src/components/register/styled.tsx new file mode 100644 index 0000000..1de2975 --- /dev/null +++ b/src/components/register/styled.tsx @@ -0,0 +1,75 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const Register = styled.div` + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 20px; + background-color: var(--violet); +`; + +export const FormWrapper = styled.div` + box-sizing: border-box; + display: flex; + align-items: flex-start; + width: 100%; + height: 100%; + padding: 64px 28px 0; + background-color: var(--violet); + + @media ${devices.tablet} { + justify-content: center; + padding: 0; + padding-top: 20vh; + } +`; + +export const RegisterForm = styled.form` + position: relative; + display: flex; + flex-direction: column; + gap: 7px; + align-items: center; + width: 100%; + max-width: 328px; + padding: 30px; + background-color: var(--white); + border: 2px solid var(--black-plus); + border-radius: 15px; + box-shadow: 0 4px 4px rgb(0 0 0 / 25%); +`; + +export const Title = styled.p` + align-self: flex-start; + font-family: ${(props) => props.theme.fonts}; + font-size: 14px; + font-weight: 800; + line-height: 20px; + color: var(--black-plus); + + @media ${devices.tablet} { + font-size: 16px; + line-height: 22px; + } + + @media ${devices.mobile} { + font-size: 14px; + line-height: 20px; + } +`; + +export const BackButtonWrapper = styled.div` + position: absolute; + bottom: -22px; + left: 22px; + box-shadow: 0 4px 4px rgb(0 0 0 / 25%); +`; + +export const GoButtonWrapper = styled.div` + position: absolute; + right: 22px; + bottom: -22px; + box-shadow: 0 4px 4px rgb(0 0 0 / 25%); +`; diff --git a/src/components/register/ui/auth-error-modal/auth-error-modal.tsx b/src/components/register/ui/auth-error-modal/auth-error-modal.tsx new file mode 100644 index 0000000..48e4f90 --- /dev/null +++ b/src/components/register/ui/auth-error-modal/auth-error-modal.tsx @@ -0,0 +1,27 @@ +import { Modal } from '@/shared'; + +import { ModalContent } from '../modal-content'; + +export const AuthErrorModal = ({ + handleClose, +}: { + handleClose: () => void; +}) => { + const content = ( + + ); + + return ( + + ); +}; diff --git a/src/components/register/ui/auth-error-modal/index.ts b/src/components/register/ui/auth-error-modal/index.ts new file mode 100644 index 0000000..a5227d9 --- /dev/null +++ b/src/components/register/ui/auth-error-modal/index.ts @@ -0,0 +1 @@ +export * from './auth-error-modal'; diff --git a/src/components/register/ui/birth-date-step/birth-date-step.tsx b/src/components/register/ui/birth-date-step/birth-date-step.tsx new file mode 100644 index 0000000..27cac54 --- /dev/null +++ b/src/components/register/ui/birth-date-step/birth-date-step.tsx @@ -0,0 +1,76 @@ +import { FC, forwardRef } from 'react'; +import { Control, Controller } from 'react-hook-form'; +import ReactDatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; + +import * as Styled from './styled'; + +interface IBirthDateStep { + control: Control; + birthdayValue: Date; +} + +interface CustomInputProps { + value?: string; + onClick?: () => void; +} + +const ExampleCustomInput = forwardRef( + ({ value, onClick }, ref) => ( + + + + + + + + + ), +); + +ExampleCustomInput.displayName = 'ExampleCustomInput'; + +export const BirthDateStep: FC = ({ + control, + birthdayValue, +}) => ( + + ( + <> + } + dateFormat="dd.MM.yyyy" + dropdownMode="select" + name={name} + selected={birthdayValue} + onBlur={onBlur} + onChange={(date) => onChange(date?.toISOString())} + /> + {error?.message && ( + {error.message} + )} + + )} + /> + +); diff --git a/src/components/register/ui/birth-date-step/index.ts b/src/components/register/ui/birth-date-step/index.ts new file mode 100644 index 0000000..c7a2add --- /dev/null +++ b/src/components/register/ui/birth-date-step/index.ts @@ -0,0 +1 @@ +export * from './birth-date-step'; diff --git a/src/components/register/ui/birth-date-step/styled.tsx b/src/components/register/ui/birth-date-step/styled.tsx new file mode 100644 index 0000000..4c9e641 --- /dev/null +++ b/src/components/register/ui/birth-date-step/styled.tsx @@ -0,0 +1,48 @@ +import styled from 'styled-components'; + +export const BirthDate = styled.div` + position: relative; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 7px; + width: 100%; +`; + +export const BirthDateInputWrapper = styled.div` + display: flex; + flex-direction: row; + margin-bottom: 28px; + border-bottom: 2px solid var(--black-plus); +`; + +export const BirthDateInput = styled.input` + width: 100%; + padding-bottom: 2px; + font-size: 14px; + font-weight: 800; + line-height: 20px; + color: var(--black-plus); + + &:focus { + border: none; + outline: none; + } +`; + +export const DateIcon = styled.div``; + +export const InputError = styled.span` + position: absolute; + top: 32px; + left: 0; + display: block; + width: 300px; + overflow: hidden; + font-size: 12px; + font-weight: 400; + line-height: 12px; + color: var(--red); + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/src/components/register/ui/contact-info-step/contact-info-step.tsx b/src/components/register/ui/contact-info-step/contact-info-step.tsx new file mode 100644 index 0000000..2049343 --- /dev/null +++ b/src/components/register/ui/contact-info-step/contact-info-step.tsx @@ -0,0 +1,55 @@ +import { Control, Controller } from 'react-hook-form'; +import { FC } from 'react'; +import { PatternFormat } from 'react-number-format'; +import classNames from 'classnames'; + +import { AuthInput } from '@/shared'; + +import * as Styled from './styled'; +import styled from '../styles/input.module.css'; + +interface IContactInfoStep { + control: Control; + className?: string; +} + +export const ContactInfoStep: FC = ({ + control, + className, +}) => ( + + ( + + + {error?.message && ( + {error.message} + )} + + )} + /> + + + +); diff --git a/src/components/register/ui/contact-info-step/index.ts b/src/components/register/ui/contact-info-step/index.ts new file mode 100644 index 0000000..19a650f --- /dev/null +++ b/src/components/register/ui/contact-info-step/index.ts @@ -0,0 +1 @@ +export * from './contact-info-step'; diff --git a/src/components/register/ui/contact-info-step/styled.tsx b/src/components/register/ui/contact-info-step/styled.tsx new file mode 100644 index 0000000..801ecb8 --- /dev/null +++ b/src/components/register/ui/contact-info-step/styled.tsx @@ -0,0 +1,38 @@ +import styled from 'styled-components'; + +export const ContactInfo = styled.div` + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; + margin-bottom: 28px; +`; + +export const AuthInputWrapper = styled.div` + position: relative; +`; + +export const Label = styled.label` + position: absolute; + display: flex; + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--gray-plus); +`; + +export const InputError = styled.span` + position: absolute; + top: 26px; + left: 0; + display: block; + width: 300px; + overflow: hidden; + font-size: 12px; + font-weight: 400; + line-height: 12px; + color: var(--red); + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/src/components/register/ui/index.ts b/src/components/register/ui/index.ts new file mode 100644 index 0000000..a396541 --- /dev/null +++ b/src/components/register/ui/index.ts @@ -0,0 +1,6 @@ +export * from './personal-info-step'; +export * from './birth-date-step'; +export * from './payment-info-step'; +export * from './contact-info-step'; +export * from './steps'; +export * from './auth-error-modal'; diff --git a/src/components/register/ui/modal-content/index.ts b/src/components/register/ui/modal-content/index.ts new file mode 100644 index 0000000..2c211af --- /dev/null +++ b/src/components/register/ui/modal-content/index.ts @@ -0,0 +1 @@ +export * from './modal-content'; diff --git a/src/components/register/ui/modal-content/modal-content.tsx b/src/components/register/ui/modal-content/modal-content.tsx new file mode 100644 index 0000000..f1145c8 --- /dev/null +++ b/src/components/register/ui/modal-content/modal-content.tsx @@ -0,0 +1,25 @@ +import { FC } from 'react'; + +import * as Styled from './styled'; + +interface IModalContent { + handleClose: () => void; + btnColor: string; + text: string; + subtitle: string; +} + +export const ModalContent: FC = ({ + handleClose, + btnColor, + text, + subtitle, +}) => ( + <> + {text} + {subtitle} + + Ok + + +); diff --git a/src/components/register/ui/modal-content/styled.tsx b/src/components/register/ui/modal-content/styled.tsx new file mode 100644 index 0000000..1bf9900 --- /dev/null +++ b/src/components/register/ui/modal-content/styled.tsx @@ -0,0 +1,70 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const MessageText = styled.p` + font-family: ${(props) => props.theme.fonts}; + font-size: 18px; + font-weight: 800; + line-height: 26px; + color: var(--black-plus); + + @media ${devices.tablet} { + font-size: 20px; + line-height: 28px; + } + + @media ${devices.mobile} { + font-size: 18px; + line-height: 26px; + } +`; + +export const MessageSubtitle = styled.p` + font-family: ${(props) => props.theme.fonts}; + font-size: 13px; + font-weight: 800; + line-height: 16px; + color: var(--gray); + + @media ${devices.tablet} { + font-size: 16px; + line-height: 22px; + } + + @media ${devices.mobile} { + font-size: 13px; + line-height: 16px; + } +`; + +export const PaymentCheckButton = styled.button<{ $btnColor: string }>` + position: absolute; + right: 22px; + bottom: -20px; + display: inline-block; + padding: 12px 30px; + font-size: 14px; + font-weight: 800; + line-height: 20px; + color: var(--white); + text-align: center; + letter-spacing: 0; + cursor: pointer; + user-select: none; + background-color: var(${({ $btnColor }) => $btnColor}); + border: 2px solid var(--black-plus); + border-radius: 5px; + box-shadow: 2px 2px 2px rgb(0 0 0 / 25%); + transition: 0.2s ease; + + &:hover { + box-shadow: 3px 3px 8px rgb(0 0 0 / 30%); + transform: translateY(-1px); + } + + &:active { + box-shadow: initial; + transform: translateY(0); + } +`; diff --git a/src/components/register/ui/payment-info-step/index.ts b/src/components/register/ui/payment-info-step/index.ts new file mode 100644 index 0000000..000ee2b --- /dev/null +++ b/src/components/register/ui/payment-info-step/index.ts @@ -0,0 +1 @@ +export * from './payment-info-step'; diff --git a/src/components/register/ui/payment-info-step/payment-info-step.tsx b/src/components/register/ui/payment-info-step/payment-info-step.tsx new file mode 100644 index 0000000..1f022c3 --- /dev/null +++ b/src/components/register/ui/payment-info-step/payment-info-step.tsx @@ -0,0 +1,93 @@ +import { Control, Controller } from 'react-hook-form'; +import { FC } from 'react'; +import { PatternFormat } from 'react-number-format'; +import classNames from 'classnames'; + +import { CustomSelect } from '@/shared'; + +import * as Styled from './styled'; +import styled from '../styles/input.module.css'; + +interface IPaymentInfoStep { + control: Control; + CardTypeValue: string; +} + +const paymentOptions = [ + { value: 'PLASTIC', label: 'Пластиковая' }, + { value: 'VIRTUAL', label: 'Виртуальная' }, +]; + +export const PaymentInfoStep: FC = ({ + control, + CardTypeValue, +}) => ( + + + + {CardTypeValue && ( + ( + + + {error?.message && ( + {error.message} + )} + + )} + /> + )} + + {CardTypeValue === 'PLASTIC' && ( + ( + + + {error?.message && ( + {error.message} + )} + + )} + /> + )} + +); diff --git a/src/components/register/ui/payment-info-step/styled.tsx b/src/components/register/ui/payment-info-step/styled.tsx new file mode 100644 index 0000000..708ab29 --- /dev/null +++ b/src/components/register/ui/payment-info-step/styled.tsx @@ -0,0 +1,44 @@ +import styled from 'styled-components'; + +export const PaymentInfo = styled.div` + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; + margin-top: 13px; + margin-bottom: 28px; +`; + +export const CardCodeWrapper = styled.div` + position: relative; + width: 47px; +`; + +export const AuthInputWrapper = styled.div` + position: relative; +`; + +export const Label = styled.label` + position: absolute; + display: flex; + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--gray-plus); +`; + +export const InputError = styled.span` + position: absolute; + top: 26px; + left: 0; + display: block; + width: 300px; + overflow: hidden; + font-size: 12px; + font-weight: 400; + line-height: 12px; + color: var(--red); + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/src/components/register/ui/personal-info-step/index.ts b/src/components/register/ui/personal-info-step/index.ts new file mode 100644 index 0000000..4c7ce0f --- /dev/null +++ b/src/components/register/ui/personal-info-step/index.ts @@ -0,0 +1 @@ +export * from './personal-info-step'; diff --git a/src/components/register/ui/personal-info-step/personal-info-step.tsx b/src/components/register/ui/personal-info-step/personal-info-step.tsx new file mode 100644 index 0000000..f87109a --- /dev/null +++ b/src/components/register/ui/personal-info-step/personal-info-step.tsx @@ -0,0 +1,45 @@ +import { Control } from 'react-hook-form'; +import { FC } from 'react'; + +import { AuthCheckbox, AuthInput } from '@/shared'; + +import * as Styled from './styled'; + +interface IPersonalInfoStep { + control: Control; + isIncognitaValue: boolean; +} + +export const PersonalInfoStep: FC = ({ + control, + isIncognitaValue, +}) => ( + + + + + + + *при наличии + + + + +); diff --git a/src/components/register/ui/personal-info-step/styled.tsx b/src/components/register/ui/personal-info-step/styled.tsx new file mode 100644 index 0000000..d7a0320 --- /dev/null +++ b/src/components/register/ui/personal-info-step/styled.tsx @@ -0,0 +1,52 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const PersonalInfo = styled.div` + display: flex; + flex-direction: column; + gap: 7px; + width: 100%; +`; + +export const Fieldset = styled.fieldset` + display: flex; + flex-direction: column; +`; + +export const NameInputGroup = styled.div``; + +export const SurnameInputGroup = styled.div` + position: relative; +`; + +export const IncognitoButton = styled.button` + margin-top: 2px; + font-size: 14px; + line-height: 20px; + color: var(--green); +`; + +export const Info = styled.p` + position: absolute; + top: 26px; + font-size: 9px; + line-height: 12px; + color: var(--grey); + + @media ${devices.tablet} { + font-size: 11px; + line-height: 16px; + } + + @media ${devices.mobile} { + font-size: 9px; + line-height: 12px; + } +`; + +export const GoButtonWrapper = styled.div` + position: absolute; + right: 13px; + bottom: -19px; +`; diff --git a/src/components/register/ui/steps/index.ts b/src/components/register/ui/steps/index.ts new file mode 100644 index 0000000..9487d67 --- /dev/null +++ b/src/components/register/ui/steps/index.ts @@ -0,0 +1 @@ +export * from './steps'; diff --git a/src/components/register/ui/steps/steps.tsx b/src/components/register/ui/steps/steps.tsx new file mode 100644 index 0000000..a8d622f --- /dev/null +++ b/src/components/register/ui/steps/steps.tsx @@ -0,0 +1,36 @@ +import { Control } from 'react-hook-form'; +import { FC } from 'react'; + +import { BirthDateStep } from '../birth-date-step'; +import { ContactInfoStep } from '../contact-info-step'; +import { PaymentInfoStep } from '../payment-info-step'; +import { PersonalInfoStep } from '../personal-info-step'; + +type TStepName = 1 | 2 | 3 | 4; + +interface ISteps { + step: TStepName; + control: Control; + CardTypeValue: string; + isIncognitaValue: boolean; + birthdayValue: Date; +} + +export const Steps: FC = ({ + step, + control, + CardTypeValue, + isIncognitaValue, + birthdayValue, +}) => { + const stepsComponents = { + 1: ( + + ), + 2: , + 3: , + 4: , + }; + + return stepsComponents[step]; +}; diff --git a/src/components/register/ui/styles/input.module.css b/src/components/register/ui/styles/input.module.css new file mode 100644 index 0000000..f91550c --- /dev/null +++ b/src/components/register/ui/styles/input.module.css @@ -0,0 +1,22 @@ +.input { + color: var(--black-plus); + border-bottom: 2px solid var(--black-plus); + font-size: 14px; + font-weight: 800; + line-height: 20px; + padding-bottom: 2px; + width: 100%; + background-color: transparent; + + &:focus { + outline: none; + } + + &::placeholder { + color: var(--gray-plus); + } +} + +.inputError { + border-bottom: 2px solid var(--red); +} diff --git a/src/page/register-page/index.ts b/src/page/register-page/index.ts new file mode 100644 index 0000000..0513956 --- /dev/null +++ b/src/page/register-page/index.ts @@ -0,0 +1 @@ +export * from './register-page'; diff --git a/src/page/register-page/register-page.tsx b/src/page/register-page/register-page.tsx new file mode 100644 index 0000000..bf079a5 --- /dev/null +++ b/src/page/register-page/register-page.tsx @@ -0,0 +1,3 @@ +import { Register } from '@/components'; + +export const RegisterPage = () => ; -- GitLab From 53ba7bb8131cec88957a8c67d70a35e883fa21e8 Mon Sep 17 00:00:00 2001 From: AlexandrValgamov Date: Tue, 4 Jun 2024 18:40:22 +0500 Subject: [PATCH 6/9] Catalog | feat: add login page --- src/app/login/loading.tsx | 9 ++ src/app/login/page.tsx | 14 ++ src/components/login/index.ts | 1 + src/components/login/login.tsx | 132 ++++++++++++++++++ src/components/login/schema/index.ts | 1 + .../login/schema/schema-login-form.ts | 13 ++ src/components/login/styled.tsx | 84 +++++++++++ src/components/login/styles/input.module.css | 22 +++ .../login/ui/error-modal/error-modal.tsx | 33 +++++ src/components/login/ui/error-modal/index.ts | 1 + .../login/ui/error-modal/styled.tsx | 9 ++ src/components/login/ui/index.ts | 1 + .../login/ui/modal-content/index.ts | 1 + .../login/ui/modal-content/modal-content.tsx | 25 ++++ .../login/ui/modal-content/styled.tsx | 70 ++++++++++ src/page/login-page/index.ts | 1 + src/page/login-page/login-page.tsx | 3 + 17 files changed, 420 insertions(+) create mode 100644 src/app/login/loading.tsx create mode 100644 src/app/login/page.tsx create mode 100644 src/components/login/index.ts create mode 100644 src/components/login/login.tsx create mode 100644 src/components/login/schema/index.ts create mode 100644 src/components/login/schema/schema-login-form.ts create mode 100644 src/components/login/styled.tsx create mode 100644 src/components/login/styles/input.module.css create mode 100644 src/components/login/ui/error-modal/error-modal.tsx create mode 100644 src/components/login/ui/error-modal/index.ts create mode 100644 src/components/login/ui/error-modal/styled.tsx create mode 100644 src/components/login/ui/index.ts create mode 100644 src/components/login/ui/modal-content/index.ts create mode 100644 src/components/login/ui/modal-content/modal-content.tsx create mode 100644 src/components/login/ui/modal-content/styled.tsx create mode 100644 src/page/login-page/index.ts create mode 100644 src/page/login-page/login-page.tsx diff --git a/src/app/login/loading.tsx b/src/app/login/loading.tsx new file mode 100644 index 0000000..e8b963e --- /dev/null +++ b/src/app/login/loading.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { Preloader } from '@/vendor'; + +const Loading = () => { + return ; +}; + +export default Loading; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..892517d --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,14 @@ +import { Suspense } from 'react'; + +import { LoginPage } from '@/page'; + +import Loading from './loading'; + +const Login = () => { + return ( + }> + + + ); +}; +export default Login; diff --git a/src/components/login/index.ts b/src/components/login/index.ts new file mode 100644 index 0000000..6cc1e6e --- /dev/null +++ b/src/components/login/index.ts @@ -0,0 +1 @@ +export * from './login'; diff --git a/src/components/login/login.tsx b/src/components/login/login.tsx new file mode 100644 index 0000000..5cb1491 --- /dev/null +++ b/src/components/login/login.tsx @@ -0,0 +1,132 @@ +'use client'; + +import { Controller, SubmitHandler, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import classNames from 'classnames'; +import { PatternFormat } from 'react-number-format'; +import { useState } from 'react'; +import { AnimatePresence } from 'framer-motion'; +import { isAxiosError } from 'axios'; +import { useRouter } from 'next/navigation'; + +import { useLoginMutation } from '@/api-hooks'; +import { + ActionAuthButton, + BackGroup, + LayoutModal, + Overlay, + Portal, +} from '@/shared'; +import { ILoginResponse } from '@/interfaces'; +import { useAppDispatch } from '@/hooks'; +import { setToken, setUser } from '@/store'; + +import styled from './styles/input.module.css'; +import * as Styled from './styled'; +import { LoginSchema, TLoginSchema } from './schema'; +import { AuthErrorModal } from './ui'; + +export const Login = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const [localError, setLocalError] = useState(null); + + const handleBackClick = () => { + router.push('/'); + }; + + const { control, handleSubmit, reset } = useForm({ + resolver: zodResolver(LoginSchema), + defaultValues: { + phone: '', + }, + }); + + const handleSuccess = (data: ILoginResponse) => { + if (data?.success) { + dispatch(setUser({ data: data.user })); + dispatch(setToken({ data: data.accessToken })); + router.replace('/profile'); + } + }; + + const handleError = (error: Error) => { + if (isAxiosError(error) && error.code === '404') router.replace('/404'); + if (isAxiosError(error) && error.code === '500') + router.replace('/server-error'); + setLocalError(error); + }; + + const { mutate, isPending } = useLoginMutation(handleError, handleSuccess); + + const onSubmit: SubmitHandler = (dataForm) => { + mutate(dataForm); + }; + + const handleCloseModal = () => { + reset(); + setLocalError(null); + }; + + return ( + + + + + + Введите номер + + ( + + + {error?.message && ( + {error.message} + )} + + )} + /> + + + + + + + + {localError && ( + + + + + + + + )} + + + ); +}; diff --git a/src/components/login/schema/index.ts b/src/components/login/schema/index.ts new file mode 100644 index 0000000..1470562 --- /dev/null +++ b/src/components/login/schema/index.ts @@ -0,0 +1 @@ +export * from './schema-login-form'; diff --git a/src/components/login/schema/schema-login-form.ts b/src/components/login/schema/schema-login-form.ts new file mode 100644 index 0000000..7395557 --- /dev/null +++ b/src/components/login/schema/schema-login-form.ts @@ -0,0 +1,13 @@ +import { isValidPhoneNumber } from 'libphonenumber-js'; +import { ZodType, z } from 'zod'; + +export const LoginSchema: ZodType = z.object({ + phone: z + .string() + .transform((val) => val.replace(/[\s()-]/g, '')) + .refine((val) => isValidPhoneNumber(val, 'RU'), { + message: 'Неверный номер телефона (RU).', + }), +}); + +export type TLoginSchema = z.infer; diff --git a/src/components/login/styled.tsx b/src/components/login/styled.tsx new file mode 100644 index 0000000..3ebdde4 --- /dev/null +++ b/src/components/login/styled.tsx @@ -0,0 +1,84 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const Login = styled.div` + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 20px; + background-color: var(--violet); +`; + +export const FormWrapper = styled.div` + box-sizing: border-box; + display: flex; + align-items: flex-start; + width: 100%; + height: 100%; + padding: 64px 28px 0; + background-color: var(--violet); + + @media ${devices.tablet} { + justify-content: center; + padding: 0; + padding-top: 20vh; + } +`; + +export const LoginForm = styled.form` + position: relative; + display: flex; + flex-direction: column; + gap: 28px; + align-items: center; + width: 100%; + max-width: 328px; + padding: 34px; + background-color: var(--white); + border: 2px solid var(--black-plus); + border-radius: 15px; + box-shadow: 0 4px 4px rgb(0 0 0 / 25%); +`; + +export const Fieldset = styled.fieldset` + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; +`; + +export const AuthInputWrapper = styled.div` + position: relative; +`; + +export const Title = styled.p` + align-self: flex-start; + font-family: ${(props) => props.theme.fonts}; + font-size: 14px; + font-weight: 800; + line-height: 20px; + color: var(--black-plus); +`; + +export const GoButtonWrapper = styled.div` + position: absolute; + right: 22px; + bottom: -22px; + box-shadow: 0 4px 4px rgb(0 0 0 / 25%); +`; + +export const InputError = styled.span` + position: absolute; + top: 26px; + left: 0; + display: block; + width: 300px; + overflow: hidden; + font-size: 12px; + font-weight: 400; + line-height: 12px; + color: var(--red); + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/src/components/login/styles/input.module.css b/src/components/login/styles/input.module.css new file mode 100644 index 0000000..fbcb5c8 --- /dev/null +++ b/src/components/login/styles/input.module.css @@ -0,0 +1,22 @@ +.input { + color: var(--black-plus); + border-bottom: 2px solid var(--black-plus); + font-size: 14px; + font-weight: 800; + line-height: 20px; + padding-bottom: 2px; + width: 100%; + background-color: transparent; + + &:focus { + outline: none; + } + + &::placeholder { + color: var(--gray-plus); + } +} + +.inputError { + border-bottom: 2px solid var(--red); +} \ No newline at end of file diff --git a/src/components/login/ui/error-modal/error-modal.tsx b/src/components/login/ui/error-modal/error-modal.tsx new file mode 100644 index 0000000..008e072 --- /dev/null +++ b/src/components/login/ui/error-modal/error-modal.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; + +import { Modal } from '@/shared'; + +import { ModalContent } from '../modal-content'; + +interface IAuthErrorModal { + handleClose: () => void; + message: string; +} + +export const AuthErrorModal: FC = ({ + handleClose, + message, +}) => { + const content = ( + + ); + + return ( + + ); +}; diff --git a/src/components/login/ui/error-modal/index.ts b/src/components/login/ui/error-modal/index.ts new file mode 100644 index 0000000..2ac788b --- /dev/null +++ b/src/components/login/ui/error-modal/index.ts @@ -0,0 +1 @@ +export * from './error-modal'; diff --git a/src/components/login/ui/error-modal/styled.tsx b/src/components/login/ui/error-modal/styled.tsx new file mode 100644 index 0000000..0230ce0 --- /dev/null +++ b/src/components/login/ui/error-modal/styled.tsx @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +export const ErrorModal = styled.div` + display: flex; + flex-direction: column; + gap: 7px; + align-items: center; + width: 100%; +`; diff --git a/src/components/login/ui/index.ts b/src/components/login/ui/index.ts new file mode 100644 index 0000000..2ac788b --- /dev/null +++ b/src/components/login/ui/index.ts @@ -0,0 +1 @@ +export * from './error-modal'; diff --git a/src/components/login/ui/modal-content/index.ts b/src/components/login/ui/modal-content/index.ts new file mode 100644 index 0000000..2c211af --- /dev/null +++ b/src/components/login/ui/modal-content/index.ts @@ -0,0 +1 @@ +export * from './modal-content'; diff --git a/src/components/login/ui/modal-content/modal-content.tsx b/src/components/login/ui/modal-content/modal-content.tsx new file mode 100644 index 0000000..f1145c8 --- /dev/null +++ b/src/components/login/ui/modal-content/modal-content.tsx @@ -0,0 +1,25 @@ +import { FC } from 'react'; + +import * as Styled from './styled'; + +interface IModalContent { + handleClose: () => void; + btnColor: string; + text: string; + subtitle: string; +} + +export const ModalContent: FC = ({ + handleClose, + btnColor, + text, + subtitle, +}) => ( + <> + {text} + {subtitle} + + Ok + + +); diff --git a/src/components/login/ui/modal-content/styled.tsx b/src/components/login/ui/modal-content/styled.tsx new file mode 100644 index 0000000..1bf9900 --- /dev/null +++ b/src/components/login/ui/modal-content/styled.tsx @@ -0,0 +1,70 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const MessageText = styled.p` + font-family: ${(props) => props.theme.fonts}; + font-size: 18px; + font-weight: 800; + line-height: 26px; + color: var(--black-plus); + + @media ${devices.tablet} { + font-size: 20px; + line-height: 28px; + } + + @media ${devices.mobile} { + font-size: 18px; + line-height: 26px; + } +`; + +export const MessageSubtitle = styled.p` + font-family: ${(props) => props.theme.fonts}; + font-size: 13px; + font-weight: 800; + line-height: 16px; + color: var(--gray); + + @media ${devices.tablet} { + font-size: 16px; + line-height: 22px; + } + + @media ${devices.mobile} { + font-size: 13px; + line-height: 16px; + } +`; + +export const PaymentCheckButton = styled.button<{ $btnColor: string }>` + position: absolute; + right: 22px; + bottom: -20px; + display: inline-block; + padding: 12px 30px; + font-size: 14px; + font-weight: 800; + line-height: 20px; + color: var(--white); + text-align: center; + letter-spacing: 0; + cursor: pointer; + user-select: none; + background-color: var(${({ $btnColor }) => $btnColor}); + border: 2px solid var(--black-plus); + border-radius: 5px; + box-shadow: 2px 2px 2px rgb(0 0 0 / 25%); + transition: 0.2s ease; + + &:hover { + box-shadow: 3px 3px 8px rgb(0 0 0 / 30%); + transform: translateY(-1px); + } + + &:active { + box-shadow: initial; + transform: translateY(0); + } +`; diff --git a/src/page/login-page/index.ts b/src/page/login-page/index.ts new file mode 100644 index 0000000..a4d80b3 --- /dev/null +++ b/src/page/login-page/index.ts @@ -0,0 +1 @@ +export * from './login-page'; diff --git a/src/page/login-page/login-page.tsx b/src/page/login-page/login-page.tsx new file mode 100644 index 0000000..67c8d1f --- /dev/null +++ b/src/page/login-page/login-page.tsx @@ -0,0 +1,3 @@ +import { Login } from '@/components'; + +export const LoginPage = () => ; -- GitLab From 6126adb46d92606a02d027b8388e124cd56b20dc Mon Sep 17 00:00:00 2001 From: AlexandrValgamov Date: Tue, 4 Jun 2024 18:41:04 +0500 Subject: [PATCH 7/9] Catalog | feat: add cart --- src/components/cart/cart.tsx | 92 +++++++ src/components/cart/index.ts | 2 + src/components/cart/styled.tsx | 43 ++++ .../ui/cart-add-button/cart-add-button.tsx | 31 +++ .../cart/ui/cart-add-button/index.ts | 1 + .../cart/ui/cart-add-button/styled.tsx | 35 +++ .../cart/ui/cart-form/cart-form.tsx | 237 ++++++++++++++++++ src/components/cart/ui/cart-form/index.ts | 2 + .../cart/ui/cart-form/schema/index.ts | 1 + .../ui/cart-form/schema/schema-cart-form.ts | 27 ++ src/components/cart/ui/cart-form/styled.tsx | 72 ++++++ .../ui/cart-form/utils/constants/constants.ts | 11 + .../ui/cart-form/utils/constants/index.ts | 1 + .../cart-form/utils/helpers/get-shop-list.ts | 17 ++ .../cart/ui/cart-form/utils/helpers/index.ts | 1 + .../cart/ui/cart-form/utils/index.ts | 2 + .../cart/ui/cart-header/cart-header.tsx | 28 +++ src/components/cart/ui/cart-header/index.ts | 1 + src/components/cart/ui/cart-header/styled.tsx | 25 ++ .../cart-item-control/cart-item-control.tsx | 37 +++ .../cart/ui/cart-item-control/index.ts | 1 + .../cart/ui/cart-item-control/styled.tsx | 38 +++ .../cart/ui/cart-payment/cart-payment.tsx | 44 ++++ src/components/cart/ui/cart-payment/index.ts | 1 + .../cart/ui/cart-payment/styled.tsx | 95 +++++++ .../cart-product-card/cart-product-card.tsx | 62 +++++ .../cart/ui/cart-product-card/index.ts | 1 + .../cart/ui/cart-product-card/styled.tsx | 72 ++++++ .../checkbox-cart-button.tsx | 48 ++++ .../cart/ui/checkbox-cart-button/index.ts | 1 + .../cart/ui/checkbox-cart-button/styled.tsx | 59 +++++ src/components/cart/ui/index.ts | 11 + src/components/cart/ui/modals/index.ts | 3 + .../cart/ui/modals/modal-content/index.ts | 1 + .../ui/modals/modal-content/modal-content.tsx | 27 ++ .../cart/ui/modals/modal-content/styled.tsx | 70 ++++++ .../ui/modals/payment-check-modal/index.ts | 1 + .../payment-check-modal.tsx | 29 +++ .../ui/modals/payment-error-modal/index.ts | 1 + .../payment-error-modal.tsx | 29 +++ .../ui/modals/payment-success-modal/index.ts | 1 + .../payment-success-modal.tsx | 45 ++++ .../cart/ui/product-list-group/index.ts | 1 + .../product-list-group/product-list-group.tsx | 57 +++++ .../cart/ui/product-list-group/styled.tsx | 51 ++++ .../cart/ui/radio-cart-button/index.ts | 1 + .../radio-cart-button/radio-cart-button.tsx | 43 ++++ .../cart/ui/radio-cart-button/styled.tsx | 24 ++ .../cart/ui/radio-with-text/index.ts | 1 + .../ui/radio-with-text/radio-with-text.tsx | 40 +++ .../cart/ui/radio-with-text/styled.tsx | 31 +++ 51 files changed, 1555 insertions(+) create mode 100644 src/components/cart/cart.tsx create mode 100644 src/components/cart/index.ts create mode 100644 src/components/cart/styled.tsx create mode 100644 src/components/cart/ui/cart-add-button/cart-add-button.tsx create mode 100644 src/components/cart/ui/cart-add-button/index.ts create mode 100644 src/components/cart/ui/cart-add-button/styled.tsx create mode 100644 src/components/cart/ui/cart-form/cart-form.tsx create mode 100644 src/components/cart/ui/cart-form/index.ts create mode 100644 src/components/cart/ui/cart-form/schema/index.ts create mode 100644 src/components/cart/ui/cart-form/schema/schema-cart-form.ts create mode 100644 src/components/cart/ui/cart-form/styled.tsx create mode 100644 src/components/cart/ui/cart-form/utils/constants/constants.ts create mode 100644 src/components/cart/ui/cart-form/utils/constants/index.ts create mode 100644 src/components/cart/ui/cart-form/utils/helpers/get-shop-list.ts create mode 100644 src/components/cart/ui/cart-form/utils/helpers/index.ts create mode 100644 src/components/cart/ui/cart-form/utils/index.ts create mode 100644 src/components/cart/ui/cart-header/cart-header.tsx create mode 100644 src/components/cart/ui/cart-header/index.ts create mode 100644 src/components/cart/ui/cart-header/styled.tsx create mode 100644 src/components/cart/ui/cart-item-control/cart-item-control.tsx create mode 100644 src/components/cart/ui/cart-item-control/index.ts create mode 100644 src/components/cart/ui/cart-item-control/styled.tsx create mode 100644 src/components/cart/ui/cart-payment/cart-payment.tsx create mode 100644 src/components/cart/ui/cart-payment/index.ts create mode 100644 src/components/cart/ui/cart-payment/styled.tsx create mode 100644 src/components/cart/ui/cart-product-card/cart-product-card.tsx create mode 100644 src/components/cart/ui/cart-product-card/index.ts create mode 100644 src/components/cart/ui/cart-product-card/styled.tsx create mode 100644 src/components/cart/ui/checkbox-cart-button/checkbox-cart-button.tsx create mode 100644 src/components/cart/ui/checkbox-cart-button/index.ts create mode 100644 src/components/cart/ui/checkbox-cart-button/styled.tsx create mode 100644 src/components/cart/ui/index.ts create mode 100644 src/components/cart/ui/modals/index.ts create mode 100644 src/components/cart/ui/modals/modal-content/index.ts create mode 100644 src/components/cart/ui/modals/modal-content/modal-content.tsx create mode 100644 src/components/cart/ui/modals/modal-content/styled.tsx create mode 100644 src/components/cart/ui/modals/payment-check-modal/index.ts create mode 100644 src/components/cart/ui/modals/payment-check-modal/payment-check-modal.tsx create mode 100644 src/components/cart/ui/modals/payment-error-modal/index.ts create mode 100644 src/components/cart/ui/modals/payment-error-modal/payment-error-modal.tsx create mode 100644 src/components/cart/ui/modals/payment-success-modal/index.ts create mode 100644 src/components/cart/ui/modals/payment-success-modal/payment-success-modal.tsx create mode 100644 src/components/cart/ui/product-list-group/index.ts create mode 100644 src/components/cart/ui/product-list-group/product-list-group.tsx create mode 100644 src/components/cart/ui/product-list-group/styled.tsx create mode 100644 src/components/cart/ui/radio-cart-button/index.ts create mode 100644 src/components/cart/ui/radio-cart-button/radio-cart-button.tsx create mode 100644 src/components/cart/ui/radio-cart-button/styled.tsx create mode 100644 src/components/cart/ui/radio-with-text/index.ts create mode 100644 src/components/cart/ui/radio-with-text/radio-with-text.tsx create mode 100644 src/components/cart/ui/radio-with-text/styled.tsx diff --git a/src/components/cart/cart.tsx b/src/components/cart/cart.tsx new file mode 100644 index 0000000..aa4372a --- /dev/null +++ b/src/components/cart/cart.tsx @@ -0,0 +1,92 @@ +'use client'; + +import React, { FC, useState } from 'react'; +import { AnimatePresence } from 'framer-motion'; + +import { Stylebook } from '@/interfaces'; +import { useAppSelector } from '@/hooks'; +import { LayoutModal, Portal } from '@/shared'; +import { basketSelector } from '@/store'; + +import * as Styled from './styled'; +import { + CartHeader, + CartForm, + PaymentCheckModal, + PaymentErrorModal, + PaymentSuccessModal, +} from './ui'; + +interface ICartProps { + handleCloseCart: () => void; + shop: string; + stylebook: Stylebook | null; +} +export const Cart: FC = ({ handleCloseCart, shop, stylebook }) => { + const basket = useAppSelector(basketSelector); + const [isModalCheck, setIsModalCheck] = useState(false); + const [isModalError, setIsModalError] = useState(false); + const [isModalSuccess, setIsModalSuccess] = useState(false); + + const handleOpenModal = (status: string) => { + if (status === 'success') setIsModalSuccess(true); + if (status === 'check') setIsModalCheck(true); + if (status === 'error') setIsModalError(true); + }; + const handleCloseModal = () => { + setIsModalCheck(false); + setIsModalSuccess(false); + setIsModalError(false); + }; + + return ( + { + e.stopPropagation(); + }} + > + + + + + + + {isModalCheck && ( + + + + + + )} + + + {isModalError && ( + + + + + + )} + + + {isModalSuccess && ( + + + + + + )} + + + ); +}; diff --git a/src/components/cart/index.ts b/src/components/cart/index.ts new file mode 100644 index 0000000..570b018 --- /dev/null +++ b/src/components/cart/index.ts @@ -0,0 +1,2 @@ +export * from './cart'; +export * from './ui'; diff --git a/src/components/cart/styled.tsx b/src/components/cart/styled.tsx new file mode 100644 index 0000000..40bcc11 --- /dev/null +++ b/src/components/cart/styled.tsx @@ -0,0 +1,43 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const Cart = styled.div` + position: absolute; + inset: 0; + z-index: 2; + display: flex; + flex-direction: column; + justify-content: end; + + @media ${devices.tablet} { + inset: auto; + right: 0; + bottom: 0; + width: 50vw; + height: 90vh; + max-height: 800px; + } + + @media ${devices.mobile} { + right: 0; + bottom: 0; + left: 0; + width: auto; + } +`; + +export const TopPattern = styled.div` + width: 100%; + height: 16px; + background-color: var(--white); + mask: url('/patterns/modal-waves.png'); +`; + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + min-height: 90%; + padding-bottom: 24px; + background-color: var(--white); +`; diff --git a/src/components/cart/ui/cart-add-button/cart-add-button.tsx b/src/components/cart/ui/cart-add-button/cart-add-button.tsx new file mode 100644 index 0000000..2cef247 --- /dev/null +++ b/src/components/cart/ui/cart-add-button/cart-add-button.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { FC } from 'react'; + +import { Icon } from '@/shared'; + +import * as Styled from './styled'; + +interface AddButtonProps { + onIncrease: () => void; + onDecrease: () => void; + value: number; + colorItem?: string; +} + +export const AddButton: FC = ({ + value, + onIncrease, + onDecrease, + colorItem, +}) => ( + + + + + {value} + + + + +); diff --git a/src/components/cart/ui/cart-add-button/index.ts b/src/components/cart/ui/cart-add-button/index.ts new file mode 100644 index 0000000..c60ddbc --- /dev/null +++ b/src/components/cart/ui/cart-add-button/index.ts @@ -0,0 +1 @@ +export * from './cart-add-button'; diff --git a/src/components/cart/ui/cart-add-button/styled.tsx b/src/components/cart/ui/cart-add-button/styled.tsx new file mode 100644 index 0000000..f4f5309 --- /dev/null +++ b/src/components/cart/ui/cart-add-button/styled.tsx @@ -0,0 +1,35 @@ +import styled from 'styled-components'; + +interface AddButtonProps { + $colorItem?: string; +} + +export const Counter = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + height: 100%; + color: var(--green-minus); + user-select: none; + background-color: var(--white); + border: 2px solid var(--black-plus); + border-radius: 5px; +`; + +export const CounterButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; + color: ${({ $colorItem }) => $colorItem || 'black'}; + cursor: pointer; +`; + +export const Count = styled.p` + font-family: Gilroy, sans-serif; + font-size: 12px; + font-weight: 800; + color: ${({ $colorItem }) => $colorItem || 'black'}; +`; diff --git a/src/components/cart/ui/cart-form/cart-form.tsx b/src/components/cart/ui/cart-form/cart-form.tsx new file mode 100644 index 0000000..76721c2 --- /dev/null +++ b/src/components/cart/ui/cart-form/cart-form.tsx @@ -0,0 +1,237 @@ +'use client'; + +import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FC, useEffect, useMemo } from 'react'; +import { getHours } from 'date-fns'; +import { isAxiosError } from 'axios'; +import { useRouter } from 'next/navigation'; + +import { usePaymentMutation, useShops } from '@/api-hooks'; +import { useAppDispatch, useAppSelector } from '@/hooks'; +import { addItem, basketSelector, clearCart, removeItem } from '@/store'; +import { Preloader } from '@/vendor'; +import { CustomSelect, Input } from '@/shared'; +import { IProductState, Stylebook } from '@/interfaces'; + +import * as Styled from './styled'; +import { CartSchema, TCartSchema } from './schema'; +import { ProductListGroup } from '../product-list-group'; +import { CartPayment } from '../cart-payment'; +import { RadioWithText } from '../radio-with-text'; +import { RadioCartButton } from '../radio-cart-button'; +import { PICKUP_METHOD_DATA, PICKUP_TIME_DATA, getShopsList } from './utils'; + +interface ICartFormProps { + handleCloseCart: () => void; + handleOpenModal: (status: string) => void; + handleCloseModal: () => void; + stylebook: Stylebook | null; + shop: string; +} + +interface BasketItem { + slug: string; + count: number; +} + +interface IPaymentProps { + shop: string; + time: number; + basket: BasketItem[]; +} + +interface IProduct { + productSlug: string; + addons: string[]; + modification?: string; + count: number; + cost: number; +} + +export const CartForm: FC = ({ + handleOpenModal, + handleCloseModal, + handleCloseCart, + stylebook, + shop, +}) => { + const { data, isError, error, isLoading } = useShops(); + + const currentShopData = data?.find((item) => item.slug === shop); + const addresses = useMemo(() => getShopsList(data || []), [data]); + + const basket = useAppSelector(basketSelector); + const dispatch = useAppDispatch(); + const router = useRouter(); + + const handleError = (reqError: Error) => { + if (isAxiosError(reqError) && reqError.code === '404') + router.replace('/404'); + if (isAxiosError(reqError) && reqError.code === '500') + router.replace('/server-error'); + handleCloseModal(); + handleOpenModal('error'); + }; + + const handleSuccess = () => { + handleCloseModal(); + handleOpenModal('success'); + }; + + const { + control, + handleSubmit, + watch, + resetField, + formState: { isValid }, + } = useForm({ + resolver: zodResolver(CartSchema), + defaultValues: { + address: currentShopData?.address.toLowerCase() || '', + comment: '', + pickupTime: PICKUP_TIME_DATA[0].value, + pickupMethod: 'С собой', + products: basket[shop]?.items, + agreeWithTerms: false, + }, + mode: 'onTouched', + }); + + const { fields } = useFieldArray({ + control, + name: 'products', + }); + + const activeValueTime = watch('pickupTime'); + const activeValueMethod = watch('pickupMethod'); + const activeValueTerms = watch('agreeWithTerms'); + + const { mutate, isPending } = usePaymentMutation(handleError, handleSuccess); + + const onSubmit: SubmitHandler = (dataForm) => { + handleOpenModal('check'); + const paymentData: IPaymentProps = { + shop: '', + time: 0, + basket: [], + }; + paymentData.shop = shop; + paymentData.time = getHours(new Date()); + paymentData.basket = []; + dataForm.products.forEach((item: IProduct) => { + paymentData.basket.push({ slug: item.productSlug, count: item.count }); + }); + + mutate(paymentData); + }; + + const handleDecreaseCount = (item: IProductState) => { + if (shop) dispatch(removeItem({ shop, item })); + }; + + const handleIncreaseCount = (item: IProductState) => { + if (shop) dispatch(addItem({ shop, item })); + }; + + const handleClearCart = () => { + dispatch(clearCart({ shop })); + handleCloseCart(); + }; + + useEffect(() => { + resetField('address', { + defaultValue: currentShopData?.address.toLowerCase(), + }); + }, [currentShopData, resetField]); + + useEffect(() => { + resetField('products', { defaultValue: basket[shop]?.items }); + if (basket[shop]?.items.length === 0) handleCloseCart(); + }, [resetField, shop, basket, handleCloseCart]); + + if (isLoading) return ; + + if (isError) { + return
Произошла ошибка: {error.message}
; + } + + return ( + + + + + + + + + Заберёте заказ через + + {PICKUP_TIME_DATA.map((item) => ( + + + + ))} + + + + {PICKUP_METHOD_DATA.map((item) => ( + + + + ))} + + + + + + + + + + + + ); +}; diff --git a/src/components/cart/ui/cart-form/index.ts b/src/components/cart/ui/cart-form/index.ts new file mode 100644 index 0000000..308ef8b --- /dev/null +++ b/src/components/cart/ui/cart-form/index.ts @@ -0,0 +1,2 @@ +export * from './cart-form'; +export * from './schema'; diff --git a/src/components/cart/ui/cart-form/schema/index.ts b/src/components/cart/ui/cart-form/schema/index.ts new file mode 100644 index 0000000..907c047 --- /dev/null +++ b/src/components/cart/ui/cart-form/schema/index.ts @@ -0,0 +1 @@ +export * from './schema-cart-form'; diff --git a/src/components/cart/ui/cart-form/schema/schema-cart-form.ts b/src/components/cart/ui/cart-form/schema/schema-cart-form.ts new file mode 100644 index 0000000..12017d2 --- /dev/null +++ b/src/components/cart/ui/cart-form/schema/schema-cart-form.ts @@ -0,0 +1,27 @@ +import { z, ZodType } from 'zod'; + +const ProductStateSchema = z.object({ + productSlug: z.string(), + addons: z.array(z.string()), + modification: z.string().optional(), + count: z.number(), + cost: z.number(), +}); + +export const CartSchema: ZodType = z + .object({ + address: z.string().optional(), + comment: z.string().max(30).optional(), + pickupTime: z.string(), + pickupMethod: z.string(), + products: z.array(ProductStateSchema).nonempty({ + message: "Can't be empty!", + }), + agreeWithTerms: z.boolean(), + }) + .refine((data) => data.agreeWithTerms === true, { + message: 'agreeWithTerms должен быть true', + path: ['agreeWithTerms'], + }); + +export type TCartSchema = z.infer; diff --git a/src/components/cart/ui/cart-form/styled.tsx b/src/components/cart/ui/cart-form/styled.tsx new file mode 100644 index 0000000..48ba26a --- /dev/null +++ b/src/components/cart/ui/cart-form/styled.tsx @@ -0,0 +1,72 @@ +import ScrollContainer from 'react-indiana-drag-scroll'; +import styled from 'styled-components'; + +export const CartForm = styled.form` + position: relative; + display: flex; + flex-direction: column; + gap: 20px; + align-items: center; + height: 100%; +`; + +export const SelectGroup = styled.div` + box-sizing: border-box; + width: 100%; + padding-right: 40px; + padding-left: 40px; +`; + +export const RadioButtonsGroup = styled.div` + display: flex; + flex-direction: column; + width: 100%; + max-width: 100%; +`; + +export const RadioPickupTimeGroup = styled(ScrollContainer)` + display: flex; + flex-direction: row; + gap: 12px; + padding-right: 40px; + padding-left: 40px; + margin: 10px 0 20px; + user-select: none; +`; + +export const PickupLabel = styled.p` + display: flex; + flex-direction: column; + padding-left: 40px; + margin-bottom: 6px; + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--gray); +`; + +export const RadioItem = styled.div``; + +export const PickupMethodGroup = styled(ScrollContainer)` + display: flex; + flex-direction: row; + gap: 56px; + padding-right: 40px; + padding-left: 40px; + white-space: nowrap; + user-select: none; +`; + +export const CartProductList = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +export const CartPaymentGroup = styled.div` + position: absolute; + bottom: 0; + width: 100%; + height: 76px; +`; diff --git a/src/components/cart/ui/cart-form/utils/constants/constants.ts b/src/components/cart/ui/cart-form/utils/constants/constants.ts new file mode 100644 index 0000000..cefe3bd --- /dev/null +++ b/src/components/cart/ui/cart-form/utils/constants/constants.ts @@ -0,0 +1,11 @@ +export const PICKUP_TIME_DATA = [ + { id: 1, label: '5 минут', value: '5 минут' }, + { id: 2, label: '10 минут', value: '10 минут' }, + { id: 3, label: '15 минут', value: '15 минут' }, + { id: 4, label: '20 минут', value: '20 минут' }, +]; + +export const PICKUP_METHOD_DATA = [ + { id: 1, label: 'С собой', value: 'С собой' }, + { id: 2, label: 'В кофейне', value: 'В кофейне' }, +]; diff --git a/src/components/cart/ui/cart-form/utils/constants/index.ts b/src/components/cart/ui/cart-form/utils/constants/index.ts new file mode 100644 index 0000000..c94f80f --- /dev/null +++ b/src/components/cart/ui/cart-form/utils/constants/index.ts @@ -0,0 +1 @@ +export * from './constants'; diff --git a/src/components/cart/ui/cart-form/utils/helpers/get-shop-list.ts b/src/components/cart/ui/cart-form/utils/helpers/get-shop-list.ts new file mode 100644 index 0000000..419a1e4 --- /dev/null +++ b/src/components/cart/ui/cart-form/utils/helpers/get-shop-list.ts @@ -0,0 +1,17 @@ +import { Shop } from '@/interfaces'; + +interface IOption { + value: string; + label: string; +} + +export const getShopsList = (shops: Shop[] | null): IOption[] => { + const addresses: IOption[] = []; + shops?.forEach((item) => { + addresses.push({ + value: item.address.toLowerCase(), + label: `ул. ${item.address}`, + }); + }); + return addresses; +}; diff --git a/src/components/cart/ui/cart-form/utils/helpers/index.ts b/src/components/cart/ui/cart-form/utils/helpers/index.ts new file mode 100644 index 0000000..dcb2fd9 --- /dev/null +++ b/src/components/cart/ui/cart-form/utils/helpers/index.ts @@ -0,0 +1 @@ +export * from './get-shop-list'; diff --git a/src/components/cart/ui/cart-form/utils/index.ts b/src/components/cart/ui/cart-form/utils/index.ts new file mode 100644 index 0000000..5a9087d --- /dev/null +++ b/src/components/cart/ui/cart-form/utils/index.ts @@ -0,0 +1,2 @@ +export * from './constants'; +export * from './helpers'; diff --git a/src/components/cart/ui/cart-header/cart-header.tsx b/src/components/cart/ui/cart-header/cart-header.tsx new file mode 100644 index 0000000..23acbcf --- /dev/null +++ b/src/components/cart/ui/cart-header/cart-header.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { FC } from 'react'; + +import { ActionButton } from '@/shared'; +import { formatPrice } from '@/utils'; + +import * as Styled from './styled'; + +interface ICartHeaderProps { + price: number; + handleCloseCart: () => void; + colorItem?: string; +} + +export const CartHeader: FC = ({ + price, + handleCloseCart, + colorItem, +}) => ( + + + Итого:{' '} + {formatPrice(price)} + + + +); diff --git a/src/components/cart/ui/cart-header/index.ts b/src/components/cart/ui/cart-header/index.ts new file mode 100644 index 0000000..1fc949c --- /dev/null +++ b/src/components/cart/ui/cart-header/index.ts @@ -0,0 +1 @@ +export * from './cart-header'; diff --git a/src/components/cart/ui/cart-header/styled.tsx b/src/components/cart/ui/cart-header/styled.tsx new file mode 100644 index 0000000..12ae8a0 --- /dev/null +++ b/src/components/cart/ui/cart-header/styled.tsx @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +interface ICartHeaderProps { + $colorItem?: string; +} + +export const CartHeader = styled.div` + box-sizing: border-box; + display: flex; + justify-content: space-between; + width: 100%; + padding: 6px 20px 20px; +`; + +export const PriceTitle = styled.h2` + font-family: ${(props) => props.theme.fonts}; + font-size: 18px; + font-weight: 800; + line-height: 26px; + color: var(--black-plus); +`; + +export const Price = styled.span` + color: ${({ $colorItem }) => $colorItem || 'black'}; +`; diff --git a/src/components/cart/ui/cart-item-control/cart-item-control.tsx b/src/components/cart/ui/cart-item-control/cart-item-control.tsx new file mode 100644 index 0000000..0a14294 --- /dev/null +++ b/src/components/cart/ui/cart-item-control/cart-item-control.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { formatPrice } from '@/utils'; + +import { AddButton } from '../cart-add-button'; +import * as Styled from './styled'; + +export interface ICartItemControlProps { + onIncrease: () => void; + onDecrease: () => void; + count: number; + price: number; + colorItem: string; +} + +export const CartItemControl = ({ + onIncrease, + onDecrease, + count, + price, + colorItem, +}: ICartItemControlProps) => ( + + + + + + + {formatPrice(count === 0 ? price : price * count)} + + +); diff --git a/src/components/cart/ui/cart-item-control/index.ts b/src/components/cart/ui/cart-item-control/index.ts new file mode 100644 index 0000000..2d7e5b0 --- /dev/null +++ b/src/components/cart/ui/cart-item-control/index.ts @@ -0,0 +1 @@ +export * from './cart-item-control'; diff --git a/src/components/cart/ui/cart-item-control/styled.tsx b/src/components/cart/ui/cart-item-control/styled.tsx new file mode 100644 index 0000000..5eeeac6 --- /dev/null +++ b/src/components/cart/ui/cart-item-control/styled.tsx @@ -0,0 +1,38 @@ +import styled from 'styled-components'; + +export const CartItemControl = styled.div` + display: grid; + grid-template-rows: clamp(36px, calc(0.8rem + 0.1vw), 44px); + grid-template-columns: 1fr 1fr 1fr; +`; + +export const AddButtonWrapper = styled.div` + z-index: 1; + grid-row: 1/2; + grid-column: 1/3; +`; + +export const PriceBackground = styled.div` + box-sizing: border-box; + display: flex; + grid-row: 1/2; + grid-column: 2/4; + align-items: center; + justify-content: center; + background-color: var(--black); + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +`; + +export const Price = styled.div` + display: flex; + grid-row: 1/2; + grid-column: 3/4; + align-items: center; + justify-content: center; + font-family: ${(props) => props.theme.fonts}; + font-size: 10px; + font-weight: 800; + line-height: 14px; + color: var(--white); +`; diff --git a/src/components/cart/ui/cart-payment/cart-payment.tsx b/src/components/cart/ui/cart-payment/cart-payment.tsx new file mode 100644 index 0000000..bba9f41 --- /dev/null +++ b/src/components/cart/ui/cart-payment/cart-payment.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { FC } from 'react'; +import { Control } from 'react-hook-form'; + +import { Stylebook } from '@/interfaces'; + +import * as Styled from './styled'; +import { CartCheckbox } from '../checkbox-cart-button'; + +interface CartPaymentProps { + stylebook: Stylebook | null; + activeValueTerms: boolean; + isValid: boolean; + control: Control; + isPending: boolean; +} + +export const CartPayment: FC = ({ + stylebook, + activeValueTerms, + isValid, + control, + isPending, +}) => ( + + + + + + + + Оплатить + + +); diff --git a/src/components/cart/ui/cart-payment/index.ts b/src/components/cart/ui/cart-payment/index.ts new file mode 100644 index 0000000..c65239b --- /dev/null +++ b/src/components/cart/ui/cart-payment/index.ts @@ -0,0 +1 @@ +export * from './cart-payment'; diff --git a/src/components/cart/ui/cart-payment/styled.tsx b/src/components/cart/ui/cart-payment/styled.tsx new file mode 100644 index 0000000..92ce62a --- /dev/null +++ b/src/components/cart/ui/cart-payment/styled.tsx @@ -0,0 +1,95 @@ +import styled from 'styled-components'; + +import { Stylebook } from '@/interfaces'; + +interface ICartPaymentWrapperProps { + $stylebook?: Stylebook | null; +} + +const getPatternUrl = ($stylebook: Stylebook | null) => { + return `/patterns/${$stylebook?.pattern || 'diamonds'}.png`; +}; + +export const CartPayment = styled.div` + position: relative; + z-index: 1; + display: grid; + grid-template-rows: 1fr; + grid-template-columns: 2fr 24px 1fr; + height: 100%; + margin: 0 20px; + filter: drop-shadow(0 4px 4px rgb(0 0 0 / 25%)); +`; + +export const MaskElementBorder = styled.div` + position: relative; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--black-plus); + mask: url('/payment-border.svg'); +`; + +export const MaskElement = styled.div` + position: relative; + width: 100%; + height: 100%; + background-color: ${({ $stylebook }) => + $stylebook?.mainColor || 'var(--black)'}; + mask: url('/payment-mask.svg'); + + &::after { + position: absolute; + inset: 0; + content: ''; + background: url(${({ $stylebook }) => getPatternUrl($stylebook || null)}); + opacity: ${({ $stylebook }) => $stylebook?.opacity || '.7'}; + } +`; + +export const DotsLine = styled.div` + position: absolute; + width: 2px; + height: 30px; + background: url('/dots.svg'); + background-size: cover; +`; + +export const ButtonText = styled.span``; + +export const SubmitButton = styled.button` + position: relative; + z-index: 1; + font-family: ${(props) => props.theme.fonts}; + font-size: 14px; + font-weight: 800; + line-height: 22px; + color: var(--white); + cursor: pointer; + background-color: ${({ $stylebook }) => + $stylebook?.mainColor || 'var(--black)'}; + border-top: 2px solid var(--black-plus); + border-right: 2px solid var(--black-plus); + border-bottom: 2px solid var(--black-plus); + + &::after { + position: absolute; + inset: 0; + z-index: -1; + content: ''; + background: url(${({ $stylebook }) => getPatternUrl($stylebook || null)}); + opacity: ${({ $stylebook }) => $stylebook?.opacity || '.7'}; + } + + &:disabled { + cursor: auto; + + ${ButtonText} { + opacity: 0.5; + } + } + + &:not(:disabled):active { + opacity: 0.8; + } +`; diff --git a/src/components/cart/ui/cart-product-card/cart-product-card.tsx b/src/components/cart/ui/cart-product-card/cart-product-card.tsx new file mode 100644 index 0000000..368b87e --- /dev/null +++ b/src/components/cart/ui/cart-product-card/cart-product-card.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { FC } from 'react'; + +import { IProductState } from '@/interfaces'; +import { Icon } from '@/shared'; + +import { CartItemControl } from '../cart-item-control'; +import * as Styled from './styled'; + +interface ICartProductCardProps { + product: IProductState; + onDecrease: (item: IProductState) => void; + onIncrease: (item: IProductState) => void; + colorItem: string; + onBlur?: () => void; +} + +export const CartProductCard: FC = ({ + product, + onDecrease, + onIncrease, + colorItem, + onBlur, +}) => { + const handleIncrease = () => { + onIncrease(product); + }; + const handleDecrease = () => { + onDecrease(product); + }; + + return ( + + + + {product.name} + + + {product.addons + ? product.addons.map((addon) => ( + + {addon} + )) + : null} + + + {product.modification && product.modification !== 'Без опций' ? ( + + {product.modification} + ) : null} + + + + + + ); +}; diff --git a/src/components/cart/ui/cart-product-card/index.ts b/src/components/cart/ui/cart-product-card/index.ts new file mode 100644 index 0000000..de8d556 --- /dev/null +++ b/src/components/cart/ui/cart-product-card/index.ts @@ -0,0 +1 @@ +export * from './cart-product-card'; diff --git a/src/components/cart/ui/cart-product-card/styled.tsx b/src/components/cart/ui/cart-product-card/styled.tsx new file mode 100644 index 0000000..7ff30dc --- /dev/null +++ b/src/components/cart/ui/cart-product-card/styled.tsx @@ -0,0 +1,72 @@ +import styled from 'styled-components'; + +export const CartProductCard = styled.div` + box-sizing: border-box; + display: grid; + grid-template-rows: 40px minmax(14px, auto) 52px; + grid-template-columns: 78px 4px 78px; + row-gap: 4px; + background-color: var(--black-plus); + border: 2px solid var(--black-plus); + border-radius: 10px; +`; + +export const TopContent = styled.div` + display: flex; + grid-column: 1/-1; + gap: 8px; + align-items: center; + justify-content: center; + background-color: var(--white); + border-radius: 10px; +`; + +export const Addons = styled.div` + display: flex; + flex-direction: column; + grid-row: 2/3; + grid-column: 1/2; + justify-content: flex-start; + padding-left: 10px; +`; + +export const Mods = styled.div` + display: flex; + grid-row: 2/3; + grid-column: 3/4; + justify-content: flex-end; + padding-right: 10px; +`; + +export const CountControl = styled.div` + grid-row: 3/4; + grid-column: 1/-1; + justify-self: center; + width: 140px; + height: 36px; + padding-top: 4px; +`; + +export const Option = styled.p` + max-width: 60px; + overflow: hidden; + font-family: ${(props) => props.theme.fonts}; + font-size: 10px; + font-weight: 800; + line-height: 14px; + color: var(--white); + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const Title = styled.p` + max-width: 125px; + overflow: hidden; + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--black); + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/src/components/cart/ui/checkbox-cart-button/checkbox-cart-button.tsx b/src/components/cart/ui/checkbox-cart-button/checkbox-cart-button.tsx new file mode 100644 index 0000000..6e9287b --- /dev/null +++ b/src/components/cart/ui/checkbox-cart-button/checkbox-cart-button.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { FC } from 'react'; +import { Control, Controller } from 'react-hook-form'; + +import { Stylebook } from '@/interfaces'; +import { Icon } from '@/shared'; + +import * as Styled from './styled'; + +interface ICartCheckbox { + stylebook: Stylebook | null; + activeValueTerms: boolean; + control: Control; +} + +export const CartCheckbox: FC = ({ + stylebook, + activeValueTerms, + control, +}) => ( + + + {activeValueTerms ? ( + + ) : ( + + )} + + ( + + )} + /> + Согласен с правилами оплаты + +); diff --git a/src/components/cart/ui/checkbox-cart-button/index.ts b/src/components/cart/ui/checkbox-cart-button/index.ts new file mode 100644 index 0000000..c646cfc --- /dev/null +++ b/src/components/cart/ui/checkbox-cart-button/index.ts @@ -0,0 +1 @@ +export * from './checkbox-cart-button'; diff --git a/src/components/cart/ui/checkbox-cart-button/styled.tsx b/src/components/cart/ui/checkbox-cart-button/styled.tsx new file mode 100644 index 0000000..53abf97 --- /dev/null +++ b/src/components/cart/ui/checkbox-cart-button/styled.tsx @@ -0,0 +1,59 @@ +import styled from 'styled-components'; + +import { Stylebook } from '@/interfaces'; + +interface ICartPaymentWrapperProps { + $stylebook?: Stylebook | null; +} +interface IStyledCartPaymentProps { + $colorItem?: string; +} + +const getPatternUrl = ($stylebook: Stylebook | null) => { + return `/patterns/${$stylebook?.pattern || 'diamonds'}.png`; +}; + +export const CheckboxLabel = styled.label` + position: relative; + z-index: 1; + display: flex; + gap: 12px; + align-items: center; + justify-content: center; + padding-left: 12px; + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--white); + cursor: pointer; + user-select: none; + background-color: ${({ $stylebook }) => + $stylebook?.mainColor || 'var(--black)'}; + border-top: 2px solid var(--black-plus); + border-bottom: 2px solid var(--black-plus); + border-left: 2px solid var(--black-plus); + + &::after { + position: absolute; + inset: 0; + z-index: -1; + content: ''; + background: url(${({ $stylebook }) => getPatternUrl($stylebook || null)}); + opacity: ${({ $stylebook }) => $stylebook?.opacity || '.7'}; + } +`; + +export const IconWrapper = styled.div` + display: flex; + justify-content: start; + color: ${({ $colorItem }) => $colorItem || 'black'}; +`; + +export const Checkbox = styled.input` + display: none; +`; + +export const SvgBase = styled.svg` + filter: drop-shadow(2px 2px 2px rgb(0 0 0 / 25%)); +`; diff --git a/src/components/cart/ui/index.ts b/src/components/cart/ui/index.ts new file mode 100644 index 0000000..4830b0e --- /dev/null +++ b/src/components/cart/ui/index.ts @@ -0,0 +1,11 @@ +export * from './cart-add-button'; +export * from './cart-form'; +export * from './cart-header'; +export * from './cart-item-control'; +export * from './cart-payment'; +export * from './cart-product-card'; +export * from './checkbox-cart-button'; +export * from './product-list-group'; +export * from './radio-cart-button'; +export * from './radio-with-text'; +export * from './modals'; diff --git a/src/components/cart/ui/modals/index.ts b/src/components/cart/ui/modals/index.ts new file mode 100644 index 0000000..bd614aa --- /dev/null +++ b/src/components/cart/ui/modals/index.ts @@ -0,0 +1,3 @@ +export * from './payment-check-modal'; +export * from './payment-error-modal'; +export * from './payment-success-modal'; diff --git a/src/components/cart/ui/modals/modal-content/index.ts b/src/components/cart/ui/modals/modal-content/index.ts new file mode 100644 index 0000000..2c211af --- /dev/null +++ b/src/components/cart/ui/modals/modal-content/index.ts @@ -0,0 +1 @@ +export * from './modal-content'; diff --git a/src/components/cart/ui/modals/modal-content/modal-content.tsx b/src/components/cart/ui/modals/modal-content/modal-content.tsx new file mode 100644 index 0000000..124142d --- /dev/null +++ b/src/components/cart/ui/modals/modal-content/modal-content.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { FC } from 'react'; + +import * as Styled from './styled'; + +interface IModalContent { + handleClose: () => void; + btnColor: string; + text: string; + subtitle: string; +} + +export const ModalContent: FC = ({ + handleClose, + btnColor, + text, + subtitle, +}) => ( + <> + {text} + {subtitle} + + Ok + + +); diff --git a/src/components/cart/ui/modals/modal-content/styled.tsx b/src/components/cart/ui/modals/modal-content/styled.tsx new file mode 100644 index 0000000..1bf9900 --- /dev/null +++ b/src/components/cart/ui/modals/modal-content/styled.tsx @@ -0,0 +1,70 @@ +import styled from 'styled-components'; + +import { devices } from '@/styles'; + +export const MessageText = styled.p` + font-family: ${(props) => props.theme.fonts}; + font-size: 18px; + font-weight: 800; + line-height: 26px; + color: var(--black-plus); + + @media ${devices.tablet} { + font-size: 20px; + line-height: 28px; + } + + @media ${devices.mobile} { + font-size: 18px; + line-height: 26px; + } +`; + +export const MessageSubtitle = styled.p` + font-family: ${(props) => props.theme.fonts}; + font-size: 13px; + font-weight: 800; + line-height: 16px; + color: var(--gray); + + @media ${devices.tablet} { + font-size: 16px; + line-height: 22px; + } + + @media ${devices.mobile} { + font-size: 13px; + line-height: 16px; + } +`; + +export const PaymentCheckButton = styled.button<{ $btnColor: string }>` + position: absolute; + right: 22px; + bottom: -20px; + display: inline-block; + padding: 12px 30px; + font-size: 14px; + font-weight: 800; + line-height: 20px; + color: var(--white); + text-align: center; + letter-spacing: 0; + cursor: pointer; + user-select: none; + background-color: var(${({ $btnColor }) => $btnColor}); + border: 2px solid var(--black-plus); + border-radius: 5px; + box-shadow: 2px 2px 2px rgb(0 0 0 / 25%); + transition: 0.2s ease; + + &:hover { + box-shadow: 3px 3px 8px rgb(0 0 0 / 30%); + transform: translateY(-1px); + } + + &:active { + box-shadow: initial; + transform: translateY(0); + } +`; diff --git a/src/components/cart/ui/modals/payment-check-modal/index.ts b/src/components/cart/ui/modals/payment-check-modal/index.ts new file mode 100644 index 0000000..4e20602 --- /dev/null +++ b/src/components/cart/ui/modals/payment-check-modal/index.ts @@ -0,0 +1 @@ +export * from './payment-check-modal'; diff --git a/src/components/cart/ui/modals/payment-check-modal/payment-check-modal.tsx b/src/components/cart/ui/modals/payment-check-modal/payment-check-modal.tsx new file mode 100644 index 0000000..954b9e2 --- /dev/null +++ b/src/components/cart/ui/modals/payment-check-modal/payment-check-modal.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { Modal } from '@/shared'; + +import { ModalContent } from '../modal-content'; + +export const PaymentCheckModal = ({ + handleClose, +}: { + handleClose: () => void; +}) => { + const content = ( + + ); + + return ( + + ); +}; diff --git a/src/components/cart/ui/modals/payment-error-modal/index.ts b/src/components/cart/ui/modals/payment-error-modal/index.ts new file mode 100644 index 0000000..b61620a --- /dev/null +++ b/src/components/cart/ui/modals/payment-error-modal/index.ts @@ -0,0 +1 @@ +export * from './payment-error-modal'; diff --git a/src/components/cart/ui/modals/payment-error-modal/payment-error-modal.tsx b/src/components/cart/ui/modals/payment-error-modal/payment-error-modal.tsx new file mode 100644 index 0000000..d0d218a --- /dev/null +++ b/src/components/cart/ui/modals/payment-error-modal/payment-error-modal.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { Modal } from '@/shared'; + +import { ModalContent } from '../modal-content'; + +export const PaymentErrorModal = ({ + handleClose, +}: { + handleClose: () => void; +}) => { + const content = ( + + ); + + return ( + + ); +}; diff --git a/src/components/cart/ui/modals/payment-success-modal/index.ts b/src/components/cart/ui/modals/payment-success-modal/index.ts new file mode 100644 index 0000000..32f5a5b --- /dev/null +++ b/src/components/cart/ui/modals/payment-success-modal/index.ts @@ -0,0 +1 @@ +export * from './payment-success-modal'; diff --git a/src/components/cart/ui/modals/payment-success-modal/payment-success-modal.tsx b/src/components/cart/ui/modals/payment-success-modal/payment-success-modal.tsx new file mode 100644 index 0000000..93a88f4 --- /dev/null +++ b/src/components/cart/ui/modals/payment-success-modal/payment-success-modal.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { FC } from 'react'; + +import { useAppDispatch, useAppSelector } from '@/hooks'; +import { clearCart, userSelector } from '@/store'; +import { Modal } from '@/shared'; + +import { ModalContent } from '../modal-content'; + +interface IPaymentSuccessModal { + handleClose: () => void; + shop: string; +} + +export const PaymentSuccessModal: FC = ({ + handleClose, + shop, +}) => { + const user = useAppSelector(userSelector); + const dispatch = useAppDispatch(); + + const handleCloseModal = () => { + handleClose(); + dispatch(clearCart({ shop })); + }; + + const content = ( + + ); + + return ( + + ); +}; diff --git a/src/components/cart/ui/product-list-group/index.ts b/src/components/cart/ui/product-list-group/index.ts new file mode 100644 index 0000000..a282299 --- /dev/null +++ b/src/components/cart/ui/product-list-group/index.ts @@ -0,0 +1 @@ +export * from './product-list-group'; diff --git a/src/components/cart/ui/product-list-group/product-list-group.tsx b/src/components/cart/ui/product-list-group/product-list-group.tsx new file mode 100644 index 0000000..9214e51 --- /dev/null +++ b/src/components/cart/ui/product-list-group/product-list-group.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { FC } from 'react'; +import { Control, Controller } from 'react-hook-form'; + +import { IProductState } from '@/interfaces'; + +import * as Styled from './styled'; +import { CartProductCard } from '../cart-product-card'; + +interface ProductListGroupProps { + colorItem: string; + onDecrease: (item: IProductState) => void; + onIncrease: (item: IProductState) => void; + control: Control; + fields: Record<'id', string>[]; + handleClearCart: () => void; +} + +export const ProductListGroup: FC = ({ + colorItem, + onDecrease, + onIncrease, + control, + fields, + handleClearCart, +}) => ( + + + Выбрано + + Убрать всё + + + + {fields.map((field) => ( + + ( + + onDecrease(field as IProductState & { id: string }) + } + /> + )} + /> + + ))} + + +); diff --git a/src/components/cart/ui/product-list-group/styled.tsx b/src/components/cart/ui/product-list-group/styled.tsx new file mode 100644 index 0000000..458420b --- /dev/null +++ b/src/components/cart/ui/product-list-group/styled.tsx @@ -0,0 +1,51 @@ +import ScrollContainer from 'react-indiana-drag-scroll'; +import styled from 'styled-components'; + +export const ProductListGroup = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; +`; + +export const SelectedProductsHeader = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + padding-right: 20px; + padding-left: 20px; +`; + +export const Title = styled.p` + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--black-plus); +`; + +export const ProductCardWrapper = styled.div``; + +export const DeleteButton = styled.button` + font-family: ${(props) => props.theme.fonts}; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--red-plus); + cursor: pointer; + background-color: transparent; + + &:hover { + opacity: 0.7; + } +`; + +export const SelectedProductsList = styled(ScrollContainer)` + display: flex; + gap: 10px; + padding-right: 20px; + padding-left: 20px; + user-select: none; +`; diff --git a/src/components/cart/ui/radio-cart-button/index.ts b/src/components/cart/ui/radio-cart-button/index.ts new file mode 100644 index 0000000..2c53140 --- /dev/null +++ b/src/components/cart/ui/radio-cart-button/index.ts @@ -0,0 +1 @@ +export * from './radio-cart-button'; diff --git a/src/components/cart/ui/radio-cart-button/radio-cart-button.tsx b/src/components/cart/ui/radio-cart-button/radio-cart-button.tsx new file mode 100644 index 0000000..b8e097e --- /dev/null +++ b/src/components/cart/ui/radio-cart-button/radio-cart-button.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { FC } from 'react'; +import { Control, Controller } from 'react-hook-form'; + +import { Radio, RadioActive } from '@/shared'; + +import * as Styled from './styled'; + +interface IRadioProps { + label: string; + value: string; + activeValue: string; + colorItem?: string; + control: Control; +} + +export const RadioCartButton: FC = ({ + label, + value, + activeValue, + control, + colorItem, +}) => ( + + {activeValue === value ? : } + ( + onChange(value)} + /> + )} + /> + {label} + +); diff --git a/src/components/cart/ui/radio-cart-button/styled.tsx b/src/components/cart/ui/radio-cart-button/styled.tsx new file mode 100644 index 0000000..802973f --- /dev/null +++ b/src/components/cart/ui/radio-cart-button/styled.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components'; + +interface StyledRadioProps { + $colorItem?: string; +} + +export const StyledRadioLabel = styled.label` + display: inline-flex; + gap: 6px; + align-items: center; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: ${({ $colorItem }) => $colorItem || 'black'}; + cursor: pointer; +`; + +export const StyledRadio = styled.input` + display: none; +`; + +export const StyledSpan = styled.span` + color: var(--black-plus); +`; diff --git a/src/components/cart/ui/radio-with-text/index.ts b/src/components/cart/ui/radio-with-text/index.ts new file mode 100644 index 0000000..382c2fb --- /dev/null +++ b/src/components/cart/ui/radio-with-text/index.ts @@ -0,0 +1 @@ +export * from './radio-with-text'; diff --git a/src/components/cart/ui/radio-with-text/radio-with-text.tsx b/src/components/cart/ui/radio-with-text/radio-with-text.tsx new file mode 100644 index 0000000..ac6a163 --- /dev/null +++ b/src/components/cart/ui/radio-with-text/radio-with-text.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { FC } from 'react'; +import { Control, Controller } from 'react-hook-form'; + +import * as Styled from './styled'; + +interface IRadioWithText { + colorItem: string; + value: string; + activeValue: string; + control: Control; +} + +export const RadioWithText: FC = ({ + colorItem, + value, + control, + activeValue, +}) => ( + <> + ( + onChange(e.currentTarget.value)} + /> + )} + /> + {value} + +); diff --git a/src/components/cart/ui/radio-with-text/styled.tsx b/src/components/cart/ui/radio-with-text/styled.tsx new file mode 100644 index 0000000..88d8f66 --- /dev/null +++ b/src/components/cart/ui/radio-with-text/styled.tsx @@ -0,0 +1,31 @@ +import styled from 'styled-components'; + +interface IRadioLabelProps { + $colorItem?: string; +} + +export const RadioLabel = styled.label` + display: inline-flex; + gap: 6px; + align-items: center; + padding: 6px 12px; + margin-bottom: 4px; + font-size: 12px; + font-weight: 800; + line-height: 16px; + color: var(--black-plus); + white-space: nowrap; + cursor: pointer; + border: 2px solid var(--black-plus); + border-radius: 10px; + box-shadow: 2px 2px 2px rgb(0 0 0 / 25%); +`; + +export const RadioWithText = styled.input` + display: none; + + &:checked + ${RadioLabel} { + color: var(--white); + background-color: ${({ $colorItem }) => $colorItem || 'black'}; + } +`; -- GitLab From 3aca8cb6f20721c25b7533e9df796440430522b3 Mon Sep 17 00:00:00 2001 From: AlexandrValgamov Date: Wed, 5 Jun 2024 13:05:02 +0500 Subject: [PATCH 8/9] Catalog | fix: fix bugs --- .../[indexCategoryParam]/loading.tsx | 9 -- .../[category]/[indexCategoryParam]/page.tsx | 19 ++-- src/app/catalog/[shop]/[category]/loading.tsx | 9 -- src/app/catalog/[shop]/[category]/page.tsx | 18 ++-- .../catalog/[shop]/product/[item]/loading.tsx | 9 -- .../catalog/[shop]/product/[item]/page.tsx | 19 ++-- src/app/favicon.ico | Bin 25931 -> 0 bytes src/app/layout.tsx | 6 +- src/app/lib/registry.tsx | 30 ------ src/app/loading.tsx | 2 +- src/app/login/loading.tsx | 9 -- src/app/login/page.tsx | 4 +- src/app/page.tsx | 13 ++- src/app/profile/loading.tsx | 9 -- src/app/profile/page.tsx | 4 +- src/app/signup/loading.tsx | 9 -- src/app/signup/page.tsx | 4 +- .../shop-list/ui/card-info/card-info.tsx | 17 ---- .../shop-list/ui/shop-image/styled.tsx | 2 +- src/components/slider/index.ts | 1 - src/components/slider/slider.tsx | 24 ----- src/components/slider/styled.tsx | 13 --- src/{app => lib}/query-provider.tsx | 8 +- src/{app => lib}/store-provider.tsx | 0 src/shared/back-group/back-group.tsx | 2 + .../auth-checkbox/auth-checkbox.tsx | 2 + src/shared/icon/icon.tsx | 2 + src/shared/layout-modal/layout-modal.tsx | 2 + src/shared/overlay/overlay.tsx | 2 + src/shared/product-card/product-card.tsx | 2 + src/shared/tab/tab.tsx | 2 + src/{app => styles}/globals.css | 0 src/styles/globals.ts | 86 ------------------ src/styles/index.ts | 1 - src/vendor/preloader/preloader.tsx | 7 +- .../catalog-page/catalog-page.tsx | 0 src/{page => view}/catalog-page/index.ts | 0 .../utils/helpers/get-tab-index.ts | 0 .../catalog-page/utils/helpers/index.ts | 0 .../catalog-page/utils/index.ts | 0 .../category-page/category-page.tsx | 0 src/{page => view}/category-page/index.ts | 0 src/{page => view}/index.ts | 0 src/{page => view}/login-page/index.ts | 0 src/{page => view}/login-page/login-page.tsx | 0 src/{page => view}/product-page/index.ts | 0 .../product-page/product-page.tsx | 0 src/{page => view}/profile-page/index.ts | 0 .../profile-page/profile-page.tsx | 0 src/{page => view}/register-page/index.ts | 0 .../register-page/register-page.tsx | 0 src/{page => view}/shop-list-page/index.ts | 0 .../shop-list-page/shop-list-page.tsx | 0 53 files changed, 78 insertions(+), 268 deletions(-) delete mode 100644 src/app/catalog/[shop]/[category]/[indexCategoryParam]/loading.tsx delete mode 100644 src/app/catalog/[shop]/[category]/loading.tsx delete mode 100644 src/app/catalog/[shop]/product/[item]/loading.tsx delete mode 100644 src/app/favicon.ico delete mode 100644 src/app/lib/registry.tsx delete mode 100644 src/app/login/loading.tsx delete mode 100644 src/app/profile/loading.tsx delete mode 100644 src/app/signup/loading.tsx delete mode 100644 src/components/slider/index.ts delete mode 100644 src/components/slider/slider.tsx delete mode 100644 src/components/slider/styled.tsx rename src/{app => lib}/query-provider.tsx (63%) rename src/{app => lib}/store-provider.tsx (100%) rename src/{app => styles}/globals.css (100%) delete mode 100644 src/styles/globals.ts rename src/{page => view}/catalog-page/catalog-page.tsx (100%) rename src/{page => view}/catalog-page/index.ts (100%) rename src/{page => view}/catalog-page/utils/helpers/get-tab-index.ts (100%) rename src/{page => view}/catalog-page/utils/helpers/index.ts (100%) rename src/{page => view}/catalog-page/utils/index.ts (100%) rename src/{page => view}/category-page/category-page.tsx (100%) rename src/{page => view}/category-page/index.ts (100%) rename src/{page => view}/index.ts (100%) rename src/{page => view}/login-page/index.ts (100%) rename src/{page => view}/login-page/login-page.tsx (100%) rename src/{page => view}/product-page/index.ts (100%) rename src/{page => view}/product-page/product-page.tsx (100%) rename src/{page => view}/profile-page/index.ts (100%) rename src/{page => view}/profile-page/profile-page.tsx (100%) rename src/{page => view}/register-page/index.ts (100%) rename src/{page => view}/register-page/register-page.tsx (100%) rename src/{page => view}/shop-list-page/index.ts (100%) rename src/{page => view}/shop-list-page/shop-list-page.tsx (100%) diff --git a/src/app/catalog/[shop]/[category]/[indexCategoryParam]/loading.tsx b/src/app/catalog/[shop]/[category]/[indexCategoryParam]/loading.tsx deleted file mode 100644 index e8b963e..0000000 --- a/src/app/catalog/[shop]/[category]/[indexCategoryParam]/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -import { Preloader } from '@/vendor'; - -const Loading = () => { - return ; -}; - -export default Loading; diff --git a/src/app/catalog/[shop]/[category]/[indexCategoryParam]/page.tsx b/src/app/catalog/[shop]/[category]/[indexCategoryParam]/page.tsx index 6940509..5b9cc50 100644 --- a/src/app/catalog/[shop]/[category]/[indexCategoryParam]/page.tsx +++ b/src/app/catalog/[shop]/[category]/[indexCategoryParam]/page.tsx @@ -1,17 +1,20 @@ import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; -import { Suspense } from 'react'; +import { FC, Suspense } from 'react'; -import { CategoryPage } from '@/page'; +import { CategoryPage } from '@/view'; import { getQueryClient } from '@/utils'; import { getCatalog } from '@/api'; -import Loading from './loading'; +import Loading from '../../../../loading'; -const Category = ({ - params, -}: { - params: { shop: string; category: string; indexCategoryParam: string }; -}) => { +interface ICategory { + params: { + shop: string; + category: string; + indexCategoryParam: string; + }; +} +const Category: FC = ({ params }) => { const { shop, category, indexCategoryParam } = params; const queryClient = getQueryClient(); diff --git a/src/app/catalog/[shop]/[category]/loading.tsx b/src/app/catalog/[shop]/[category]/loading.tsx deleted file mode 100644 index e8b963e..0000000 --- a/src/app/catalog/[shop]/[category]/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -import { Preloader } from '@/vendor'; - -const Loading = () => { - return ; -}; - -export default Loading; diff --git a/src/app/catalog/[shop]/[category]/page.tsx b/src/app/catalog/[shop]/[category]/page.tsx index 380dd6e..5de0040 100644 --- a/src/app/catalog/[shop]/[category]/page.tsx +++ b/src/app/catalog/[shop]/[category]/page.tsx @@ -1,17 +1,19 @@ import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; -import { Suspense } from 'react'; +import { FC, Suspense } from 'react'; -import { CatalogPage } from '@/page'; +import { CatalogPage } from '@/view'; import { getQueryClient } from '@/utils'; import { getCatalog } from '@/api'; -import Loading from './loading'; +import Loading from '../../../loading'; -const Catalog = async ({ - params, -}: { - params: { shop: string; category: string }; -}) => { +interface ICatalog { + params: { + shop: string; + category: string; + }; +} +const Catalog: FC = async ({ params }) => { const { shop, category } = params; const queryClient = getQueryClient(); await queryClient.prefetchQuery({ diff --git a/src/app/catalog/[shop]/product/[item]/loading.tsx b/src/app/catalog/[shop]/product/[item]/loading.tsx deleted file mode 100644 index e8b963e..0000000 --- a/src/app/catalog/[shop]/product/[item]/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -import { Preloader } from '@/vendor'; - -const Loading = () => { - return ; -}; - -export default Loading; diff --git a/src/app/catalog/[shop]/product/[item]/page.tsx b/src/app/catalog/[shop]/product/[item]/page.tsx index afcd220..6d16439 100644 --- a/src/app/catalog/[shop]/product/[item]/page.tsx +++ b/src/app/catalog/[shop]/product/[item]/page.tsx @@ -1,17 +1,20 @@ import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; -import { Suspense } from 'react'; +import { FC, Suspense } from 'react'; -import { ProductPage } from '@/page'; +import { ProductPage } from '@/view'; import { getQueryClient } from '@/utils'; import { getProduct, postRecommendations } from '@/api'; -import Loading from './loading'; +import Loading from '../../../../loading'; -const Product = async ({ - params, -}: { - params: { shop: string; item: string }; -}) => { +interface IProduct { + params: { + shop: string; + item: string; + }; +} + +const Product: FC = async ({ params }) => { const { shop, item } = params; const queryClient = getQueryClient(); diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c96ac0b..c8ed436 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -11,10 +11,10 @@ import { Widget, } from '@/components'; -import StoreProvider from './store-provider'; -import './globals.css'; +import StoreProvider from '../lib/store-provider'; +import '../styles/globals.css'; import StyledComponentsRegistry from '../lib/registry'; -import ReactQueryProvider from './query-provider'; +import ReactQueryProvider from '../lib/query-provider'; import type { Metadata } from 'next'; diff --git a/src/app/lib/registry.tsx b/src/app/lib/registry.tsx deleted file mode 100644 index 04b0fea..0000000 --- a/src/app/lib/registry.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import { ReactNode, useState } from 'react'; -import { useServerInsertedHTML } from 'next/navigation'; -import { ServerStyleSheet, StyleSheetManager } from 'styled-components'; - - -const StyledComponentsRegistry = ({ - children, -}: { - children: ReactNode; -}) => { - const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()); - - useServerInsertedHTML(() => { - const styles = styledComponentsStyleSheet.getStyleElement(); - styledComponentsStyleSheet.instance.clearTag(); - return <>{ styles }; - }); - - if (typeof window !== 'undefined') return <>{ children }; - - return ( - - { children } - - ); -}; - -export default StyledComponentsRegistry; diff --git a/src/app/loading.tsx b/src/app/loading.tsx index e8b963e..4c299bc 100644 --- a/src/app/loading.tsx +++ b/src/app/loading.tsx @@ -3,7 +3,7 @@ import { Preloader } from '@/vendor'; const Loading = () => { - return ; + return ; }; export default Loading; diff --git a/src/app/login/loading.tsx b/src/app/login/loading.tsx deleted file mode 100644 index e8b963e..0000000 --- a/src/app/login/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -import { Preloader } from '@/vendor'; - -const Loading = () => { - return ; -}; - -export default Loading; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 892517d..b6f551e 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,8 +1,8 @@ import { Suspense } from 'react'; -import { LoginPage } from '@/page'; +import { LoginPage } from '@/view'; -import Loading from './loading'; +import Loading from '../loading'; const Login = () => { return ( diff --git a/src/app/page.tsx b/src/app/page.tsx index f68e871..3b39017 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,12 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; +import { Suspense } from 'react'; -import { ShopListPage } from '@/page'; +import { ShopListPage } from '@/view'; import { getShops } from '@/api'; import { getQueryClient } from '@/utils'; +import Loading from './loading'; + const Home = async () => { const queryClient = getQueryClient(); await queryClient.prefetchQuery({ @@ -14,9 +17,11 @@ const Home = async () => { const dehydratedState = dehydrate(queryClient); return ( - - - + }> + + + + ); }; diff --git a/src/app/profile/loading.tsx b/src/app/profile/loading.tsx deleted file mode 100644 index e8b963e..0000000 --- a/src/app/profile/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -import { Preloader } from '@/vendor'; - -const Loading = () => { - return ; -}; - -export default Loading; diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 3c636d0..928e2d7 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -1,8 +1,8 @@ import { Suspense } from 'react'; -import { ProfilePage } from '@/page'; +import { ProfilePage } from '@/view'; -import Loading from './loading'; +import Loading from '../loading'; const Profile = () => { return ( diff --git a/src/app/signup/loading.tsx b/src/app/signup/loading.tsx deleted file mode 100644 index e8b963e..0000000 --- a/src/app/signup/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -import { Preloader } from '@/vendor'; - -const Loading = () => { - return ; -}; - -export default Loading; diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 5cff167..1fcb613 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -1,8 +1,8 @@ import { Suspense } from 'react'; -import { RegisterPage } from '@/page'; +import { RegisterPage } from '@/view'; -import Loading from './loading'; +import Loading from '../loading'; const Register = () => { return ( diff --git a/src/components/shop-list/ui/card-info/card-info.tsx b/src/components/shop-list/ui/card-info/card-info.tsx index 181314f..e0d6632 100644 --- a/src/components/shop-list/ui/card-info/card-info.tsx +++ b/src/components/shop-list/ui/card-info/card-info.tsx @@ -34,23 +34,6 @@ export const CardInfo: FC = ({ shop }) => { ))} - {/* - ( - - - - {category.name} - - - ))} - /> */} diff --git a/src/components/shop-list/ui/shop-image/styled.tsx b/src/components/shop-list/ui/shop-image/styled.tsx index 49071e3..f7e1805 100644 --- a/src/components/shop-list/ui/shop-image/styled.tsx +++ b/src/components/shop-list/ui/shop-image/styled.tsx @@ -16,7 +16,7 @@ export const ImageWrapper = styled.div` margin-bottom: 2px; overflow: hidden; line-height: 0; - background-color: rgb(var(--black-plus-rgb) 0.44); + background-color: rgb(56 56 56 / 44%); border: 2px solid var(--black-plus); border-radius: 5px; box-shadow: 2px 2px 2px rgb(0 0 0 / 28%); diff --git a/src/components/slider/index.ts b/src/components/slider/index.ts deleted file mode 100644 index eb0742f..0000000 --- a/src/components/slider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './slider'; diff --git a/src/components/slider/slider.tsx b/src/components/slider/slider.tsx deleted file mode 100644 index 5fb7718..0000000 --- a/src/components/slider/slider.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client'; - -import { FC, ReactNode } from 'react'; - -import * as Styled from './styled'; - -interface SliderProps { - items: ReactNode[]; - spaceBetween: number; -} - -export const Slider: FC = ({ items, spaceBetween }) => { - return ( - - {items.map((item, index) => - item ? {item} : null, - )} - - ); -}; diff --git a/src/components/slider/styled.tsx b/src/components/slider/styled.tsx deleted file mode 100644 index 3060519..0000000 --- a/src/components/slider/styled.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import styled from 'styled-components'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import 'swiper/css'; - -export const Slider = styled(Swiper)` - padding: 0 20px; - margin: 0; - cursor: pointer; -`; - -export const SliderItem = styled(SwiperSlide)` - width: fit-content; -`; diff --git a/src/app/query-provider.tsx b/src/lib/query-provider.tsx similarity index 63% rename from src/app/query-provider.tsx rename to src/lib/query-provider.tsx index 4ac0076..f2ff60a 100644 --- a/src/app/query-provider.tsx +++ b/src/lib/query-provider.tsx @@ -1,9 +1,13 @@ 'use client'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; -import { ReactNode, useState } from 'react'; +import { FC, ReactNode, useState } from 'react'; -const ReactQueryProvider = ({ children }: { children: ReactNode }) => { +interface IReactQueryProvider { + children: ReactNode; +} + +const ReactQueryProvider: FC = ({ children }) => { const [queryClient] = useState(() => new QueryClient()); return ( diff --git a/src/app/store-provider.tsx b/src/lib/store-provider.tsx similarity index 100% rename from src/app/store-provider.tsx rename to src/lib/store-provider.tsx diff --git a/src/shared/back-group/back-group.tsx b/src/shared/back-group/back-group.tsx index 429a243..805b881 100644 --- a/src/shared/back-group/back-group.tsx +++ b/src/shared/back-group/back-group.tsx @@ -1,3 +1,5 @@ +'use client'; + import { FC } from 'react'; import { ActionButton } from '../buttons'; diff --git a/src/shared/form-fields/auth-checkbox/auth-checkbox.tsx b/src/shared/form-fields/auth-checkbox/auth-checkbox.tsx index 1c0f838..a9a74a7 100644 --- a/src/shared/form-fields/auth-checkbox/auth-checkbox.tsx +++ b/src/shared/form-fields/auth-checkbox/auth-checkbox.tsx @@ -1,3 +1,5 @@ +'use client'; + import { FC } from 'react'; import { Control, Controller } from 'react-hook-form'; diff --git a/src/shared/icon/icon.tsx b/src/shared/icon/icon.tsx index 4d51f00..19cb57f 100644 --- a/src/shared/icon/icon.tsx +++ b/src/shared/icon/icon.tsx @@ -1,3 +1,5 @@ +'use client'; + import { FC } from 'react'; import * as Styled from './styled'; diff --git a/src/shared/layout-modal/layout-modal.tsx b/src/shared/layout-modal/layout-modal.tsx index 4b93c76..19766ee 100644 --- a/src/shared/layout-modal/layout-modal.tsx +++ b/src/shared/layout-modal/layout-modal.tsx @@ -1,3 +1,5 @@ +'use client'; + import { FC, ReactNode } from 'react'; import * as Styled from './styled'; diff --git a/src/shared/overlay/overlay.tsx b/src/shared/overlay/overlay.tsx index 17cb9cf..1f577b5 100644 --- a/src/shared/overlay/overlay.tsx +++ b/src/shared/overlay/overlay.tsx @@ -1,3 +1,5 @@ +'use client'; + import { FC, ReactNode } from 'react'; import * as Styled from './styled'; diff --git a/src/shared/product-card/product-card.tsx b/src/shared/product-card/product-card.tsx index 3ce9168..048d433 100644 --- a/src/shared/product-card/product-card.tsx +++ b/src/shared/product-card/product-card.tsx @@ -1,3 +1,5 @@ +'use client'; + import { FC, ReactNode } from 'react'; import * as Styled from './styled'; diff --git a/src/shared/tab/tab.tsx b/src/shared/tab/tab.tsx index 896eb1a..11faa0a 100644 --- a/src/shared/tab/tab.tsx +++ b/src/shared/tab/tab.tsx @@ -1,3 +1,5 @@ +'use client'; + import { FC } from 'react'; import * as Styled from './styled'; diff --git a/src/app/globals.css b/src/styles/globals.css similarity index 100% rename from src/app/globals.css rename to src/styles/globals.css diff --git a/src/styles/globals.ts b/src/styles/globals.ts deleted file mode 100644 index 4e49df9..0000000 --- a/src/styles/globals.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { css } from 'styled-components'; - -export const globals = css` - :root { - * { - margin: 0; - padding: 0; - } - - h1, - h2, - h3 { - margin: 0; - } - - ol, - ul { - list-style: none; - } - - img { - max-width: 100%; - display: block; - } - - a { - text-decoration: none; - color: inherit; - } - - table { - border-collapse: collapse; - border-spacing: 0; - } - - button { - cursor: pointer; - } - - button, - input { - border: none; - background: none; - box-shadow: none; - } - - input[type='text'], - input[type='search'] { - -webkit-appearance: none; - appearance: none; - } - - fieldset { - border: none; - padding: 0; - } - - --family: Gilroy, sans-serif; - --family-secondary: Comfortaa, sans-serif; - - --white: #f3f4f0; - --gray-plus: #bebebe; - --gray: #959595; - --black-plus: #383838; - --black-plus-rgb: 56, 56, 56; - --black: #242424; - --brown: #cb9666; - --pink-plus: #f3b6d1; - --pink: #f669a2; - --red-plus: #fe6a69; - --red: #fa5452; - --yellow-plus: #ffc633; - --yellow: #ffb92a; - --aquamarine-plus: #a5dfdd; - --aquamarine: #23bcc7; - --pistachio: #b1d465; - --green-plus: #5dd1b7; - --green: #51c7a5; - --green-minus: #14ad99; - --violet-plus: #a36ebe; - --violet: #9e50c7; - --blue-plus: #01a9d5; - --blue: #027ec2; - --sky-blue: #a7c3f8; - } -`; diff --git a/src/styles/index.ts b/src/styles/index.ts index 8b1dcca..33d4737 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1,2 +1 @@ export * from './media'; -export * from './globals'; diff --git a/src/vendor/preloader/preloader.tsx b/src/vendor/preloader/preloader.tsx index b38ad29..381e4cb 100644 --- a/src/vendor/preloader/preloader.tsx +++ b/src/vendor/preloader/preloader.tsx @@ -1,6 +1,11 @@ +import { FC } from 'react'; + import * as Styled from './styled'; -export const Preloader = ({ colorItem }: { colorItem: string }) => ( +interface IPreloader { + colorItem?: string; +} +export const Preloader: FC = ({ colorItem = '' }) => ( diff --git a/src/page/catalog-page/catalog-page.tsx b/src/view/catalog-page/catalog-page.tsx similarity index 100% rename from src/page/catalog-page/catalog-page.tsx rename to src/view/catalog-page/catalog-page.tsx diff --git a/src/page/catalog-page/index.ts b/src/view/catalog-page/index.ts similarity index 100% rename from src/page/catalog-page/index.ts rename to src/view/catalog-page/index.ts diff --git a/src/page/catalog-page/utils/helpers/get-tab-index.ts b/src/view/catalog-page/utils/helpers/get-tab-index.ts similarity index 100% rename from src/page/catalog-page/utils/helpers/get-tab-index.ts rename to src/view/catalog-page/utils/helpers/get-tab-index.ts diff --git a/src/page/catalog-page/utils/helpers/index.ts b/src/view/catalog-page/utils/helpers/index.ts similarity index 100% rename from src/page/catalog-page/utils/helpers/index.ts rename to src/view/catalog-page/utils/helpers/index.ts diff --git a/src/page/catalog-page/utils/index.ts b/src/view/catalog-page/utils/index.ts similarity index 100% rename from src/page/catalog-page/utils/index.ts rename to src/view/catalog-page/utils/index.ts diff --git a/src/page/category-page/category-page.tsx b/src/view/category-page/category-page.tsx similarity index 100% rename from src/page/category-page/category-page.tsx rename to src/view/category-page/category-page.tsx diff --git a/src/page/category-page/index.ts b/src/view/category-page/index.ts similarity index 100% rename from src/page/category-page/index.ts rename to src/view/category-page/index.ts diff --git a/src/page/index.ts b/src/view/index.ts similarity index 100% rename from src/page/index.ts rename to src/view/index.ts diff --git a/src/page/login-page/index.ts b/src/view/login-page/index.ts similarity index 100% rename from src/page/login-page/index.ts rename to src/view/login-page/index.ts diff --git a/src/page/login-page/login-page.tsx b/src/view/login-page/login-page.tsx similarity index 100% rename from src/page/login-page/login-page.tsx rename to src/view/login-page/login-page.tsx diff --git a/src/page/product-page/index.ts b/src/view/product-page/index.ts similarity index 100% rename from src/page/product-page/index.ts rename to src/view/product-page/index.ts diff --git a/src/page/product-page/product-page.tsx b/src/view/product-page/product-page.tsx similarity index 100% rename from src/page/product-page/product-page.tsx rename to src/view/product-page/product-page.tsx diff --git a/src/page/profile-page/index.ts b/src/view/profile-page/index.ts similarity index 100% rename from src/page/profile-page/index.ts rename to src/view/profile-page/index.ts diff --git a/src/page/profile-page/profile-page.tsx b/src/view/profile-page/profile-page.tsx similarity index 100% rename from src/page/profile-page/profile-page.tsx rename to src/view/profile-page/profile-page.tsx diff --git a/src/page/register-page/index.ts b/src/view/register-page/index.ts similarity index 100% rename from src/page/register-page/index.ts rename to src/view/register-page/index.ts diff --git a/src/page/register-page/register-page.tsx b/src/view/register-page/register-page.tsx similarity index 100% rename from src/page/register-page/register-page.tsx rename to src/view/register-page/register-page.tsx diff --git a/src/page/shop-list-page/index.ts b/src/view/shop-list-page/index.ts similarity index 100% rename from src/page/shop-list-page/index.ts rename to src/view/shop-list-page/index.ts diff --git a/src/page/shop-list-page/shop-list-page.tsx b/src/view/shop-list-page/shop-list-page.tsx similarity index 100% rename from src/page/shop-list-page/shop-list-page.tsx rename to src/view/shop-list-page/shop-list-page.tsx -- GitLab From 9cec797815bf9f47d2edb53105bf41a2a92477ae Mon Sep 17 00:00:00 2001 From: AlexandrValgamov Date: Wed, 5 Jun 2024 13:17:40 +0500 Subject: [PATCH 9/9] Catalog | chore: fix sc registry --- src/lib/registry.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/registry.tsx b/src/lib/registry.tsx index d65f43b..4e5a9c8 100644 --- a/src/lib/registry.tsx +++ b/src/lib/registry.tsx @@ -1,10 +1,15 @@ 'use client'; -import { ReactNode, useState } from 'react'; +import { FC, ReactNode, useState } from 'react'; import { useServerInsertedHTML } from 'next/navigation'; import { ServerStyleSheet, StyleSheetManager } from 'styled-components'; -const StyledComponentsRegistry = ({ children }: { children: ReactNode }) => { +interface IStyledComponentsRegistry { + children: ReactNode; +} +const StyledComponentsRegistry: FC = ({ + children, +}) => { const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()); useServerInsertedHTML(() => { -- GitLab