mirror of
https://github.com/vercel/commerce.git
synced 2025-07-27 04:01:23 +00:00
Merge branch 'common' into m5-sonnguyen
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
@import "../../../styles/utilities";
|
||||
|
||||
.banner {
|
||||
@apply bg-primary-light custom-border-radius-lg overflow-hidden;
|
||||
@screen md {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
&.large {
|
||||
margin-bottom: 2.8rem;
|
||||
.inner {
|
||||
@screen xl {
|
||||
@apply bg-right-bottom;
|
||||
background-size: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
.inner {
|
||||
@apply bg-no-repeat;
|
||||
background-size: 90%;
|
||||
background-position: right -500% bottom 0%;
|
||||
.content {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
rgb(227, 242, 233, 0.9),
|
||||
rgb(227, 242, 233, 0.5) 80%,
|
||||
rgb(227, 242, 233, 0)
|
||||
);
|
||||
padding: 1.6rem;
|
||||
max-width: 37rem;
|
||||
@screen md {
|
||||
max-width: 49.6rem;
|
||||
padding: 4.8rem;
|
||||
}
|
||||
.top {
|
||||
.heading {
|
||||
@apply heading-1 font-heading;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
.subHeading {
|
||||
@apply sub-headline;
|
||||
@screen md {
|
||||
@apply caption;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,47 +1,24 @@
|
||||
import classNames from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import React, { memo } from 'react'
|
||||
import { IconArrowRight } from 'src/components/icons'
|
||||
import { ROUTE } from 'src/utils/constanst.utils'
|
||||
import { LANGUAGE } from 'src/utils/language.utils'
|
||||
import ButtonCommon from '../ButtonCommon/ButtonCommon'
|
||||
import s from './Banner.module.scss'
|
||||
import CarouselCommon from '../CarouselCommon/CarouselCommon'
|
||||
import BannerItem, { BannerItemProps } from './BannerItem/BannerItem'
|
||||
|
||||
interface Props {
|
||||
imgLink: string,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
buttonLabel?: string,
|
||||
linkButton?: string,
|
||||
size?: 'small' | 'large',
|
||||
data: BannerItemProps[],
|
||||
}
|
||||
|
||||
const Banner = memo(({ imgLink, title, subtitle, buttonLabel = LANGUAGE.BUTTON_LABEL.SHOP_NOW, linkButton = ROUTE.HOME, size = 'large' }: Props) => {
|
||||
const option = {
|
||||
slidesPerView: 1,
|
||||
breakpoints: {}
|
||||
}
|
||||
const Banner = memo(({ data }: Props) => {
|
||||
return (
|
||||
<div className={classNames({
|
||||
[s.banner]: true,
|
||||
[s[size]]: true,
|
||||
})}>
|
||||
<div className={s.inner} style={{ backgroundImage: `url(${imgLink})` }}>
|
||||
<div className={s.content}>
|
||||
<div className={s.top}>
|
||||
<h1 className={s.heading}>
|
||||
{title}
|
||||
</h1>
|
||||
<div className={s.subHeading}>
|
||||
{subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.bottom}>
|
||||
<Link href={linkButton}>
|
||||
<a>
|
||||
<ButtonCommon icon={<IconArrowRight />} isIconSuffix={true}>{buttonLabel}</ButtonCommon>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CarouselCommon<BannerItemProps>
|
||||
data={data}
|
||||
itemKey="banner"
|
||||
Component={BannerItem}
|
||||
option={option}
|
||||
isDot = {true}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
|
@@ -0,0 +1,52 @@
|
||||
@import "../../../../styles/utilities";
|
||||
|
||||
.bannerItem {
|
||||
@apply bg-primary-light custom-border-radius-lg overflow-hidden;
|
||||
@screen md {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
&.large {
|
||||
margin-bottom: 2.8rem;
|
||||
.inner {
|
||||
@screen xl {
|
||||
@apply bg-right-bottom;
|
||||
background-size: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
.inner {
|
||||
@apply bg-no-repeat;
|
||||
background-size: 90%;
|
||||
background-position: right -500% bottom 0%;
|
||||
.content {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
rgb(227, 242, 233, 0.9),
|
||||
rgb(227, 242, 233, 0.5) 80%,
|
||||
rgb(227, 242, 233, 0)
|
||||
);
|
||||
padding: 1.6rem;
|
||||
max-width: 37rem;
|
||||
@screen md {
|
||||
max-width: 49.6rem;
|
||||
padding: 4.8rem;
|
||||
}
|
||||
.top {
|
||||
.heading {
|
||||
@apply heading-1 font-heading;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
.subHeading {
|
||||
@apply sub-headline;
|
||||
@screen md {
|
||||
@apply caption;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
src/components/common/Banner/BannerItem/BannerItem.tsx
Normal file
48
src/components/common/Banner/BannerItem/BannerItem.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import classNames from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import React, { memo } from 'react'
|
||||
import { IconArrowRight } from 'src/components/icons'
|
||||
import { ROUTE } from 'src/utils/constanst.utils'
|
||||
import { LANGUAGE } from 'src/utils/language.utils'
|
||||
import ButtonCommon from '../../ButtonCommon/ButtonCommon'
|
||||
import s from './BannerItem.module.scss'
|
||||
|
||||
export interface BannerItemProps {
|
||||
imgLink: string,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
buttonLabel?: string,
|
||||
linkButton?: string,
|
||||
size?: 'small' | 'large',
|
||||
}
|
||||
|
||||
const BannerItem = memo(({ imgLink, title, subtitle, buttonLabel = LANGUAGE.BUTTON_LABEL.SHOP_NOW, linkButton = ROUTE.HOME, size = 'large' }: BannerItemProps) => {
|
||||
return (
|
||||
<div className={classNames({
|
||||
[s.bannerItem]: true,
|
||||
[s[size]]: true,
|
||||
})}>
|
||||
<div className={s.inner} style={{ backgroundImage: `url(${imgLink})` }}>
|
||||
<div className={s.content}>
|
||||
<div className={s.top}>
|
||||
<h1 className={s.heading}>
|
||||
{title}
|
||||
</h1>
|
||||
<div className={s.subHeading}>
|
||||
{subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.bottom}>
|
||||
<Link href={linkButton}>
|
||||
<a>
|
||||
<ButtonCommon icon={<IconArrowRight />} isIconSuffix={true}>{buttonLabel}</ButtonCommon>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default BannerItem
|
@@ -1,6 +1,11 @@
|
||||
@import '../../../styles/utilities';
|
||||
|
||||
.breadcrumbCommon {
|
||||
@apply spacing-horizontal-left;
|
||||
color: var(--text-base);
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
.currentItem {
|
||||
cursor: default;
|
||||
}
|
||||
>>>>>>> a9f9f06eb9dee2a1ddefe907ff804237a78c5210
|
||||
}
|
||||
|
@@ -1,64 +1,37 @@
|
||||
import React from 'react'
|
||||
import { ROUTE } from 'src/utils/constanst.utils'
|
||||
import s from './BreadcrumbCommon.module.scss'
|
||||
|
||||
import BreadcrumbItem from './components/BreadcrumbItem/BreadcrumbItem'
|
||||
import BreadcrumbSeparator from './components/BreadcrumbSeparator/BreadcrumbSeparator'
|
||||
|
||||
interface BreadcrumbCommonProps {
|
||||
crumbs: { link:string, name:string }[];
|
||||
crumbs: { link: string, name: string }[];
|
||||
showHomePage?: boolean;
|
||||
}
|
||||
|
||||
const BreadcrumbCommon = ({ crumbs, showHomePage=true } : BreadcrumbCommonProps) => {
|
||||
const BreadcrumbCommon = ({ crumbs, showHomePage = true }: BreadcrumbCommonProps) => {
|
||||
return (
|
||||
<section className={s.breadcrumbCommon}>
|
||||
{
|
||||
showHomePage && crumbs[0].link==="/" && crumbs.map((crumb, i) => {
|
||||
if (i === 0) {
|
||||
return (
|
||||
<BreadcrumbItem key={crumb.name} text={crumb.name} href={crumb.link} />
|
||||
)
|
||||
}
|
||||
if (i === crumbs.length-1) {
|
||||
return (
|
||||
<BreadcrumbSeparator key={crumb.name}>
|
||||
<span>{crumb.name}</span>
|
||||
</BreadcrumbSeparator>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<BreadcrumbSeparator key={crumb.name}>
|
||||
<BreadcrumbItem text={crumb.name} href={crumb.link} />
|
||||
</BreadcrumbSeparator>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
!showHomePage && crumbs.map((crumb, i) => {
|
||||
if (i === 0) {
|
||||
return
|
||||
}
|
||||
if (i === 1) {
|
||||
return (
|
||||
<BreadcrumbItem key={crumb.name} text={crumb.name} href={crumb.link} />
|
||||
)
|
||||
}
|
||||
if (i === crumbs.length-1) {
|
||||
return (
|
||||
<BreadcrumbSeparator key={crumb.name}>
|
||||
<span>{crumb.name}</span>
|
||||
</BreadcrumbSeparator>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<BreadcrumbSeparator key={crumb.name}>
|
||||
<BreadcrumbItem text={crumb.name} href={crumb.link} />
|
||||
</BreadcrumbSeparator>
|
||||
)
|
||||
})
|
||||
showHomePage && <BreadcrumbItem key='Home' text='Home' href={ROUTE.HOME} />
|
||||
}
|
||||
</section>
|
||||
{
|
||||
crumbs.length > 0 && <>
|
||||
|
||||
{
|
||||
crumbs.slice(0, crumbs.length - 1).map((crumb) => (
|
||||
< BreadcrumbSeparator key={crumb.name}>
|
||||
<BreadcrumbItem text={crumb.name} href={crumb.link} />
|
||||
</BreadcrumbSeparator>
|
||||
))}
|
||||
< BreadcrumbSeparator>
|
||||
<span className={s.currentItem}>{crumbs[crumbs.length - 1].name}</span>
|
||||
</BreadcrumbSeparator>
|
||||
</>
|
||||
}
|
||||
</section >
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,5 @@
|
||||
.breadcrumbItem {
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import s from './BreadcrumbItem.module.scss'
|
||||
|
||||
interface BreadcrumbItemProps {
|
||||
text: string;
|
||||
@@ -9,7 +10,7 @@ interface BreadcrumbItemProps {
|
||||
const BreadcrumbItem = ({ text, href }: BreadcrumbItemProps) => {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a>{text}</a>
|
||||
<a className={s.breadcrumbItem}>{text}</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
interface BreadcrumbSeparatorProps {
|
||||
children?: React.ReactNode;
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const BreadcrumbSeparator = ({ children }: BreadcrumbSeparatorProps) => {
|
||||
|
@@ -7,6 +7,9 @@
|
||||
align-items: center;
|
||||
padding: 1rem 2rem;
|
||||
@screen md {
|
||||
padding: 0.8rem 1.6rem;
|
||||
}
|
||||
@screen lg {
|
||||
padding: 0.8rem 3.2rem;
|
||||
}
|
||||
&:disabled {
|
||||
@@ -84,11 +87,14 @@
|
||||
padding: 1rem;
|
||||
}
|
||||
@screen md {
|
||||
padding: 1.6rem 4.8rem;
|
||||
padding: 1.6rem 3.2rem;
|
||||
&.onlyIcon {
|
||||
padding: 1.6rem;
|
||||
}
|
||||
}
|
||||
@screen lg {
|
||||
padding: 1.6rem 4.8rem;
|
||||
}
|
||||
&.loading {
|
||||
&::before {
|
||||
width: 2.4rem;
|
||||
|
34
src/components/common/CardBlog/CardBlog.module.scss
Normal file
34
src/components/common/CardBlog/CardBlog.module.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
@import "../../../styles/utilities";
|
||||
|
||||
.cardBlogWarpper {
|
||||
@apply inline-flex flex-col justify-start;
|
||||
max-width: 39.2rem;
|
||||
min-height: 34.4rem;
|
||||
.image {
|
||||
width: 100%;
|
||||
max-height: 22rem;
|
||||
border-radius: 2.4rem;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
padding: 1.6rem 0.8rem 0.4rem 0.8rem;
|
||||
@apply font-bold;
|
||||
font-size: 2rem;
|
||||
line-height: 2.8rem;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text-active);
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
padding: 0 0.8rem;
|
||||
@apply overflow-hidden overflow-ellipsis ;
|
||||
color: var(--text-label);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
31
src/components/common/CardBlog/CardBlog.tsx
Normal file
31
src/components/common/CardBlog/CardBlog.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { ROUTE } from 'src/utils/constanst.utils'
|
||||
import { BlogProps } from 'src/utils/types.utils'
|
||||
import s from './CardBlog.module.scss'
|
||||
export interface BlogCardProps extends BlogProps {
|
||||
// todo: edit when intergrate API
|
||||
|
||||
}
|
||||
|
||||
const CardBlog = ({ imageSrc, title, description, slug }: BlogCardProps) => {
|
||||
return (
|
||||
<div className={s.cardBlogWarpper}>
|
||||
<Link href={`${ROUTE.BLOG_DETAIL}/${slug}`}>
|
||||
<a>
|
||||
<div className={s.image}>
|
||||
<img src={imageSrc} alt="image cardblog" />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`${ROUTE.BLOG_DETAIL}/${slug}`}>
|
||||
<a>
|
||||
<div className={s.title}>{title}</div>
|
||||
</a>
|
||||
</Link>
|
||||
<div className={s.description}>{description}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardBlog
|
@@ -8,15 +8,22 @@
|
||||
:global(.customArrow) {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: .8rem;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
@apply absolute top-1/2 bg-background-arrow transform -translate-y-1/2 flex justify-center items-center transition duration-100;
|
||||
&:global(.leftArrow) {
|
||||
@apply left-0;
|
||||
@apply hidden left-0;
|
||||
@screen md {
|
||||
@apply flex
|
||||
}
|
||||
}
|
||||
&:global(.rightArrow) {
|
||||
@apply right-0;
|
||||
@apply hidden right-0;
|
||||
@screen md {
|
||||
@apply flex;
|
||||
}
|
||||
}
|
||||
&:global(.isDisabledArrow) {
|
||||
@apply hidden;
|
||||
|
@@ -28,7 +28,6 @@ const CarouselCommon = <T,>({
|
||||
option: { slideChanged,slidesPerView, ...sliderOption },
|
||||
}: CarouselCommonProps<T>) => {
|
||||
const [currentSlide, setCurrentSlide] = React.useState(0)
|
||||
// const [dotActive, setDotActive] = React.useState<number>(0)
|
||||
const [dotArr, setDotArr] = React.useState<number[]>([])
|
||||
const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({
|
||||
...sliderOption,
|
||||
|
@@ -1,20 +1,2 @@
|
||||
.navigationWrapper{
|
||||
:global(.customArrow) {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
&:focus{
|
||||
outline: none;
|
||||
}
|
||||
@apply absolute top-1/2 bg-background-arrow transform -translate-y-1/2 flex justify-center items-center transition duration-100;
|
||||
&.leftArrow{
|
||||
@apply left-0;
|
||||
}
|
||||
&.rightArrow{
|
||||
@apply right-0;
|
||||
}
|
||||
&.isDisabled{
|
||||
@apply hidden ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
12
src/components/common/CartDrawer/CartDrawer.module.scss
Normal file
12
src/components/common/CartDrawer/CartDrawer.module.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@import '../../../styles/utilities';
|
||||
|
||||
|
||||
.cartDrawer {
|
||||
@apply flex flex-col h-full;
|
||||
.body {
|
||||
@apply overflow-y-auto overflow-x-hidden h-full custom-scroll;
|
||||
}
|
||||
.bottom {
|
||||
padding-top: 1.6rem;
|
||||
}
|
||||
}
|
35
src/components/common/CartDrawer/CartDrawer.tsx
Normal file
35
src/components/common/CartDrawer/CartDrawer.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { PRODUCT_CART_DATA_TEST } from 'src/utils/demo-data';
|
||||
import { DrawerCommon } from '..';
|
||||
import s from './CartDrawer.module.scss';
|
||||
import CartCheckoutButton from './components/CartCheckoutButton/CartCheckoutButton';
|
||||
import CartMessage from './components/CartMessage/CartMessage';
|
||||
import CartRecommendation from './components/CartRecommendation/CartRecommendation';
|
||||
import ProductsInCart from './components/ProductsInCart/ProductsInCart';
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const CartDrawer = ({ visible, onClose }: Props) => {
|
||||
return (
|
||||
<DrawerCommon
|
||||
title={`Your cart (${PRODUCT_CART_DATA_TEST.length})`}
|
||||
visible={visible}
|
||||
onClose={onClose}>
|
||||
<div className={s.cartDrawer}>
|
||||
<div className={s.body}>
|
||||
<ProductsInCart data={PRODUCT_CART_DATA_TEST}/>
|
||||
<CartRecommendation />
|
||||
</div>
|
||||
<div className={s.bottom}>
|
||||
<CartMessage />
|
||||
<CartCheckoutButton />
|
||||
</div>
|
||||
</div>
|
||||
</DrawerCommon>
|
||||
)
|
||||
}
|
||||
|
||||
export default CartDrawer;
|
@@ -0,0 +1,6 @@
|
||||
.cartCheckoutButton {
|
||||
padding: 1.6rem;
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import React, { memo } from 'react';
|
||||
import { ButtonCommon } from 'src/components/common';
|
||||
import s from './CartCheckoutButton.module.scss';
|
||||
|
||||
const CartCheckoutButton = memo(() => {
|
||||
return (
|
||||
<div className={s.cartCheckoutButton}>
|
||||
<ButtonCommon size='large'>Check out - Rp 120.500</ButtonCommon>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default CartCheckoutButton;
|
@@ -0,0 +1,16 @@
|
||||
@import "../../../../../styles/utilities";
|
||||
|
||||
.cartMessage {
|
||||
@apply flex bg-info;
|
||||
padding: 1.2rem 1.6rem;
|
||||
.text {
|
||||
color: var(--white);
|
||||
font-weight: bold;
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
.icon {
|
||||
svg path {
|
||||
fill: var(--text-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
import React, { memo } from 'react';
|
||||
import { IconInfo } from 'src/components/icons';
|
||||
import s from './CartMessage.module.scss';
|
||||
|
||||
const CartMessage = memo(() => {
|
||||
return (
|
||||
<div className={s.cartMessage}>
|
||||
<div className={s.text}>
|
||||
You save - Rp 150
|
||||
</div>
|
||||
<div className={s.icon}>
|
||||
<IconInfo />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default CartMessage;
|
@@ -0,0 +1,26 @@
|
||||
@import '../../../../../styles/utilities';
|
||||
|
||||
.cartRecommendation {
|
||||
@apply w-full bg-background-gray;
|
||||
.top {
|
||||
@apply flex justify-between items-center;
|
||||
padding: 1.6rem;
|
||||
.heading {
|
||||
@apply font-bold text-active sm-headline;
|
||||
}
|
||||
}
|
||||
.productCardWarpper {
|
||||
padding-left: 1.6rem;
|
||||
:global(.customArrow) {
|
||||
@apply bg-line;
|
||||
@screen lg {
|
||||
&:global(.leftArrow) {
|
||||
left: calc(-6.4rem - 2rem);
|
||||
}
|
||||
&:global(.rightArrow) {
|
||||
right: calc(-6.4rem - 2rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
import { TOptionsEvents } from 'keen-slider';
|
||||
import React from 'react';
|
||||
import { CarouselCommon, ViewAllItem } from 'src/components/common';
|
||||
import ProductCard, { ProductCardProps } from 'src/components/common/ProductCard/ProductCard';
|
||||
import { ROUTE } from 'src/utils/constanst.utils';
|
||||
import { PRODUCT_DATA_TEST } from 'src/utils/demo-data';
|
||||
import s from './CartRecommendation.module.scss';
|
||||
|
||||
const option: TOptionsEvents = {
|
||||
slidesPerView: 2,
|
||||
mode: 'free',
|
||||
breakpoints: {
|
||||
'(min-width: 640px)': {
|
||||
slidesPerView: 1,
|
||||
},
|
||||
'(min-width: 768px)': {
|
||||
slidesPerView: 2.5,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const CartRecommendation = () => {
|
||||
return (
|
||||
<div className={s.cartRecommendation}>
|
||||
<div className={s.top}>
|
||||
<div className={s.heading}>
|
||||
Recommendation
|
||||
</div>
|
||||
<ViewAllItem link={ROUTE.PRODUCTS} />
|
||||
</div>
|
||||
<div className={s.productCardWarpper}>
|
||||
<CarouselCommon<ProductCardProps>
|
||||
data={PRODUCT_DATA_TEST}
|
||||
Component={ProductCard}
|
||||
itemKey="cart-recommendation"
|
||||
option={option}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CartRecommendation;
|
@@ -0,0 +1,54 @@
|
||||
@import "../../../../../styles/utilities";
|
||||
|
||||
.productCartItem {
|
||||
@apply grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
padding-bottom: 1.6rem;
|
||||
padding-top: 1.6rem;
|
||||
.info {
|
||||
@apply flex;
|
||||
.imgWrap {
|
||||
width: 11rem;
|
||||
height: 7.5rem;
|
||||
margin-right: 1.6rem;
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.detail {
|
||||
min-height: 9rem;
|
||||
.name {
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
.price {
|
||||
margin-top: 0.8rem;
|
||||
.old {
|
||||
margin-bottom: 0.8rem;
|
||||
.number {
|
||||
margin-right: 0.8rem;
|
||||
color: var(--text-label);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
.current {
|
||||
@apply text-active font-bold sm-headline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
@apply flex flex-col justify-between items-end;
|
||||
margin-left: 1.6rem;
|
||||
.iconDelete {
|
||||
@apply cursor-pointer;
|
||||
&:hover {
|
||||
svg path {
|
||||
fill: var(--negative);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link'
|
||||
import { QuanittyInput } from 'src/components/common';
|
||||
import { IconDelete } from 'src/components/icons';
|
||||
import { ROUTE } from 'src/utils/constanst.utils';
|
||||
import { ProductProps } from 'src/utils/types.utils';
|
||||
import ImgWithLink from '../../../ImgWithLink/ImgWithLink';
|
||||
import LabelCommon from '../../../LabelCommon/LabelCommon';
|
||||
import s from './ProductCartItem.module.scss';
|
||||
|
||||
export interface ProductCartItempProps extends ProductProps {
|
||||
quantity: number,
|
||||
}
|
||||
|
||||
const ProductCartItem = ({ name, slug, weight, price, oldPrice, discount, imageSrc, quantity }: ProductCartItempProps) => {
|
||||
return (
|
||||
<div className={s.productCartItem}>
|
||||
<div className={s.info}>
|
||||
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
|
||||
<a href="">
|
||||
<div className={s.imgWrap}>
|
||||
<ImgWithLink src={imageSrc} alt={name} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<div className={s.detail}>
|
||||
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
|
||||
<a>
|
||||
<div className={s.name}>
|
||||
{name} {weight ? `(${weight})` : ''}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<div className={s.price}>
|
||||
{
|
||||
oldPrice &&
|
||||
<div className={s.old}>
|
||||
<span className={s.number}>{oldPrice}</span>
|
||||
<LabelCommon type='discount'>{discount}</LabelCommon>
|
||||
</div>
|
||||
}
|
||||
<div className={s.current}>{price}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.actions}>
|
||||
<div className={s.iconDelete}>
|
||||
<IconDelete />
|
||||
</div>
|
||||
<QuanittyInput size='small' initValue={quantity} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductCartItem;
|
@@ -0,0 +1,9 @@
|
||||
.productsInCart {
|
||||
padding: 1.6rem 1.6rem 0 1.6rem;
|
||||
list-style: none;
|
||||
li {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--border-line);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import ProductCartItem, { ProductCartItempProps } from '../ProductCartItem/ProductCartItem';
|
||||
import s from './ProductsInCart.module.scss';
|
||||
|
||||
interface Props {
|
||||
data: ProductCartItempProps[]
|
||||
}
|
||||
|
||||
const ProductsInCart = ({ data }: Props) => {
|
||||
return (
|
||||
<ul className={s.productsInCart}>
|
||||
{
|
||||
data.map(item => <li key={item.name}>
|
||||
<ProductCartItem
|
||||
name={item.name}
|
||||
slug={item.slug}
|
||||
weight={item.weight}
|
||||
price={item.price}
|
||||
oldPrice={item.oldPrice}
|
||||
discount={item.discount}
|
||||
imageSrc={item.imageSrc}
|
||||
quantity={item.quantity}
|
||||
/>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductsInCart;
|
@@ -0,0 +1,66 @@
|
||||
@import "../../../../styles/utilities";
|
||||
|
||||
.collapseWrapper {
|
||||
@apply border-t border-b;
|
||||
border-color: var(--border-line);
|
||||
max-width: 80.4rem;
|
||||
min-height: 4rem;
|
||||
&.isActive {
|
||||
.title {
|
||||
@apply pb-0;
|
||||
}
|
||||
.contentContainer {
|
||||
@apply block;
|
||||
animation: ContentAnimationIn 0.5s ease-out;
|
||||
}
|
||||
.toggle {
|
||||
&:before {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
&:after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
@apply outline-none flex justify-between font-heading items-center pt-16 pb-16;
|
||||
font-size: 3.2rem;
|
||||
line-height: 4rem;
|
||||
letter-spacing: -0.01em;
|
||||
.toggle {
|
||||
height: 2.2rem;
|
||||
width: 2.2rem;
|
||||
position: relative;
|
||||
&:before,
|
||||
&:after {
|
||||
@apply absolute h-2;
|
||||
content: "";
|
||||
border-radius: 0.8rem;
|
||||
background: var(--text-active);
|
||||
top: 40%;
|
||||
width: 2.2rem;
|
||||
transition: transform 500ms ease;
|
||||
}
|
||||
&:before {
|
||||
transform-origin: center;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.contentContainer {
|
||||
@apply hidden pb-16;
|
||||
}
|
||||
@keyframes ContentAnimationIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-1.6rem);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import s from './CollapseChild.module.scss'
|
||||
import { useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import CollapseContent from './CollapseContent/CollapseContent'
|
||||
|
||||
interface CollapseProps{
|
||||
title?: string,
|
||||
content: Array<string>,
|
||||
isToggle?: boolean,
|
||||
}
|
||||
const CollapseChild = ({title, content, isToggle=false}: CollapseProps) => {
|
||||
const [isActive, changeActive] = useState(isToggle)
|
||||
|
||||
const handleToggle = () => {
|
||||
changeActive(!isActive)
|
||||
}
|
||||
return(
|
||||
<div className={classNames({
|
||||
[s.collapseWrapper] : true,
|
||||
[s.isActive] : isActive
|
||||
})}
|
||||
onClick = { handleToggle }
|
||||
>
|
||||
<div className={s.title}>
|
||||
<h4>{title}</h4>
|
||||
<div className={s.toggle}></div>
|
||||
</div>
|
||||
<div className={s.contentContainer}>
|
||||
{
|
||||
content.map(item => <CollapseContent key={item} content={item} />)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CollapseChild
|
@@ -0,0 +1,3 @@
|
||||
.content {
|
||||
margin-top: 1.6rem;
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
import s from './CollapseContent.module.scss'
|
||||
|
||||
interface CollapseContentProps{
|
||||
content: string
|
||||
}
|
||||
|
||||
const CollapseContent = ({content}: CollapseContentProps) => {
|
||||
return (
|
||||
<div className={s.content}>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CollapseContent
|
19
src/components/common/CollapseCommon/CollapseCommon.tsx
Normal file
19
src/components/common/CollapseCommon/CollapseCommon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import CollapseChild from './CollapseChild/CollapseChild'
|
||||
|
||||
interface CollapseCommonProps{
|
||||
data: {title: string, content: Array<string>}[],
|
||||
}
|
||||
|
||||
const CollapseCommon = ({data}: CollapseCommonProps) => {
|
||||
return (
|
||||
<section>
|
||||
{
|
||||
data.map(item =>
|
||||
<CollapseChild key={item.title} title={item.title} content={item.content}/>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default CollapseCommon
|
52
src/components/common/DrawerCommon/DrawerCommon.module.scss
Normal file
52
src/components/common/DrawerCommon/DrawerCommon.module.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
@import "../../../styles/utilities";
|
||||
|
||||
.drawerCommon {
|
||||
@apply fixed flex justify-end transition-all duration-200;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100vh;
|
||||
width: 90%;
|
||||
box-shadow: -3px 0 10px rgba(0, 0, 0, 0.15);
|
||||
z-index: 20000;
|
||||
|
||||
.inner {
|
||||
@apply flex flex-col bg-white;
|
||||
width: fit-content;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
@screen md {
|
||||
max-width: 52rem;
|
||||
}
|
||||
.top {
|
||||
@apply flex justify-between items-center;
|
||||
padding: 1.6rem;
|
||||
.heading {
|
||||
@apply sm-headline;
|
||||
}
|
||||
.iconClose {
|
||||
@apply cursor-pointer transition-all duration-200;
|
||||
&:hover {
|
||||
svg path {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
&.hide {
|
||||
transform: translateX(110%);
|
||||
}
|
||||
|
||||
@screen md {
|
||||
width: unset;
|
||||
.inner {
|
||||
min-width: 48rem;
|
||||
}
|
||||
}
|
||||
}
|
36
src/components/common/DrawerCommon/DrawerCommon.tsx
Normal file
36
src/components/common/DrawerCommon/DrawerCommon.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { useRef } from 'react';
|
||||
import s from './DrawerCommon.module.scss';
|
||||
import classNames from 'classnames';
|
||||
import { IconClose } from 'src/components/icons';
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
title?: string
|
||||
children?: React.ReactNode
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const DrawerCommon = ({ title, visible, children, onClose }: Props) => {
|
||||
return (
|
||||
<div className={classNames({
|
||||
[s.drawerCommon]: true,
|
||||
[s.hide]: !visible
|
||||
})}>
|
||||
<div className={s.inner}>
|
||||
<div className={s.top}>
|
||||
<h4 className={s.heading}>
|
||||
{title}
|
||||
</h4>
|
||||
<div className={s.iconClose} onClick={onClose}>
|
||||
<IconClose />
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.content}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DrawerCommon;
|
@@ -1,8 +1,9 @@
|
||||
import classNames from 'classnames'
|
||||
import React, { memo, useEffect, useState } from 'react'
|
||||
import { useModalCommon } from 'src/components/hooks/useModalCommon'
|
||||
import { useModalCommon } from 'src/components/hooks'
|
||||
import { isMobile } from 'src/utils/funtion.utils'
|
||||
import ModalAuthenticate from '../ModalAuthenticate/ModalAuthenticate'
|
||||
import ModalCreateUserInfo from '../ModalCreateUserInfo/ModalCreateUserInfo'
|
||||
import HeaderHighLight from './components/HeaderHighLight/HeaderHighLight'
|
||||
import HeaderMenu from './components/HeaderMenu/HeaderMenu'
|
||||
import HeaderSubMenu from './components/HeaderSubMenu/HeaderSubMenu'
|
||||
@@ -13,6 +14,7 @@ import s from './Header.module.scss'
|
||||
const Header = memo(() => {
|
||||
const [isFullHeader, setIsFullHeader] = useState<boolean>(true)
|
||||
const { visible: visibleModalAuthen, closeModal: closeModalAuthen, openModal: openModalAuthen } = useModalCommon({ initialValue: false })
|
||||
const { visible: visibleModalInfo, closeModal: closeModalInfo, openModal: openModalInfo } = useModalCommon({ initialValue: false })
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
@@ -35,12 +37,15 @@ const Header = memo(() => {
|
||||
<header className={classNames({ [s.header]: true, [s.full]: isFullHeader })}>
|
||||
<HeaderHighLight isShow={isFullHeader} />
|
||||
<div className={s.menu}>
|
||||
<HeaderMenu isFull={isFullHeader} openModalAuthen={openModalAuthen} />
|
||||
<HeaderMenu isFull={isFullHeader}
|
||||
openModalAuthen={openModalAuthen}
|
||||
openModalInfo={openModalInfo} />
|
||||
<HeaderSubMenu isShow={isFullHeader} />
|
||||
</div>
|
||||
</header>
|
||||
<HeaderSubMenuMobile />
|
||||
<ModalAuthenticate visible={visibleModalAuthen} closeModal={closeModalAuthen} />
|
||||
<ModalCreateUserInfo demoVisible={visibleModalInfo} demoCloseModal={closeModalInfo}/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
@@ -42,7 +42,7 @@
|
||||
@apply hidden;
|
||||
@screen md {
|
||||
@apply flex items-center list-none;
|
||||
li {
|
||||
> li {
|
||||
@apply flex justify-center items-center w-full;
|
||||
&:not(:last-child) {
|
||||
margin-right: 4.8rem;
|
||||
@@ -52,13 +52,23 @@
|
||||
}
|
||||
a {
|
||||
@appy no-underline;
|
||||
&.iconFovourite {
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
&.iconFavourite {
|
||||
svg path {
|
||||
fill: var(--negative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btnCart {
|
||||
&:hover {
|
||||
svg path {
|
||||
fill: var(--primary);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,20 +5,26 @@ import InputSearch from 'src/components/common/InputSearch/InputSearch'
|
||||
import MenuDropdown from 'src/components/common/MenuDropdown/MenuDropdown'
|
||||
import { IconBuy, IconHeart, IconHistory, IconUser } from 'src/components/icons'
|
||||
import { ACCOUNT_TAB, QUERY_KEY, ROUTE } from 'src/utils/constanst.utils'
|
||||
import Logo from '../../../Logo/Logo'
|
||||
import s from './HeaderMenu.module.scss'
|
||||
|
||||
interface Props {
|
||||
children?: any,
|
||||
isFull: boolean,
|
||||
openModalAuthen: () => void,
|
||||
openModalInfo: () => void,
|
||||
}
|
||||
|
||||
const HeaderMenu = memo(({ isFull, openModalAuthen }: Props) => {
|
||||
const HeaderMenu = memo(({ isFull, openModalAuthen, openModalInfo }: Props) => {
|
||||
const optionMenu = useMemo(() => [
|
||||
{
|
||||
onClick: openModalAuthen,
|
||||
name: 'Login (Demo)',
|
||||
},
|
||||
{
|
||||
onClick: openModalInfo,
|
||||
name: 'Create User Info (Demo)',
|
||||
},
|
||||
{
|
||||
link: ROUTE.ACCOUNT,
|
||||
name: 'Account',
|
||||
@@ -34,7 +40,7 @@ const HeaderMenu = memo(({ isFull, openModalAuthen }: Props) => {
|
||||
<section className={classNames({ [s.headerMenu]: true, [s.full]: isFull })}>
|
||||
<div className={s.left}>
|
||||
<div className={s.top}>
|
||||
<div>Online Grocery</div>
|
||||
<Logo/>
|
||||
<button className={s.iconCart}>
|
||||
<IconBuy />
|
||||
</button>
|
||||
@@ -46,14 +52,14 @@ const HeaderMenu = memo(({ isFull, openModalAuthen }: Props) => {
|
||||
<ul className={s.menu}>
|
||||
<li>
|
||||
<Link href={`${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.ORDER}`}>
|
||||
<a >
|
||||
<a>
|
||||
<IconHistory />
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={`${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.FAVOURITE}`}>
|
||||
<a className={s.iconFovourite}>
|
||||
<a className={s.iconFavourite}>
|
||||
<IconHeart />
|
||||
</a>
|
||||
</Link>
|
||||
@@ -62,7 +68,7 @@ const HeaderMenu = memo(({ isFull, openModalAuthen }: Props) => {
|
||||
<MenuDropdown options={optionMenu} isHasArrow={false}><IconUser /></MenuDropdown>
|
||||
</li>
|
||||
<li>
|
||||
<button>
|
||||
<button className={s.btnCart}>
|
||||
<IconBuy />
|
||||
</button>
|
||||
</li>
|
||||
|
@@ -0,0 +1,9 @@
|
||||
.imgWithLink {
|
||||
position: relative;
|
||||
min-width: 5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
img {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
18
src/components/common/ImgWithLink/ImgWithLink.tsx
Normal file
18
src/components/common/ImgWithLink/ImgWithLink.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import s from './ImgWithLink.module.scss'
|
||||
import Image from 'next/image'
|
||||
|
||||
export interface ImgWithLinkProps {
|
||||
src: string,
|
||||
alt?: string,
|
||||
}
|
||||
|
||||
const ImgWithLink = ({ src, alt }: ImgWithLinkProps) => {
|
||||
return (
|
||||
<div className={s.imgWithLink}>
|
||||
<Image src={src} alt={alt} layout="fill" className={s.imgWithLink} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImgWithLink
|
@@ -1,51 +1,94 @@
|
||||
@import "../../../styles/utilities";
|
||||
|
||||
.inputWrap {
|
||||
@apply flex items-center relative;
|
||||
.icon {
|
||||
@apply absolute;
|
||||
content: "";
|
||||
left: 1.6rem;
|
||||
margin-right: 1.6rem;
|
||||
svg path {
|
||||
fill: currentColor;
|
||||
.inputInner {
|
||||
@apply flex items-center relative;
|
||||
.icon {
|
||||
@apply absolute flex justify-center items-center;
|
||||
content: "";
|
||||
left: 1.6rem;
|
||||
margin-right: 1.6rem;
|
||||
svg path {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon + .inputCommon {
|
||||
padding-left: 4.8rem;
|
||||
}
|
||||
.inputCommon {
|
||||
@apply block w-full transition-all duration-200 rounded;
|
||||
padding: 1.2rem 1.6rem;
|
||||
border: 1px solid var(--border-line);
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: none;
|
||||
border: 1px solid var(--primary);
|
||||
@apply shadow-md;
|
||||
.icon + .inputCommon {
|
||||
padding-left: 4.8rem;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
@apply text-label;
|
||||
}
|
||||
|
||||
&.custom {
|
||||
@apply custom-border-radius;
|
||||
border: 1px solid transparent;
|
||||
background: var(--gray);
|
||||
.inputCommon {
|
||||
@apply block w-full transition-all duration-200 rounded;
|
||||
padding: 1.2rem 1.6rem;
|
||||
border: 1px solid var(--border-line);
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: none;
|
||||
border: 1px solid var(--primary);
|
||||
@apply shadow-md;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
@apply text-label;
|
||||
}
|
||||
|
||||
&.custom {
|
||||
@apply custom-border-radius;
|
||||
border: 1px solid transparent;
|
||||
background: var(--gray);
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
}
|
||||
&.bgTransparent {
|
||||
background: rgb(227, 242, 233, 0.3);
|
||||
color: var(--white);
|
||||
&::placeholder {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.bgTransparent {
|
||||
background: rgb(227, 242, 233, 0.3);
|
||||
color: var(--white);
|
||||
&::placeholder {
|
||||
color: var(--white);
|
||||
|
||||
&.preserve {
|
||||
@apply flex-row-reverse;
|
||||
.icon {
|
||||
left: unset;
|
||||
right: 1.6rem;
|
||||
margin-left: 1.6rem;
|
||||
margin-right: 0;
|
||||
svg path {
|
||||
fill: var(--text-label);
|
||||
}
|
||||
}
|
||||
.icon + .inputCommon {
|
||||
padding-left: 1.6rem;
|
||||
padding-right: 4.8rem;
|
||||
}
|
||||
}
|
||||
&.success {
|
||||
.icon {
|
||||
svg path {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
.icon {
|
||||
svg path {
|
||||
fill: var(--negative);
|
||||
}
|
||||
}
|
||||
input {
|
||||
border-color: var(--negative) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.errorMessage {
|
||||
@apply caption;
|
||||
color: var(--negative);
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import React, { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { IconCheck, IconError, IconPassword, IconPasswordCross } from 'src/components/icons';
|
||||
import { KEY } from 'src/utils/constanst.utils';
|
||||
import s from './InputCommon.module.scss';
|
||||
|
||||
@@ -14,14 +15,29 @@ interface Props {
|
||||
styleType?: 'default' | 'custom',
|
||||
backgroundTransparent?: boolean,
|
||||
icon?: React.ReactNode,
|
||||
isIconSuffix?: boolean,
|
||||
isShowIconSuccess?: boolean,
|
||||
error?: string,
|
||||
onChange?: (value: string | number) => void,
|
||||
onEnter?: (value: string | number) => void,
|
||||
}
|
||||
|
||||
const InputCommon = forwardRef<Ref, Props>(({ value, placeholder, type, styleType = 'default', icon, backgroundTransparent = false,
|
||||
isIconSuffix, isShowIconSuccess, error,
|
||||
onChange, onEnter }: Props, ref) => {
|
||||
const inputElementRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const iconElement = useMemo(() => {
|
||||
if (error) {
|
||||
return <span className={s.icon}><IconError /> </span>
|
||||
} else if (isShowIconSuccess) {
|
||||
return <span className={s.icon}><IconCheck /> </span>
|
||||
} else if (icon) {
|
||||
return <span className={s.icon}>{icon} </span>
|
||||
}
|
||||
return <></>
|
||||
}, [icon, error, isShowIconSuccess])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputElementRef.current?.focus();
|
||||
@@ -44,23 +60,33 @@ const InputCommon = forwardRef<Ref, Props>(({ value, placeholder, type, styleTyp
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.inputWrap}>
|
||||
<div className={classNames({
|
||||
[s.inputWrap]: true,
|
||||
})}>
|
||||
<div className={classNames({
|
||||
[s.inputInner]: true,
|
||||
[s.preserve]: isIconSuffix,
|
||||
[s.success]: isShowIconSuccess,
|
||||
[s.error]: !!error,
|
||||
})}>
|
||||
{iconElement}
|
||||
<input
|
||||
ref={inputElementRef}
|
||||
value={value}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={classNames({
|
||||
[s.inputCommon]: true,
|
||||
[s[styleType]]: true,
|
||||
[s.bgTransparent]: backgroundTransparent
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
icon && <span className={s.icon}>{icon}</span>
|
||||
error && <div className={s.errorMessage}>{error}</div>
|
||||
}
|
||||
<input
|
||||
ref={inputElementRef}
|
||||
value={value}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={classNames({
|
||||
[s.inputCommon]: true,
|
||||
[s[styleType]]: true,
|
||||
[s.bgTransparent]: backgroundTransparent
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
@@ -0,0 +1,10 @@
|
||||
.iconPassword {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--text-active);
|
||||
}
|
||||
}
|
40
src/components/common/InputPassword/InputPassword.tsx
Normal file
40
src/components/common/InputPassword/InputPassword.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconPassword, IconPasswordCross } from 'src/components/icons';
|
||||
import { Inputcommon } from '..';
|
||||
import s from './InputPassword.module.scss';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode,
|
||||
value?: string | number,
|
||||
placeholder?: string,
|
||||
styleType?: 'default' | 'custom',
|
||||
error?: string,
|
||||
onChange?: (value: string | number) => void,
|
||||
onEnter?: (value: string | number) => void,
|
||||
}
|
||||
|
||||
const InputPassword = ({ value, placeholder, styleType = 'default', error,
|
||||
onChange, onEnter }: Props) => {
|
||||
const [isShowPassword, setIsShowPassword] = useState<boolean>(false)
|
||||
const toggleShowPassword = () => {
|
||||
setIsShowPassword(!isShowPassword)
|
||||
}
|
||||
|
||||
return (
|
||||
<Inputcommon
|
||||
value={value}
|
||||
type={isShowPassword ? 'text' : 'password'}
|
||||
styleType={styleType}
|
||||
error={error}
|
||||
placeholder={placeholder}
|
||||
icon={<button className={s.iconPassword} onClick={toggleShowPassword}>
|
||||
{isShowPassword ? <IconPassword /> : <IconPasswordCross />}
|
||||
</button>}
|
||||
isIconSuffix={true}
|
||||
onChange={onChange}
|
||||
onEnter={onEnter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputPassword
|
@@ -1,6 +1,8 @@
|
||||
import { CommerceProvider } from '@framework'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC } from 'react'
|
||||
import { useModalCommon } from 'src/components/hooks'
|
||||
import { CartDrawer } from '..'
|
||||
import Footer from '../Footer/Footer'
|
||||
import Header from '../Header/Header'
|
||||
import s from './Layout.module.scss'
|
||||
@@ -13,12 +15,24 @@ interface Props {
|
||||
// note: demo code
|
||||
const Layout: FC<Props> = ({ children }) => {
|
||||
const { locale = 'en-US' } = useRouter()
|
||||
const { visible: visibleCartDrawer, openModal, closeModal: closeCartDrawer } = useModalCommon({ initialValue: false })
|
||||
|
||||
const toggle = () => {
|
||||
if (visibleCartDrawer) {
|
||||
closeCartDrawer()
|
||||
} else {
|
||||
openModal()
|
||||
}
|
||||
}
|
||||
return (
|
||||
<CommerceProvider locale={locale}>
|
||||
<div className={s.mainLayout}>
|
||||
<Header />
|
||||
<main >{children}</main>
|
||||
<button onClick={toggle}>toggle card: {visibleCartDrawer.toString()}</button>
|
||||
<CartDrawer
|
||||
visible={visibleCartDrawer}
|
||||
onClose={closeCartDrawer} />
|
||||
<Footer />
|
||||
</div>
|
||||
</CommerceProvider>
|
||||
|
@@ -0,0 +1,23 @@
|
||||
@import "../../../../styles/utilities";
|
||||
|
||||
.infoProducts {
|
||||
@apply flex justify-between items-center spacing-horizontal;
|
||||
|
||||
.top {
|
||||
.sub {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@screen lg {
|
||||
@apply block;
|
||||
margin-right: 4rem;
|
||||
padding: 0;
|
||||
.top {
|
||||
margin-bottom: 3.2rem;
|
||||
.sub {
|
||||
display: block;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { ROUTE } from 'src/utils/constanst.utils';
|
||||
import HeadingCommon from '../../HeadingCommon/HeadingCommon';
|
||||
import ViewAllItem from '../../ViewAllItem/ViewAllItem';
|
||||
import s from './InfoProducts.module.scss'
|
||||
interface Props {
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
}
|
||||
|
||||
const InfoProducts = ({ title, subtitle }: Props) => {
|
||||
return (
|
||||
<div className={s.infoProducts}>
|
||||
<div className={s.top}>
|
||||
<HeadingCommon>{title}</HeadingCommon>
|
||||
<div className={s.sub}>
|
||||
{subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<ViewAllItem link={ROUTE.PRODUCTS} />
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoProducts;
|
@@ -0,0 +1,48 @@
|
||||
@import "../../../styles/utilities";
|
||||
|
||||
.listProductWithInfo {
|
||||
background-color: var(--background);
|
||||
border-top: 1rem solid var(--gray);
|
||||
border-bottom: 1rem solid var(--gray);
|
||||
padding-top: 6rem;
|
||||
padding-bottom: 6rem;
|
||||
@screen lg {
|
||||
@apply flex spacing-horizontal-left;
|
||||
padding-top: 5.6rem;
|
||||
padding-bottom: 5.6rem;
|
||||
border: none;
|
||||
background-color: var(--background-gray);
|
||||
}
|
||||
.productsWrap {
|
||||
@apply spacing-horizontal-left;
|
||||
@screen lg {
|
||||
max-width: 75%;
|
||||
@apply custom-border-radius-lg bg-white;
|
||||
padding: 4rem .8rem;
|
||||
:global(.customArrow) {
|
||||
@screen lg {
|
||||
&:global(.leftArrow) {
|
||||
left: calc(-6.4rem + 3rem);
|
||||
}
|
||||
&:global(.rightArrow) {
|
||||
right: calc(-6.4rem + 3rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@screen xl {
|
||||
padding: 4rem 2.4rem;
|
||||
max-width: 80%;
|
||||
:global(.customArrow) {
|
||||
@screen lg {
|
||||
&:global(.leftArrow) {
|
||||
left: calc(-6.4rem + 1rem);
|
||||
}
|
||||
&:global(.rightArrow) {
|
||||
right: calc(-6.4rem + 1rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
import { TOptionsEvents } from 'keen-slider';
|
||||
import React from 'react';
|
||||
import CarouselCommon from '../CarouselCommon/CarouselCommon';
|
||||
import ProductCard, { ProductCardProps } from '../ProductCard/ProductCard';
|
||||
import InfoProducts from './InfoProducts/InfoProducts';
|
||||
import s from './ListProductWithInfo.module.scss';
|
||||
|
||||
interface Props {
|
||||
data: ProductCardProps[],
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
}
|
||||
const OPTION_DEFAULT: TOptionsEvents = {
|
||||
slidesPerView: 2,
|
||||
mode: 'free',
|
||||
breakpoints: {
|
||||
'(min-width: 640px)': {
|
||||
slidesPerView: 3,
|
||||
},
|
||||
'(min-width: 768px)': {
|
||||
slidesPerView: 4,
|
||||
},
|
||||
'(min-width: 1024px)': {
|
||||
slidesPerView: 3,
|
||||
},
|
||||
'(min-width: 1280px)': {
|
||||
slidesPerView: 4.5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const ListProductWithInfo = ({ data, title, subtitle }: Props) => {
|
||||
return (
|
||||
<div className={s.listProductWithInfo}>
|
||||
<InfoProducts
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
<div className={s.productsWrap}>
|
||||
<CarouselCommon<ProductCardProps>
|
||||
data={data}
|
||||
Component={ProductCard}
|
||||
itemKey={title}
|
||||
option={OPTION_DEFAULT}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListProductWithInfo;
|
@@ -1,7 +1,7 @@
|
||||
@import '../../../styles/utilities';
|
||||
@import "../../../styles/utilities";
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
@apply flex justify-center items-center;
|
||||
.eclipse {
|
||||
width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
@@ -10,9 +10,9 @@
|
||||
margin-right: 1.2rem;
|
||||
}
|
||||
.content {
|
||||
@apply font-logo;
|
||||
@apply font-logo font-bold;
|
||||
font-size: 16px;
|
||||
line-height: 32px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,14 +1,18 @@
|
||||
import s from './Logo.module.scss'
|
||||
import Link from 'next/link'
|
||||
import { ROUTE } from 'src/utils/constanst.utils'
|
||||
|
||||
const Logo = () => {
|
||||
return(
|
||||
<div className={s.logo}>
|
||||
<div className={s.eclipse}>
|
||||
</div>
|
||||
<div className={s.content}>
|
||||
ONLINE GROCERY
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<Link href={ROUTE.HOME}>
|
||||
<a className={s.logo}>
|
||||
<div className={s.eclipse}>
|
||||
</div>
|
||||
<div className={s.content}>
|
||||
ONLINE GROCERY
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -17,10 +17,21 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
all: unset;
|
||||
@apply flex justify-end items-center transition-all duration-200;
|
||||
svg path {
|
||||
width: fit-content;
|
||||
}
|
||||
&:focus,
|
||||
&:active {
|
||||
color: var(--primary);
|
||||
svg path {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid #000;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow {
|
||||
@@ -63,8 +74,9 @@
|
||||
@apply rounded list-none bg-white;
|
||||
border: 1px solid var(--text-active);
|
||||
margin-top: 0.4rem;
|
||||
li {
|
||||
> li {
|
||||
@apply block w-full transition-all duration-200 cursor-pointer text-active;
|
||||
white-space: nowrap;
|
||||
button {
|
||||
all: unset;
|
||||
color: currentColor;
|
||||
@@ -78,7 +90,7 @@
|
||||
@apply block;
|
||||
}
|
||||
&:hover {
|
||||
@apply bg-primary-lightest;
|
||||
@apply bg-gray;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
@@ -16,9 +16,9 @@ const MenuDropdown = ({ options, children, isHasArrow = true, align }: Props) =>
|
||||
[s.menuDropdown]: true,
|
||||
[s.arrow]: isHasArrow,
|
||||
})}>
|
||||
<span className={s.label}>
|
||||
<button className={s.label}>
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
<section className={classNames({
|
||||
[s.menu]: true,
|
||||
[s.left]: align === 'left',
|
||||
|
40
src/components/common/MenuFilter/MenuFilter.module.scss
Normal file
40
src/components/common/MenuFilter/MenuFilter.module.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
@import "../../../styles/utilities";
|
||||
.menuFilterWrapper{
|
||||
|
||||
@screen md {
|
||||
@apply hidden;
|
||||
}
|
||||
.menuFilterHeading{
|
||||
@apply sub-headline font-bold ;
|
||||
color: var(--text-active);
|
||||
font-feature-settings: 'salt' on;
|
||||
margin: 0.8rem 0;
|
||||
}
|
||||
.menuFilterList{
|
||||
@apply flex flex-wrap justify-start relative;
|
||||
margin-bottom: 3rem;
|
||||
box-sizing: border-box;
|
||||
&::after{
|
||||
@apply absolute;
|
||||
top: 110%;
|
||||
content: "";
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--border-line);
|
||||
}
|
||||
|
||||
li{
|
||||
margin: 1rem 0;
|
||||
padding:0;
|
||||
div{
|
||||
padding: 0.8rem 1.6rem;
|
||||
margin-right: 0.8rem;
|
||||
background-color: var(--gray);
|
||||
border-radius: 0.8rem;
|
||||
&.active {
|
||||
color:white;
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
src/components/common/MenuFilter/MenuFilter.tsx
Normal file
48
src/components/common/MenuFilter/MenuFilter.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import classNames from 'classnames'
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import s from './MenuFilter.module.scss'
|
||||
interface Props {
|
||||
children?: any,
|
||||
heading?:string,
|
||||
categories:{name:string,link:string}[],
|
||||
type:string,
|
||||
onChangeValue?: (value: Object) => void
|
||||
}
|
||||
|
||||
const MenuFilter = ({heading,categories,type,onChangeValue}:Props)=> {
|
||||
const [active, setActive] = useState<string>('');
|
||||
|
||||
function handleClick(link:string){
|
||||
setActive(link);
|
||||
|
||||
if(active === link){
|
||||
setActive('');
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
let href = active?.split("=");
|
||||
const linkValue = href[1];
|
||||
|
||||
onChangeValue && onChangeValue({[type]:linkValue});
|
||||
},[active])
|
||||
|
||||
return (
|
||||
<section className={s.menuFilterWrapper}>
|
||||
<h2 className={s.menuFilterHeading}>{heading}</h2>
|
||||
<ul className={s.menuFilterList}>
|
||||
{
|
||||
categories.map(item => <li key={item.name}>
|
||||
<div onClick={()=> handleClick(item.link)} className={classNames({ [s.active]: item.link === active? true: false })}>
|
||||
{item.name}
|
||||
</div>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuFilter
|
@@ -0,0 +1,29 @@
|
||||
@import "../../../styles/utilities";
|
||||
.menuNavigationWrapper{
|
||||
.menuNavigationHeading{
|
||||
@screen md {
|
||||
@apply sub-headline font-bold ;
|
||||
color: var(--text-active);
|
||||
font-feature-settings: 'salt' on;
|
||||
margin: 1.6rem 0;
|
||||
}
|
||||
}
|
||||
.menuNavigationList{
|
||||
@screen md {
|
||||
li{
|
||||
margin: 0.8rem 0;
|
||||
a{
|
||||
display:block;
|
||||
width:100%;
|
||||
color:var(--text-base);
|
||||
&:hover {
|
||||
@apply text-primary;
|
||||
}
|
||||
&.active {
|
||||
@apply text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
src/components/common/MenuNavigation/MenuNavigation.tsx
Normal file
34
src/components/common/MenuNavigation/MenuNavigation.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import classNames from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import s from './MenuNavigation.module.scss'
|
||||
|
||||
interface Props {
|
||||
children?: any,
|
||||
heading:string,
|
||||
categories:{name:string,link:string}[]
|
||||
}
|
||||
|
||||
const MenuNavigation = ({heading,categories}:Props)=> {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<section className={s.menuNavigationWrapper}>
|
||||
<h2 className={s.menuNavigationHeading}>{heading}({categories.length})</h2>
|
||||
<ul className={s.menuNavigationList}>
|
||||
{
|
||||
categories.map(item => <li key={item.name}
|
||||
>
|
||||
<Link href={item.link}>
|
||||
<a className={classNames({ [s.active]: router.asPath === item.link})}>
|
||||
{item.name}
|
||||
</a>
|
||||
</Link>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuNavigation
|
@@ -0,0 +1,45 @@
|
||||
@import "../../../styles/utilities";
|
||||
.menuNavigationProductListDesktop{
|
||||
@screen sm-only {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
.menuNavigationProductListMobile{
|
||||
@apply hidden;
|
||||
&.isShow{
|
||||
@apply block;
|
||||
@screen md {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
.menuNavigationProductModal{
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
.content{
|
||||
@apply spacing-horizontal;
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem ;
|
||||
padding-bottom: 5rem;
|
||||
background-color: white;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
border-radius: 2.4rem 2.4rem 0 0;
|
||||
.head{
|
||||
@apply flex justify-between;
|
||||
h3{
|
||||
@apply heading-3 font-bold;
|
||||
color:var(--text-base);
|
||||
}
|
||||
}
|
||||
button{
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
import {ButtonCommon} from 'src/components/common';
|
||||
import s from './MenuNavigationProductList.module.scss';
|
||||
import MenuSort from './MenuSort/MenuSort';
|
||||
import {LANGUAGE} from 'src/utils/language.utils';
|
||||
import classNames from 'classnames'
|
||||
import MenuFilter from '../MenuFilter/MenuFilter';
|
||||
import MenuNavigation from '../MenuNavigation/MenuNavigation';
|
||||
import IconHide from 'src/components/icons/IconHide';
|
||||
|
||||
interface Props{
|
||||
categories:{name:string,link:string}[],
|
||||
brands:{name:string,link:string}[],
|
||||
featured:{name:string,link:string}[],
|
||||
}
|
||||
|
||||
const MenuNavigationProductList = ({categories,brands,featured}:Props)=>{
|
||||
|
||||
const [dataSort,setDataSort] = useState({});
|
||||
const [isShow,setIsShow] = useState(true);
|
||||
|
||||
function handleValue(value:Object){
|
||||
setDataSort({...dataSort,...value});
|
||||
}
|
||||
function filter(){
|
||||
console.log(dataSort)
|
||||
}
|
||||
|
||||
function hideMenu(){
|
||||
if(isShow === true){
|
||||
setIsShow(false);
|
||||
}
|
||||
}
|
||||
return(
|
||||
<>
|
||||
<div className={s.menuNavigationProductListDesktop}>
|
||||
<MenuNavigation categories={categories} heading="Categories"/>
|
||||
<MenuNavigation categories={brands} heading="Brands"/>
|
||||
<MenuNavigation categories={featured} heading="Featured"/>
|
||||
</div>
|
||||
<div className={classNames({ [s.menuNavigationProductListMobile] :true,[s.isShow]: isShow})}>
|
||||
<div className={s.menuNavigationProductModal}>
|
||||
<div className={s.content}>
|
||||
<div className={s.head}>
|
||||
<h3>FILTER</h3>
|
||||
<div onClick={hideMenu}><IconHide/></div>
|
||||
</div>
|
||||
<MenuFilter categories={categories} heading="Categories" type="category" onChangeValue={handleValue}/>
|
||||
<MenuFilter categories={brands} heading="Brand" type="brand" onChangeValue={handleValue}/>
|
||||
<MenuFilter categories={featured} heading="Featured" type="featured" onChangeValue={handleValue}/>
|
||||
<MenuSort heading="SORT BY" type="sort" onChangeValue={handleValue}/>
|
||||
<ButtonCommon size="large" onClick={filter}>{LANGUAGE.BUTTON_LABEL.CONFIRM}</ButtonCommon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuNavigationProductList
|
@@ -0,0 +1,46 @@
|
||||
@import "../../../../styles/utilities";
|
||||
.menuSortWrapper{
|
||||
|
||||
@screen md {
|
||||
@apply hidden;
|
||||
}
|
||||
.menuSortHeading{
|
||||
@apply sub-headline font-bold ;
|
||||
color: var(--text-active);
|
||||
font-feature-settings: 'salt' on;
|
||||
margin: 0.8rem 0;
|
||||
}
|
||||
.menuSortList{
|
||||
box-sizing: border-box;
|
||||
&::after{
|
||||
@apply absolute;
|
||||
top: 110%;
|
||||
content: "";
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--border-line);
|
||||
}
|
||||
li{
|
||||
div{
|
||||
height: 4.8rem;
|
||||
line-height: 4.8rem;
|
||||
padding: 0 1.6rem;
|
||||
margin-right: 0.8rem;
|
||||
border-radius: 0.8rem;
|
||||
&.active {
|
||||
@apply font-bold relative;
|
||||
color:var(--text-active);
|
||||
background-color: var(--primary-lightest);
|
||||
&::after{
|
||||
@apply absolute;
|
||||
content:"";
|
||||
background-image: url('/assets/svg/check.svg');
|
||||
right: 1.6rem;
|
||||
top: calc(50% - 24px/2);
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { QUERY_KEY, ROUTE } from 'src/utils/constanst.utils';
|
||||
import s from './MenuSort.module.scss';
|
||||
|
||||
interface Props {
|
||||
children?: any,
|
||||
heading:string,
|
||||
type:string,
|
||||
onChangeValue?: (value: Object) => void
|
||||
}
|
||||
const SORT = [
|
||||
{
|
||||
name: 'By Name',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=by-name`,
|
||||
},
|
||||
{
|
||||
name: 'Price(High to Low)',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=high-to-low`,
|
||||
},
|
||||
{
|
||||
name: 'Price (Low to High)',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=low-to-high`,
|
||||
},
|
||||
{
|
||||
name: 'On Sale',
|
||||
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=on-sale`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const MenuSort = ({heading,type,onChangeValue}:Props)=> {
|
||||
const [active, setActive] = useState<string>('');
|
||||
|
||||
function handleClick(link:string){
|
||||
setActive(link);
|
||||
|
||||
if(active === link){
|
||||
setActive('');
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
let href = active?.split("=");
|
||||
const linkValue = href[1];
|
||||
|
||||
onChangeValue && onChangeValue({[type]:linkValue});
|
||||
},[active])
|
||||
|
||||
return (
|
||||
<section className={classNames(s.menuSortWrapper)}>
|
||||
<h2 className={classNames(s.menuSortHeading)}>{heading}</h2>
|
||||
<ul className={s.menuSortList}>
|
||||
{
|
||||
SORT.map(item => <li key={item.name}>
|
||||
<div onClick={()=> handleClick(item.link)} className={classNames({ [s.active]: item.link === active? true: false })}>
|
||||
{item.name}
|
||||
</div>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuSort
|
@@ -1,17 +1,12 @@
|
||||
@import '../../../../styles/utilities';
|
||||
|
||||
.formAuthen {
|
||||
@apply bg-white w-full;
|
||||
@apply bg-white w-full u-form;
|
||||
.inner {
|
||||
@screen md {
|
||||
max-width: 52rem;
|
||||
width: 60rem;
|
||||
margin: auto;
|
||||
}
|
||||
.body {
|
||||
> div {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.others {
|
||||
@apply font-bold text-center;
|
||||
margin-top: 4rem;
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import Link from 'next/link'
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import { Inputcommon, ButtonCommon } from 'src/components/common'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { ButtonCommon, Inputcommon, InputPassword } from 'src/components/common'
|
||||
import { ROUTE } from 'src/utils/constanst.utils'
|
||||
import SocialAuthen from '../SocialAuthen/SocialAuthen'
|
||||
import s from '../FormAuthen.module.scss'
|
||||
import styles from './FormLogin.module.scss'
|
||||
import classNames from 'classnames'
|
||||
import { CustomInputCommon } from 'src/utils/type.utils'
|
||||
import s from '../FormAuthen.module.scss'
|
||||
import SocialAuthen from '../SocialAuthen/SocialAuthen'
|
||||
import styles from './FormLogin.module.scss'
|
||||
|
||||
interface Props {
|
||||
isHide: boolean,
|
||||
@@ -23,14 +22,13 @@ const FormLogin = ({ onSwitch, isHide }: Props) => {
|
||||
}, [isHide])
|
||||
|
||||
return (
|
||||
<section className={classNames({
|
||||
[s.formAuthen]: true,
|
||||
// [styles.hide]: isHide
|
||||
})}>
|
||||
<section className={s.formAuthen}>
|
||||
<div className={s.inner}>
|
||||
<div className={s.body}>
|
||||
<Inputcommon placeholder='Email Address' type='email' ref={emailRef} />
|
||||
<Inputcommon placeholder='Password' type='password' />
|
||||
<Inputcommon placeholder='Email Address' type='email' ref={emailRef} />
|
||||
{/* <Inputcommon placeholder='Email Address' type='email' ref={emailRef}
|
||||
isShowIconSuccess={true} isIconSuffix={true} /> */}
|
||||
<InputPassword placeholder='Password'/>
|
||||
</div>
|
||||
<div className={styles.bottom}>
|
||||
<Link href={ROUTE.FORGOT_PASSWORD}>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { ButtonCommon, Inputcommon } from 'src/components/common'
|
||||
import { ButtonCommon, Inputcommon, InputPassword } from 'src/components/common'
|
||||
import s from '../FormAuthen.module.scss'
|
||||
import styles from './FormRegister.module.scss'
|
||||
import SocialAuthen from '../SocialAuthen/SocialAuthen'
|
||||
@@ -24,12 +24,11 @@ const FormRegister = ({ onSwitch, isHide }: Props) => {
|
||||
<section className={classNames({
|
||||
[s.formAuthen]: true,
|
||||
[styles.formRegister]: true,
|
||||
// [styles.hide]: isHide
|
||||
})}>
|
||||
<div className={s.inner}>
|
||||
<div className={s.body}>
|
||||
<Inputcommon placeholder='Email Address' type='email' ref={emailRef}/>
|
||||
<Inputcommon placeholder='Password' type='password' />
|
||||
<InputPassword placeholder='Password'/>
|
||||
<div className={styles.passwordNote}>
|
||||
Must contain 8 characters with at least 1 uppercase and 1 lowercase letter and either 1 number or 1 special character.
|
||||
</div>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
@apply flex justify-center items-center min-h-screen;
|
||||
.modal{
|
||||
@apply inline-block align-bottom bg-white relative;
|
||||
max-width: 60rem;
|
||||
max-width: 66.4rem;
|
||||
padding: 3.2rem;
|
||||
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.24);
|
||||
border-radius: 1.2rem;
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
.title{
|
||||
@apply font-heading heading-3;
|
||||
padding: 0 0.8rem 0 0.8rem;
|
||||
padding: 0 1.6rem 0 0.8rem;
|
||||
}
|
||||
.close{
|
||||
@apply absolute;
|
||||
|
@@ -2,7 +2,7 @@ import React, { useRef } from 'react'
|
||||
import { Close } from 'src/components/icons'
|
||||
import { useOnClickOutside } from 'src/utils/useClickOutSide'
|
||||
import s from './ModalCommon.module.scss'
|
||||
interface Props {
|
||||
export interface ModalCommonProps {
|
||||
onClose: () => void
|
||||
visible: boolean
|
||||
children: React.ReactNode
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
maxWidth?:string
|
||||
}
|
||||
|
||||
const ModalCommon = ({ onClose, visible, children, title="Modal",maxWidth }: Props) => {
|
||||
const ModalCommon = ({ onClose, visible, children, title="Modal",maxWidth }: ModalCommonProps) => {
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const clickOutSide = () => {
|
||||
onClose && onClose()
|
||||
|
@@ -0,0 +1,4 @@
|
||||
.footer{
|
||||
margin-top: 4rem;
|
||||
@apply flex justify-end items-center;
|
||||
}
|
34
src/components/common/ModalConfirm/ModalConfirm.tsx
Normal file
34
src/components/common/ModalConfirm/ModalConfirm.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import ButtonCommon from '../ButtonCommon/ButtonCommon'
|
||||
import ModalCommon, { ModalCommonProps } from '../ModalCommon/ModalCommon'
|
||||
import s from './ModalConfirm.module.scss'
|
||||
interface ModalConfirmProps extends ModalCommonProps {
|
||||
okText?: String
|
||||
cancelText?: String
|
||||
onOk?: () => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
const ModalConfirm = ({
|
||||
okText = 'Ok',
|
||||
cancelText = 'cancel',
|
||||
onOk,
|
||||
onCancel,
|
||||
children,
|
||||
title = 'Confirm',
|
||||
...props
|
||||
}: ModalConfirmProps) => {
|
||||
return (
|
||||
<ModalCommon {...props} title={title}>
|
||||
{children}
|
||||
<div className={s.footer}>
|
||||
<div className="mr-4">
|
||||
<ButtonCommon onClick={onCancel} type="light"> {cancelText}</ButtonCommon>
|
||||
</div>
|
||||
<ButtonCommon onClick={onOk}>{okText}</ButtonCommon>
|
||||
</div>
|
||||
</ModalCommon>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalConfirm
|
@@ -0,0 +1,19 @@
|
||||
@import "../../../styles/utilities";
|
||||
|
||||
.formUserInfo {
|
||||
@apply u-form;
|
||||
.inner {
|
||||
@screen md {
|
||||
width: 60rem;
|
||||
margin: auto;
|
||||
}
|
||||
.bottom {
|
||||
@apply grid grid-cols-2;
|
||||
margin-top: 4rem;
|
||||
grid-gap: 1.6rem;
|
||||
> button {
|
||||
@apply w-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
import classNames from 'classnames';
|
||||
import Link from 'next/link';
|
||||
import React, { useRef } from 'react';
|
||||
import { useModalCommon } from 'src/components/hooks/useModalCommon';
|
||||
import { CustomInputCommon } from 'src/utils/type.utils';
|
||||
import { Inputcommon } from '..';
|
||||
import ButtonCommon from '../ButtonCommon/ButtonCommon';
|
||||
import ModalCommon from '../ModalCommon/ModalCommon';
|
||||
import s from './ModalCreateUserInfo.module.scss';
|
||||
|
||||
// todo: remove
|
||||
interface Props {
|
||||
demoVisible: boolean,
|
||||
demoCloseModal: () => void,
|
||||
}
|
||||
|
||||
|
||||
const ModalCreateUserInfo = ({ demoVisible: visible, demoCloseModal: closeModal }: Props) => {
|
||||
// const { visible, closeModal } = useModalCommon({ initialValue: false})
|
||||
const firstInputRef = useRef<CustomInputCommon>(null)
|
||||
|
||||
return (
|
||||
<ModalCommon visible={visible} onClose={closeModal} title='Enter your Information'>
|
||||
<div className={s.formUserInfo}>
|
||||
<div className={s.inner}>
|
||||
<div className={s.body}>
|
||||
<Inputcommon placeholder='Street Address' ref={firstInputRef} />
|
||||
<Inputcommon placeholder='City' />
|
||||
<div className={s.line}>
|
||||
{/* todo: select, not input */}
|
||||
<Inputcommon placeholder='State' />
|
||||
<Inputcommon placeholder='Zip code' />
|
||||
</div>
|
||||
<Inputcommon placeholder='Phone (delivery contact)' />
|
||||
</div>
|
||||
<div className={s.bottom}>
|
||||
<ButtonCommon size='large' onClick={closeModal} type='light'>Skip</ButtonCommon>
|
||||
<ButtonCommon size='large'>Submit</ButtonCommon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalCommon>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalCreateUserInfo;
|
4
src/components/common/ModalInfo/ModalInfo.module.scss
Normal file
4
src/components/common/ModalInfo/ModalInfo.module.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.footer{
|
||||
margin-top: 4rem;
|
||||
@apply flex justify-end items-center;
|
||||
}
|
27
src/components/common/ModalInfo/ModalInfo.tsx
Normal file
27
src/components/common/ModalInfo/ModalInfo.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import ButtonCommon from '../ButtonCommon/ButtonCommon'
|
||||
import ModalCommon, { ModalCommonProps } from '../ModalCommon/ModalCommon'
|
||||
import s from './ModalInfo.module.scss'
|
||||
interface ModalInfoProps extends ModalCommonProps {
|
||||
okText?: String
|
||||
onOk?: () => void
|
||||
}
|
||||
|
||||
const ModalInfo = ({
|
||||
okText = 'Ok',
|
||||
onOk,
|
||||
children,
|
||||
title = 'Confirm',
|
||||
...props
|
||||
}: ModalInfoProps) => {
|
||||
return (
|
||||
<ModalCommon {...props} title={title}>
|
||||
{children}
|
||||
<div className={s.footer}>
|
||||
<ButtonCommon onClick={onOk}>{okText}</ButtonCommon>
|
||||
</div>
|
||||
</ModalCommon>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalInfo
|
@@ -0,0 +1,22 @@
|
||||
.warpper{
|
||||
.item{
|
||||
@apply inline-flex items-center justify-center cursor-pointer;
|
||||
background-color: var(--gray);
|
||||
margin: 0 0.4rem;
|
||||
width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
&.active{
|
||||
@apply border border-solid;
|
||||
border-color: var(--text-active);
|
||||
background-color: var(--white);
|
||||
}
|
||||
&.disable{
|
||||
svg{
|
||||
path{
|
||||
fill: var(--disabled)
|
||||
}
|
||||
}
|
||||
@apply text-disabled cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
75
src/components/common/PaginationCommon/PaginationCommon.tsx
Normal file
75
src/components/common/PaginationCommon/PaginationCommon.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import classNames from 'classnames'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { ArrowLeftSmall, ArrowRightSmall } from 'src/components/icons'
|
||||
import { DEFAULT_PAGE_SIZE } from 'src/utils/constanst.utils'
|
||||
import PaginationItem from './components/PaginationItem'
|
||||
import s from './PaginationCommon.module.scss'
|
||||
interface PaginationCommonProps {
|
||||
defaultCurrent?: number
|
||||
pageSize?: number
|
||||
total: number
|
||||
onChange?: (page: number, pageSize: number) => void
|
||||
}
|
||||
|
||||
const PaginationCommon = ({
|
||||
total,
|
||||
pageSize=DEFAULT_PAGE_SIZE,
|
||||
defaultCurrent,
|
||||
onChange,
|
||||
}: PaginationCommonProps) => {
|
||||
const [pageNum, setPageNum] = useState<number>(0)
|
||||
const [currentPage, setCurrentPage] = useState<number>(0)
|
||||
useEffect(() => {
|
||||
setPageNum(Math.ceil(total / pageSize))
|
||||
}, [total, pageSize])
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultCurrent) {
|
||||
setCurrentPage(defaultCurrent)
|
||||
}
|
||||
}, [defaultCurrent])
|
||||
|
||||
const onPageClick = (page: number) => {
|
||||
setCurrentPage(page)
|
||||
onChange && onChange(page, pageSize)
|
||||
}
|
||||
|
||||
const onPrevClick = () => {
|
||||
setCurrentPage(currentPage - 1 < 0 ? 0 : currentPage - 1)
|
||||
}
|
||||
|
||||
const onNextClick = () => {
|
||||
setCurrentPage((currentPage + 1) > (pageNum - 1) ? (pageNum - 1) : currentPage + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.warpper}>
|
||||
<div
|
||||
className={classNames(s.item, { [`${s.disable}`]: currentPage <= 0 })}
|
||||
onClick={onPrevClick}
|
||||
>
|
||||
<ArrowLeftSmall disable={currentPage <= 0} />
|
||||
</div>
|
||||
{[...Array(pageNum).keys()].map((index) => {
|
||||
return (
|
||||
<PaginationItem
|
||||
page={index}
|
||||
onClick={onPageClick}
|
||||
key={index}
|
||||
active={index === currentPage}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<div
|
||||
className={classNames(s.item, {
|
||||
[s.disable]: currentPage >= pageNum - 1,
|
||||
})}
|
||||
onClick={onNextClick}
|
||||
>
|
||||
<ArrowRightSmall disable={currentPage >= pageNum} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationCommon
|
@@ -0,0 +1,21 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import s from "../PaginationCommon.module.scss"
|
||||
interface PaginationItemProps {
|
||||
onClick:(page:number)=>void
|
||||
page:number
|
||||
active:boolean
|
||||
}
|
||||
|
||||
const PaginationItem = ({onClick, page, active}: PaginationItemProps) => {
|
||||
const onPageClick = () => {
|
||||
onClick && onClick(page)
|
||||
}
|
||||
return (
|
||||
<div onClick={onPageClick} className={classNames(s.item,{[`${s.active}`]:active})}>
|
||||
{page+1}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationItem
|
@@ -2,8 +2,12 @@
|
||||
max-width: 20.8rem;
|
||||
min-height: 31.8rem;
|
||||
padding: 1.2rem 1.2rem 0 1.2rem;
|
||||
margin: auto;
|
||||
margin-bottom: 1px;
|
||||
@apply flex flex-col justify-between;
|
||||
&.notSell {
|
||||
@apply justify-center;
|
||||
}
|
||||
.cardTop{
|
||||
@apply relative;
|
||||
height: 13.8rem;
|
||||
@@ -29,8 +33,6 @@
|
||||
.cardMidTop{
|
||||
.productname{
|
||||
font-weight: bold;
|
||||
line-height: 2.4rem;
|
||||
font-size: 1.6rem;
|
||||
color: var(--text-active);
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
@@ -57,7 +59,8 @@
|
||||
.cardBot{
|
||||
min-height: 4rem;
|
||||
@apply flex justify-between items-center;
|
||||
.cardButton{
|
||||
.cardIcon{
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ import ButtonIconBuy from '../ButtonIconBuy/ButtonIconBuy'
|
||||
import ItemWishList from '../ItemWishList/ItemWishList'
|
||||
import LabelCommon from '../LabelCommon/LabelCommon'
|
||||
import s from './ProductCard.module.scss'
|
||||
import ProductNotSell from './ProductNotSell/ProductNotSell'
|
||||
|
||||
export interface ProductCardProps extends ProductProps {
|
||||
buttonText?: string
|
||||
@@ -18,7 +19,15 @@ const ProductCard = ({
|
||||
price,
|
||||
buttonText = 'Buy Now',
|
||||
imageSrc,
|
||||
isNotSell,
|
||||
}: ProductCardProps) => {
|
||||
if (isNotSell) {
|
||||
return <div className={`${s.productCardWarpper} ${s.notSell}`}>
|
||||
<ProductNotSell name={name} imageSrc={imageSrc} />
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.productCardWarpper}>
|
||||
<div className={s.cardTop}>
|
||||
|
@@ -0,0 +1,27 @@
|
||||
@import "../../../../styles/utilities";
|
||||
|
||||
.imgWrap {
|
||||
img {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
@apply text-label cursor-default font-bold;
|
||||
}
|
||||
|
||||
.info {
|
||||
@apply flex justify-center items-center custom-border-radius bg-info-light text-center;
|
||||
padding: .8rem 1.6rem;
|
||||
margin-top: 1.6rem;
|
||||
color: var(--info);
|
||||
svg {
|
||||
@apply u-icon;
|
||||
path {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
margin-left: 0.8rem;
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { IconInfo } from 'src/components/icons';
|
||||
import ImgWithLink from '../../ImgWithLink/ImgWithLink';
|
||||
import s from './ProductNotSell.module.scss';
|
||||
|
||||
export interface Props {
|
||||
name: string,
|
||||
imageSrc: string,
|
||||
}
|
||||
|
||||
const ProductNotSell = ({ name, imageSrc }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className={s.imgWrap}>
|
||||
<ImgWithLink src={imageSrc} alt={name} />
|
||||
</div>
|
||||
<div className={s.name}>{name}</div>
|
||||
<div className={s.info}>
|
||||
<IconInfo />
|
||||
<div className={s.text}>
|
||||
Not Sell
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductNotSell;
|
11
src/components/common/ProductList/ProductList.module.scss
Normal file
11
src/components/common/ProductList/ProductList.module.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.wrapper{
|
||||
.list{
|
||||
// max-width: 109.4rem;
|
||||
@apply flex flex-wrap justify-around;
|
||||
}
|
||||
.pagination{
|
||||
padding-top: 4.8rem;
|
||||
// max-width: 109.4rem;
|
||||
@apply flex justify-center items-center ;
|
||||
}
|
||||
}
|
30
src/components/common/ProductList/ProductList.tsx
Normal file
30
src/components/common/ProductList/ProductList.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { useState } from 'react'
|
||||
import PaginationCommon from '../PaginationCommon/PaginationCommon'
|
||||
import ProductCard, { ProductCardProps } from '../ProductCard/ProductCard'
|
||||
import s from "./ProductList.module.scss"
|
||||
interface ProductListProps {
|
||||
data: ProductCardProps[]
|
||||
}
|
||||
|
||||
const ProductList = ({data}: ProductListProps) => {
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const onPageChange = (page:number) => {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
return (
|
||||
<div className={s.wrapper}>
|
||||
<div className={s.list}>
|
||||
{
|
||||
data.slice(currentPage*20,(currentPage+1)*20).map((product,index)=>{
|
||||
return <ProductCard {...product} key={index}/>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className={s.pagination}>
|
||||
<PaginationCommon total={data.length} pageSize={20} onChange={onPageChange}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductList
|
@@ -1,7 +1,7 @@
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react'
|
||||
import s from './QuanittyInput.module.scss'
|
||||
import classNames from 'classnames'
|
||||
import { Minus, Plus } from '@components/icons'
|
||||
import { IconMinus, IconPlus } from '../../icons'
|
||||
interface QuanittyInputProps
|
||||
extends Omit<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
@@ -64,7 +64,7 @@ const QuanittyInput = ({
|
||||
return (
|
||||
<div className={classNames(s.quanittyInputWarper, { [s[size]]: size })}>
|
||||
<div className={s.minusIcon} onClick={onMinusClick}>
|
||||
<Minus />
|
||||
<IconMinus />
|
||||
</div>
|
||||
<input
|
||||
{...props}
|
||||
@@ -74,7 +74,7 @@ const QuanittyInput = ({
|
||||
className={s.quanittyInput}
|
||||
/>
|
||||
<div className={s.plusIcon} onClick={onPlusClick}>
|
||||
<Plus />
|
||||
<IconPlus />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@@ -6,6 +6,9 @@
|
||||
width: 100%;
|
||||
max-height: 22rem;
|
||||
border-radius: 2.4rem;
|
||||
img {
|
||||
border-radius: 2.4rem;
|
||||
}
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
23
src/components/common/RecipeDetail/RecipeDetail.tsx
Normal file
23
src/components/common/RecipeDetail/RecipeDetail.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import { ProductCardProps } from '../ProductCard/ProductCard'
|
||||
import RecipeDetailInfo from './components/RecipeDetailInfo/RecipeDetailInfo'
|
||||
import RecipeIngredient from './components/RecipeIngredient/RecipeIngredient'
|
||||
import s from './RecipeDetail.module.scss'
|
||||
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any,
|
||||
ingredients: ProductCardProps[],
|
||||
}
|
||||
|
||||
const RecipeDetail = ({ ingredients }: Props) => {
|
||||
return (
|
||||
<section className={s.recipeDetail}>
|
||||
<RecipeDetailInfo />
|
||||
<RecipeIngredient data={ingredients} />
|
||||
</section >
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeDetail
|
@@ -0,0 +1,19 @@
|
||||
.recipeBriefInfo {
|
||||
@apply flex;
|
||||
.item {
|
||||
@apply flex;
|
||||
&:not(:last-child) {
|
||||
margin-right: 2.4rem;
|
||||
}
|
||||
svg {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
path {
|
||||
fill: var(--text-label);
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin-left: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import { IconLocation, IconPeople, IconTime } from 'src/components/icons'
|
||||
import s from './RecipeBriefInfo.module.scss'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any,
|
||||
}
|
||||
|
||||
const RecipeBriefInfo = ({ }: Props) => {
|
||||
return (
|
||||
<section className={s.recipeBriefInfo}>
|
||||
<div className={s.item}>
|
||||
<IconTime />
|
||||
<div className={s.content}>15 minutes</div>
|
||||
</div>
|
||||
<div className={s.item}>
|
||||
<IconPeople />
|
||||
<div className={s.content}>4 People</div>
|
||||
</div>
|
||||
<div className={s.item}>
|
||||
<IconLocation />
|
||||
<div className={s.content}>15 minutes</div>
|
||||
</div>
|
||||
</section >
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeBriefInfo
|
@@ -0,0 +1,61 @@
|
||||
@import "../../../../../styles/utilities";
|
||||
|
||||
.recipeDetailInfo {
|
||||
@apply spacing-horizontal;
|
||||
margin: 5.6rem auto;
|
||||
@screen md {
|
||||
@apply flex;
|
||||
}
|
||||
.img {
|
||||
width: fit-content;
|
||||
margin-top: 0;
|
||||
|
||||
@screen sm-only {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
@screen lg {
|
||||
max-width: 60rem;
|
||||
}
|
||||
img {
|
||||
@apply w-full;
|
||||
object-fit: contain;
|
||||
max-height: 64rem;
|
||||
border-radius: 2.4rem;
|
||||
@screen md {
|
||||
max-height: 90rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recipeInfo {
|
||||
@screen md {
|
||||
max-width: 39rem;
|
||||
margin-left: 4.8rem;
|
||||
}
|
||||
@screen lg {
|
||||
margin-left: 11.2rem;
|
||||
}
|
||||
.top {
|
||||
margin-bottom: 4.8rem;
|
||||
.name {
|
||||
@apply heading-1 font-heading;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
}
|
||||
.detail {
|
||||
.item {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
.heading {
|
||||
@apply heading-3 font-heading;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.content {
|
||||
list-style: disc;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
import React from 'react'
|
||||
import RecipeBriefInfo from '../RecipeBriefInfo/RecipeBriefInfo'
|
||||
import s from './RecipeDetailInfo.module.scss'
|
||||
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any
|
||||
}
|
||||
|
||||
const RecipeDetailInfo = ({ }: Props) => {
|
||||
return (
|
||||
<section className={s.recipeDetailInfo}>
|
||||
<div className={s.img}>
|
||||
<img src="https://user-images.githubusercontent.com/76729908/131634880-8ae1437b-d3f8-421e-a546-d5a4f9a28e5f.png" alt="Recipe" />
|
||||
</div>
|
||||
<div className={s.recipeInfo}>
|
||||
<div className={s.top}>
|
||||
<h1 className={s.name}>
|
||||
Crispy Fried Calamari
|
||||
</h1>
|
||||
<RecipeBriefInfo />
|
||||
</div>
|
||||
<div className={s.detail}>
|
||||
<div className={s.item}>
|
||||
<h3 className={s.heading}>Ingredients</h3>
|
||||
<ul className={s.content}>
|
||||
<li>Canola oil for frying</li>
|
||||
<li>1 pound clean squid bodies cut in 1/4 inch rings and dried with a paper towel</li>
|
||||
<li>2 cups flour</li>
|
||||
<li>1/2 teaspoon kosher salt</li>
|
||||
<li>1/2 teaspoon garlic powder</li>
|
||||
<li>1/8 teaspoon coarse ground black pepper</li>
|
||||
<li>1 lemon cut into wedges</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={s.item}>
|
||||
<h3 className={s.heading}>Preparation</h3>
|
||||
<ul className={s.content}>
|
||||
<li>1In a large pot or dutch oven add three inches of oil and bring to 350 degrees.</li>
|
||||
<li>Add the flour, salt, garlic powder and pepper to a large bowl and stir to combine.</li>
|
||||
<li>Toss the squid pieces in the flour then into the hot oil.</li>
|
||||
<li>Fry the squid for 1-2 minutes. You want the color to stay pale like in the pictures.</li>
|
||||
<li>Remove to a cookie sheet to drain (do not add paper towels as it will steam the calamari and make it soft.)</li>
|
||||
<li>Serve with lemon wedges.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={s.item}>
|
||||
<h3 className={s.heading}>Link</h3>
|
||||
<a href="https://dinnerthendessert.com/crispy-fried-calamari" target="_blank" rel="noopener noreferrer">https://dinnerthendessert.com/crispy-fried-calamari</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section >
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeDetailInfo
|
@@ -0,0 +1,21 @@
|
||||
@import "../../../../../styles/utilities";
|
||||
|
||||
.recipeIngredient {
|
||||
padding: 6rem 0;
|
||||
@screen md {
|
||||
padding: 5.6rem 0;
|
||||
}
|
||||
.top {
|
||||
@apply flex justify-between items-center spacing-horizontal;
|
||||
}
|
||||
.bottom {
|
||||
@apply flex justify-center items-center spacing-horizontal;
|
||||
margin-top: 4rem;
|
||||
button {
|
||||
width: 100%;
|
||||
@screen md {
|
||||
width: 39rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import ButtonCommon from 'src/components/common/ButtonCommon/ButtonCommon'
|
||||
import HeadingCommon from 'src/components/common/HeadingCommon/HeadingCommon'
|
||||
import { ProductCardProps } from 'src/components/common/ProductCard/ProductCard'
|
||||
import ProductCarousel from 'src/components/common/ProductCarousel/ProductCarousel'
|
||||
import ViewAllItem from 'src/components/common/ViewAllItem/ViewAllItem'
|
||||
import { ROUTE } from 'src/utils/constanst.utils'
|
||||
import s from './RecipeIngredient.module.scss'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any,
|
||||
data: ProductCardProps[],
|
||||
}
|
||||
|
||||
const RecipeIngredient = ({ data }: Props) => {
|
||||
return (
|
||||
<section className={s.recipeIngredient}>
|
||||
<div className={s.top}>
|
||||
<HeadingCommon>Ingredients</HeadingCommon>
|
||||
<div>
|
||||
<ViewAllItem link={ROUTE.PRODUCTS} />
|
||||
</div>
|
||||
</div>
|
||||
<ProductCarousel data={data} itemKey="recipe-ingredient" />
|
||||
<div className={s.bottom}>
|
||||
<ButtonCommon type='ghost' size='large'>Buy all</ButtonCommon>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeIngredient
|
@@ -0,0 +1,16 @@
|
||||
@import '../../../../styles/utilities';
|
||||
.blogCardWarpper {
|
||||
@apply spacing-horizontal;
|
||||
@screen xl {
|
||||
:global(.customArrow) {
|
||||
@screen lg {
|
||||
&:global(.leftArrow) {
|
||||
left: calc(-6.4rem - 2rem);
|
||||
}
|
||||
&:global(.rightArrow) {
|
||||
right: calc(-6.4rem - 2rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
import { TOptionsEvents } from 'keen-slider'
|
||||
import React from 'react'
|
||||
import CarouselCommon, {
|
||||
CarouselCommonProps,
|
||||
} from '../../CarouselCommon/CarouselCommon'
|
||||
import BlogCard, { BlogCardProps } from 'src/components/common/CardBlog/CardBlog'
|
||||
import s from "./BlogPostCarousel.module.scss"
|
||||
|
||||
interface BlogPostCarouselProps
|
||||
extends Omit<CarouselCommonProps<BlogCardProps>, 'Component'|"option"> {
|
||||
option?:TOptionsEvents
|
||||
}
|
||||
|
||||
const OPTION_DEFAULT: TOptionsEvents = {
|
||||
slidesPerView: 1.25,
|
||||
mode: 'free',
|
||||
spacing:24,
|
||||
breakpoints: {
|
||||
'(min-width: 640px)': {
|
||||
slidesPerView: 2,
|
||||
},
|
||||
'(min-width: 1024px)': {
|
||||
slidesPerView: 2.5,
|
||||
},
|
||||
'(min-width: 1440px)': {
|
||||
slidesPerView: 3,
|
||||
},
|
||||
'(min-width: 1536px)': {
|
||||
slidesPerView: 3.5,
|
||||
},
|
||||
},
|
||||
}
|
||||
const BlogPostCarousel = ({ option, data, ...props }: BlogPostCarouselProps) => {
|
||||
return (
|
||||
<div className={s.blogCardWarpper}>
|
||||
<CarouselCommon<BlogCardProps>
|
||||
data={data}
|
||||
Component={BlogCard}
|
||||
{...props}
|
||||
option={{ ...OPTION_DEFAULT, ...option }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPostCarousel
|
@@ -0,0 +1,19 @@
|
||||
@import '../../../styles/utilities';
|
||||
|
||||
.blogPostWarpper {
|
||||
&.cream{
|
||||
background-color: #F5F4F2;
|
||||
}
|
||||
padding-top: 5.6rem;
|
||||
padding-bottom: 5.2rem;
|
||||
@apply flex flex-col;
|
||||
.top {
|
||||
@apply spacing-horizontal flex w-full justify-between;
|
||||
padding-bottom: 3.2rem;
|
||||
@screen xl {
|
||||
.right {
|
||||
margin-right: 2.476rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
import image15 from '../../../../public/assets/images/image15.png'
|
||||
import image16 from '../../../../public/assets/images/image16.png'
|
||||
import image17 from '../../../../public/assets/images/image17.png'
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import { HeadingCommon, ViewAllItem } from 'src/components/common'
|
||||
import { BlogCardProps } from 'src/components/common/CardBlog/CardBlog'
|
||||
import BlogPostCarousel from './BlogPostCarousel/BlogPostCarousel'
|
||||
import s from './RelevantBlogPosts.module.scss'
|
||||
import { ROUTE } from 'src/utils/constanst.utils';
|
||||
|
||||
interface RelevantProps {
|
||||
data?: BlogCardProps[],
|
||||
itemKey?: string,
|
||||
title?: string,
|
||||
viewAllLink?: string,
|
||||
bgcolor?: "default" | "cream"
|
||||
}
|
||||
|
||||
const recipe:BlogCardProps[] = [
|
||||
{
|
||||
title: "Want to Lose Weight? Here are 10 DEBM Diet Guidelines for Beginners",
|
||||
slug: 'have-a-nice-lunch',
|
||||
description:"The DEBM diet stands for "+'"Delicious Happy Fun Diet"'+". This diet was popularized by Robert...",
|
||||
imageSrc: image15.src,
|
||||
},{
|
||||
title: "9 Ways to Make an Aloe Vera Mask at Home",
|
||||
slug: 'have-a-nice-lunch',
|
||||
description:"Aloe vera or aloe vera is a green plant, has thorns on the side of the skin with yellowish patches and...",
|
||||
imageSrc: image16.src,
|
||||
},{
|
||||
title: "Don't Buy Wrong, Here Are 7 Ways to Choose a Ripe Dragon Fruit",
|
||||
slug: 'have-a-nice-lunch',
|
||||
description:"Dragon fruit is a type of fruit that is a favorite for many people because of its delicious and fresh...",
|
||||
imageSrc: image17.src,
|
||||
},{
|
||||
title: "Want to Lose Weight? Here are 10 DEBM Diet Guidelines for Beginners",
|
||||
slug: 'have-a-nice-lunch',
|
||||
description:"The DEBM diet stands for "+'"Delicious Happy Fun Diet"'+". This diet was popularized by Robert...",
|
||||
imageSrc: image15.src,
|
||||
},{
|
||||
title: "9 Ways to Make an Aloe Vera Mask at Home",
|
||||
slug: 'have-a-nice-lunch',
|
||||
description:"Aloe vera or aloe vera is a green plant, has thorns on the side of the skin with yellowish patches and...",
|
||||
imageSrc: image16.src,
|
||||
},{
|
||||
title: "Don't Buy Wrong, Here Are 7 Ways to Choose a Ripe Dragon Fruit",
|
||||
slug: 'have-a-nice-lunch',
|
||||
description:"Dragon fruit is a type of fruit that is a favorite for many people because of its delicious and fresh...",
|
||||
imageSrc: image17.src,
|
||||
}]
|
||||
|
||||
const RelevantBlogPosts = ({ data = recipe, itemKey="detail-relevant", title="Relevant Blog Posts", bgcolor = "default" }: RelevantProps) => {
|
||||
return (
|
||||
<div className={classNames({
|
||||
[s.blogPostWarpper] : true,
|
||||
[s[bgcolor]] : bgcolor,
|
||||
})}
|
||||
>
|
||||
<div className={s.top}>
|
||||
<div className={s.left}>
|
||||
<HeadingCommon>{title}</HeadingCommon>
|
||||
</div>
|
||||
<div className={s.right}>
|
||||
<ViewAllItem link={ROUTE.BLOGS}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.bot}>
|
||||
<BlogPostCarousel data={data} itemKey={itemKey} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RelevantBlogPosts
|
@@ -1,15 +0,0 @@
|
||||
import React, { MutableRefObject } from 'react'
|
||||
|
||||
interface ScrollTargetProps {
|
||||
refScrollUp: MutableRefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const ScrollTarget = ({ refScrollUp } : ScrollTargetProps) => {
|
||||
|
||||
return (
|
||||
<div ref={refScrollUp}></div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default ScrollTarget
|
@@ -5,11 +5,10 @@ import s from './ScrollToTop.module.scss'
|
||||
import ArrowUp from '../../icons/IconArrowUp'
|
||||
|
||||
interface ScrollToTopProps {
|
||||
target: MutableRefObject<HTMLDivElement>;
|
||||
visibilityHeight?: number;
|
||||
}
|
||||
|
||||
const ScrollToTop = ({ target, visibilityHeight=450 }: ScrollToTopProps) => {
|
||||
const ScrollToTop = ({ visibilityHeight=450 }: ScrollToTopProps) => {
|
||||
|
||||
const [scrollPosition, setSrollPosition] = useState(0);
|
||||
const [showScrollToTop, setShowScrollToTop] = useState("hide");
|
||||
@@ -26,7 +25,7 @@ const ScrollToTop = ({ target, visibilityHeight=450 }: ScrollToTopProps) => {
|
||||
};
|
||||
|
||||
function handleScrollUp() {
|
||||
target.current.scrollIntoView({ behavior: "smooth" });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function addEventScroll() {
|
||||
@@ -34,7 +33,7 @@ const ScrollToTop = ({ target, visibilityHeight=450 }: ScrollToTopProps) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
addEventScroll()
|
||||
addEventScroll();
|
||||
});
|
||||
|
||||
return (
|
||||
|
@@ -1,32 +1,90 @@
|
||||
@import "../../../styles/utilities";
|
||||
|
||||
.select{
|
||||
@apply rounded-lg border-solid;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.2rem 1.6rem;
|
||||
&.base{
|
||||
width: 18.6rem;
|
||||
height: 4.8rem;
|
||||
.select {
|
||||
background-color: var(--white);
|
||||
.selectTrigger {
|
||||
svg {
|
||||
@apply transition-all duration-200;
|
||||
}
|
||||
}
|
||||
&.large{
|
||||
&.base {
|
||||
width: 20.6rem;
|
||||
.selectTrigger {
|
||||
width: 20.6rem;
|
||||
padding: 1.2rem 1.6rem;
|
||||
}
|
||||
}
|
||||
&.large {
|
||||
width: 34.25rem;
|
||||
height: 5.6rem;
|
||||
.selectTrigger {
|
||||
width: 34.25rem;
|
||||
padding: 1.6rem 1.6rem;
|
||||
}
|
||||
}
|
||||
&.default{
|
||||
@apply border;
|
||||
&.default {
|
||||
.selectTrigger {
|
||||
@apply border-solid border border-current;
|
||||
}
|
||||
}
|
||||
&.custom{
|
||||
@apply border-2;
|
||||
border-color: var(--border-line);
|
||||
color: var(--text-label);
|
||||
&.custom {
|
||||
.selectTrigger {
|
||||
@apply border-2;
|
||||
border-color: var(--border-line);
|
||||
color: var(--text-label);
|
||||
}
|
||||
}
|
||||
.option{
|
||||
&:hover{
|
||||
background-color: black;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
.hoverWrapper {
|
||||
@apply block;
|
||||
animation: SelectAnimation 0.2s ease-out;
|
||||
}
|
||||
.selectTrigger {
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.selectTrigger {
|
||||
@apply outline-none flex justify-between;
|
||||
color: var(--text-active);
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
.hoverWrapper {
|
||||
@apply hidden outline-none absolute z-10;
|
||||
padding-top: 0.6rem;
|
||||
.selectOptionWrapper {
|
||||
border-radius: 0.8rem;
|
||||
background-color: var(--white);
|
||||
padding: 0.4rem 0rem 0.4rem 0rem;
|
||||
&.base {
|
||||
width: 20.6rem;
|
||||
}
|
||||
&.large {
|
||||
width: 34.25rem;
|
||||
}
|
||||
&.default {
|
||||
@apply border-solid border border-current;
|
||||
}
|
||||
&.custom {
|
||||
@apply border-2;
|
||||
border-color: var(--border-line);
|
||||
color: var(--text-label);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes SelectAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(1.6rem);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
@@ -1,26 +1,57 @@
|
||||
import s from './SelectCommon.module.scss'
|
||||
import classNames from 'classnames'
|
||||
import { useState } from 'react'
|
||||
import { IconVectorDown } from 'src/components/icons'
|
||||
import SelectOption from './SelectOption/SelectOption'
|
||||
|
||||
interface Props {
|
||||
placeHolder? : string,
|
||||
placeholder? : string,
|
||||
size?: 'base' | 'large',
|
||||
type?: 'default' | 'custom',
|
||||
option: {name: string}[],
|
||||
option: {name: string, value: string}[],
|
||||
onChange?: (value: string) => void,
|
||||
}
|
||||
|
||||
const SelectCommon = ({ type = 'default', size = 'base', option, placeHolder }: Props) => {
|
||||
const SelectCommon = ({ type = 'default', size = 'base', option, placeholder, onChange}: Props) => {
|
||||
const [selectedName, setSelectedName] = useState(placeholder)
|
||||
const [selectedValue, setSelectedValue] = useState('')
|
||||
|
||||
const changeSelectedName = (item:string, value: string) => {
|
||||
setSelectedValue(value)
|
||||
setSelectedName(item)
|
||||
onChange && onChange(value)
|
||||
}
|
||||
return(
|
||||
<select className={classNames({
|
||||
[s.select] : true,
|
||||
[s[type]]: !!type,
|
||||
[s[size]]: !!size,
|
||||
})}
|
||||
>
|
||||
<option disabled selected hidden>{placeHolder}</option>
|
||||
{
|
||||
option.map(item => <option className={s.option} value={item.name}> {item.name} </option>)
|
||||
}
|
||||
</select>
|
||||
<>
|
||||
<div className={classNames({
|
||||
[s.select] : true,
|
||||
[s[size]] : !!size,
|
||||
[s[type]] : !!type,
|
||||
})}
|
||||
>
|
||||
<div className={classNames({
|
||||
[s.selectTrigger] : true,
|
||||
|
||||
})}
|
||||
>{selectedName}<IconVectorDown /></div>
|
||||
|
||||
<div className={s.hoverWrapper}>
|
||||
<div className={classNames({
|
||||
[s.selectOptionWrapper] : true,
|
||||
[s[type]] : !!type,
|
||||
[s[size]] : !!size,
|
||||
})}
|
||||
>
|
||||
{
|
||||
option.map(item =>
|
||||
<SelectOption key={item.value} itemName={item.name} value={item.value} onClick={changeSelectedName} size={size} selected={(selectedValue === item.value)} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,21 @@
|
||||
@import "../../../../styles/utilities";
|
||||
|
||||
.selectOption {
|
||||
@apply outline-none;
|
||||
background-color: var(--white);
|
||||
&.base{
|
||||
width: 20.4rem;
|
||||
padding: 0.8rem 1.6rem;
|
||||
}
|
||||
&.large{
|
||||
width: 33.75rem;
|
||||
padding: 0.8rem 1.6rem;
|
||||
}
|
||||
&:hover{
|
||||
background-color: var(--gray);
|
||||
color: var(--primary);
|
||||
}
|
||||
&.isChoose{
|
||||
background-color: var(--gray);
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import s from './SelectOption.module.scss'
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface Props{
|
||||
onClick: (name: string, value: string) => void,
|
||||
itemName: string,
|
||||
size: 'base' | 'large',
|
||||
value: string,
|
||||
selected?: boolean,
|
||||
}
|
||||
|
||||
const SelectOption = ({onClick, itemName, size, value, selected} : Props) => {
|
||||
const changeName = () => {
|
||||
onClick(itemName, value)
|
||||
}
|
||||
return(
|
||||
<div className={classNames({
|
||||
[s.selectOption] : true,
|
||||
[s[size]] : !!size,
|
||||
[s.isChoose] : selected ,
|
||||
})}
|
||||
onClick = {changeName}
|
||||
>{itemName}</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectOption
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user