Merge pull request #13 from KieIO/m2-datnguyen

Feat: HomeRecipe, ProductCollection
This commit is contained in:
lytrankieio123 2021-08-27 16:05:29 +07:00 committed by GitHub
commit 7893e98357
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 4771 additions and 3686 deletions

View File

@ -1,14 +1,17 @@
import { Layout } from 'src/components/common'; import { Layout } from 'src/components/common'
import { HomeBanner, HomeCategories, HomeCTA, HomeSubscribe, HomeVideo } from 'src/components/modules/home'; import { HomeBanner, HomeCollection, HomeCTA, HomeSubscribe, HomeVideo, HomeCategories } from 'src/components/modules/home';
import HomeRecipe from 'src/components/modules/home/HomeRecipe/HomeRecipe';
export default function Home() { export default function Home() {
return ( return (
<> <>
<HomeBanner /> <HomeBanner />
<HomeCollection/>
<HomeCategories/> <HomeCategories/>
<HomeVideo /> <HomeVideo />
<HomeCTA /> <HomeCTA />
<HomeRecipe />
<HomeSubscribe /> <HomeSubscribe />
</> </>
) )

119
pages/test.tsx Normal file
View File

@ -0,0 +1,119 @@
import { useState } from 'react'
import { ButtonCommon, Layout, ModalCommon, ProductCarousel } from 'src/components/common'
import { CollectionCarcousel } from 'src/components/modules/home'
import image5 from '../public/assets/images/image5.png'
import image6 from '../public/assets/images/image6.png'
import image7 from '../public/assets/images/image7.png'
import image8 from '../public/assets/images/image8.png'
const dataTest = [
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
{
name: 'Carrot',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image7.src,
},
{
name: 'Salad',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image8.src,
},
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
{
name: 'Carrot',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image7.src,
},
{
name: 'Salad',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image8.src,
},
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
]
export default function Test() {
const [visible, setVisible] = useState(false)
const onClose = () => {
setVisible(false)
}
const onOpen = () => {
setVisible(true)
}
return (
<>
<ButtonCommon onClick={onOpen}>open</ButtonCommon>
<ModalCommon visible={visible} onClose={onClose} >
<div className="">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur officiis dolorum ea incidunt. Sint, cum ullam. Labore vero quod itaque, officia magni molestias! Architecto deserunt soluta laborum commodi nesciunt delectus similique temporibus distinctio? Facere eaque minima enim modi magni, laudantium, animi mollitia beatae repudiandae maxime labore error nesciunt, nisi est?
</div>
</ModalCommon>
<ProductCarousel
data={dataTest}
itemKey="product-2"
isDot
/>
</>
)
}
Test.Layout = Layout

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -5,7 +5,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 1.2rem 3.2rem; padding: 0.8rem 3.2rem;
&:disabled { &:disabled {
filter: brightness(0.9); filter: brightness(0.9);
cursor: not-allowed; cursor: not-allowed;
@ -63,7 +63,7 @@
border: 1px solid var(--primary); border: 1px solid var(--primary);
&.loading { &.loading {
&::before { &::before {
border-top-color: var(--primary); border-top-color: var(--text-active);
} }
} }
} }

View File

@ -1,5 +0,0 @@
.navigation_wrapper{
@apply relative;
min-height: theme("caroucel.arrow-height") ;
}

View File

@ -0,0 +1,51 @@
@import '../../../styles/utilities';
.navigationWrapper {
@apply relative;
min-height: theme('caroucel.arrow-height');
.isPadding {
@apply spacing-horizontal;
}
: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;
&:global(.leftArrow) {
@apply left-0;
}
&:global(.rightArrow) {
@apply right-0;
}
&:global(.isDisabledArrow) {
@apply hidden;
}
}
:global {
.dots {
display: flex;
padding: 1rem 0;
justify-content: center;
}
.dot {
border: none;
width: 1rem;
height: 1rem;
background: #c5c5c5;
border-radius: 50%;
margin: 0 0.5rem;
padding: 0.5rem;
cursor: pointer;
}
.dot:focus {
outline: none;
}
.dot.active {
background: #000;
}
}
}

View File

@ -1,25 +1,65 @@
import { useKeenSlider } from 'keen-slider/react' import { useKeenSlider } from 'keen-slider/react'
import React from 'react' import React, { useEffect } from 'react'
import 'keen-slider/keen-slider.min.css' import 'keen-slider/keen-slider.min.css'
import { CustomCarouselArrow } from './CustomArrow/CustomCarouselArrow'; import { CustomCarouselArrow } from './CustomArrow/CustomCarouselArrow'
import s from "./CaroucelCommon.module.scss" import s from './CarouselCommon.module.scss'
interface CarouselCommonProps { import { TOptionsEvents } from 'keen-slider'
children?: React.ReactNode import classNames from 'classnames'
data?: any[] import CustomDot from './CustomDot/CustomDot'
Component: React.ComponentType<any> export interface CarouselCommonProps<T> {
data: T[]
Component: React.ComponentType<T>
isArrow?: Boolean isArrow?: Boolean
isDot?: Boolean
itemKey: String itemKey: String
option: TOptionsEvents
keenClassname?: string
isPadding?: boolean
} }
const CarouselCommon = ({ data, Component,itemKey }: CarouselCommonProps) => { const CarouselCommon = <T,>({
data,
Component,
itemKey,
keenClassname,
isPadding = false,
isArrow = true,
isDot = false,
option: { slideChanged,slidesPerView, ...sliderOption },
}: CarouselCommonProps<T>) => {
const [currentSlide, setCurrentSlide] = React.useState(0) const [currentSlide, setCurrentSlide] = React.useState(0)
const [dotActive, setDotActive] = React.useState<number>(0)
const [dotArr, setDotArr] = React.useState<number[]>([])
const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({ const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({
slidesPerView: 1, ...sliderOption,
initial: 0, slidesPerView,
slideChanged(s) { slideChanged(s) {
setCurrentSlide(s.details().relativeSlide) setCurrentSlide(s.details().relativeSlide)
}, },
afterChange(s) {
let dot = 0
dotArr.forEach((index)=>{
if(s.details().relativeSlide >= index){
dot = index
}
}) })
console.log(dot)
setDotActive(dot)
}
})
useEffect(() => {
if(isDot && slider){
let array:number[]
array = [...Array(Math.ceil(data.length/(Number(slider.details().slidesPerView)||1))).keys()].map((i)=>{
return (Number(slider.details().slidesPerView)||1)*i
})
console.log(array)
setDotArr(array)
}
}, [isDot,slider])
const handleRightArrowClick = () => { const handleRightArrowClick = () => {
slider.next() slider.next()
} }
@ -27,29 +67,46 @@ const CarouselCommon = ({ data, Component,itemKey }: CarouselCommonProps) => {
const handleLeftArrowClick = () => { const handleLeftArrowClick = () => {
slider.prev() slider.prev()
} }
const onDotClick = (index:number) => {
slider.moveToSlideRelative(index)
setDotActive(index)
}
return ( return (
<div className={s.navigation_wrapper}> <div className={s.navigationWrapper}>
<div ref={sliderRef} className="keen-slider"> <div
ref={sliderRef}
className={classNames('keen-slider', keenClassname, {
[s.isPadding]: isPadding,
})}
>
{data?.map((props, index) => ( {data?.map((props, index) => (
<div className="keen-slider__slide" key={`${itemKey}-${index}`}> <div className="keen-slider__slide" key={`${itemKey}-${index}`}>
<Component {...props} /> <Component {...props} />
</div> </div>
))} ))}
</div> </div>
{slider && ( {slider && isArrow && (
<> <>
<CustomCarouselArrow <CustomCarouselArrow
side="right" side="right"
onClick={handleRightArrowClick} onClick={handleRightArrowClick}
isDisabled={currentSlide === slider.details().size - 1}
/> />
<CustomCarouselArrow <CustomCarouselArrow
side="left" side="left"
onClick={handleLeftArrowClick} onClick={handleLeftArrowClick}
isDisabled={currentSlide === 0}
/> />
</> </>
)} )}
{slider && isDot && (
<div className="dots">
{dotArr.map((index) => {
return (
<CustomDot key={`dot-${index}`} index={index} dotActive={dotActive} onClick={onDotClick}/>
)
})}
</div>
)}
</div> </div>
) )
} }

View File

@ -1,17 +1,20 @@
.custom_arrow{ .navigationWrapper{
:global(.customArrow) {
width: 64px; width: 64px;
height: 64px; height: 64px;
&:focus{ &:focus{
outline: none; outline: none;
} }
@apply absolute top-1/2 bg-background-arrow transform -translate-y-1/2 flex justify-center items-center transition duration-100; @apply absolute top-1/2 bg-background-arrow transform -translate-y-1/2 flex justify-center items-center transition duration-100;
&.left{ &.leftArrow{
@apply left-0; @apply left-0;
} }
&.right{ &.rightArrow{
@apply right-0; @apply right-0;
} }
&.isDisabled{ &.isDisabled{
@apply hidden ; @apply hidden ;
} }
} }
}

View File

@ -2,11 +2,12 @@ import classNames from 'classnames'
import React from 'react' import React from 'react'
import ArrowLeft from 'src/components/icons/ArrowLeft' import ArrowLeft from 'src/components/icons/ArrowLeft'
import ArrowRight from 'src/components/icons/ArrowRight' import ArrowRight from 'src/components/icons/ArrowRight'
import s from "./CustomCarouselArrow.module.scss" import "./CustomCarouselArrow.module.scss"
interface CustomCarouselArrowProps interface CustomCarouselArrowProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> { extends React.ButtonHTMLAttributes<HTMLButtonElement> {
side: 'left' | 'right' side: 'left' | 'right'
isDisabled:Boolean isDisabled?:Boolean
} }
export const CustomCarouselArrow = ({ export const CustomCarouselArrow = ({
@ -16,7 +17,7 @@ export const CustomCarouselArrow = ({
return ( return (
<button <button
{...props} {...props}
className={classNames(`${s.custom_arrow}`, { [`${s[side]}`]: side,[`${s.isDisabled}`]:isDisabled })} className={classNames("customArrow", { [`${side}Arrow`]: side,"isDisabledArrow":isDisabled})}
> >
{side==='left'?(<ArrowLeft/>):(<ArrowRight/>)} {side==='left'?(<ArrowLeft/>):(<ArrowRight/>)}
</button> </button>

View File

@ -0,0 +1,21 @@
import React from 'react'
interface Props {
index: number
dotActive:number
onClick: (index: number) => void
}
const CustomDot = ({ index, onClick, dotActive }: Props) => {
const handleOnClick = () => {
onClick && onClick(index)
}
return (
<button
onClick={handleOnClick}
className={'dot' + (dotActive === index ? ' active' : '')}
/>
)
}
export default CustomDot

View File

@ -1,3 +1,5 @@
.subtitle { .subtitle {
font-size: var(--font-size);
line-height: var(--line-height);
margin-top: .4rem; margin-top: .4rem;
} }

View File

@ -2,7 +2,7 @@ import React from 'react'
import s from './CollectionHeading.module.scss' import s from './CollectionHeading.module.scss'
import HeadingCommon from '../HeadingCommon/HeadingCommon' import HeadingCommon from '../HeadingCommon/HeadingCommon'
interface CollectionHeadingProps { export interface CollectionHeadingProps {
type?: 'default' | 'highlight' | 'light'; type?: 'default' | 'highlight' | 'light';
title: string; title: string;
subtitle: string; subtitle: string;

View File

@ -0,0 +1,61 @@
@import '../../../styles/utilities';
.featuredProductCardWarpper{
width: 59.8rem;
height: 28.8rem;
padding: 2.4rem;
@apply bg-primary-light inline-flex justify-start items-center custom-border-radius ;
.left{
width: 24rem;
height: 24rem;
}
.right{
padding-left: 2.4rem;
min-width: 27rem;
max-width: 28.6rem;
min-height: 16.8rem;
@apply flex justify-between flex-col;
.rightTop{
min-height: 9.6rem;
@apply flex justify-between flex-col;
.title{
@apply font-bold;
font-size: 2rem;
line-height: 2.8rem;
letter-spacing: -0.01em;
color: var(--text-active);
}
.subTitle{
color: var(--text-base);
font-size: 1.6rem;
line-height: 2.4rem;
}
.priceWrapper{
@apply flex justify-start;
.price{
@apply font-bold;
font-size: 2rem;
line-height: 2.8rem;
letter-spacing: -0.01em;
color: var(--text-active);
}
.originPrice{
margin-left: 0.8rem;
font-size: 2rem;
line-height: 2.8rem;
color: var(--text-label);
text-decoration-line: line-through;
}
}
}
.buttonWarpper{
@apply flex;
.icon{
width: 5.6rem;
}
.button{
margin-left: 0.8rem;
width: 20.6rem;
}
}
}
}

View File

@ -0,0 +1,46 @@
import React from 'react'
import { FeaturedProductProps } from 'src/utils/types.utils'
import s from './FeaturedProductCard.module.scss'
import { LANGUAGE } from '../../../utils/language.utils'
import ButtonIconBuy from '../ButtonIconBuy/ButtonIconBuy'
import ButtonCommon from '../ButtonCommon/ButtonCommon'
interface FeaturedProductCardProps extends FeaturedProductProps {
buttonText?: string
}
const FeaturedProductCard = ({
imageSrc,
title,
subTitle,
price,
originPrice,
buttonText = LANGUAGE.BUTTON_LABEL.BUY_NOW,
}: FeaturedProductCardProps) => {
return (
<div className={s.featuredProductCardWarpper}>
<div className={s.left}>
<img src={imageSrc} alt="image" />
</div>
<div className={s.right}>
<div className={s.rightTop}>
<div className={s.title}>{title}</div>
<div className={s.subTitle}>{subTitle}</div>
<div className={s.priceWrapper}>
<div className={s.price}>{price} </div>
<div className={s.originPrice}>{originPrice} </div>
</div>
</div>
<div className={s.buttonWarpper}>
<div className={s.icon}>
<ButtonIconBuy />
</div>
<div className={s.button}>
<ButtonCommon>{buttonText}</ButtonCommon>
</div>
</div>
</div>
</div>
)
}
export default FeaturedProductCard

View File

@ -12,4 +12,6 @@
&.center { &.center {
@apply text-center; @apply text-center;
} }
} }

View File

@ -0,0 +1,28 @@
.background{
@apply fixed inset-0 overflow-y-auto;
background: rgba(20, 20, 20, 0.65);
z-index: 10000;
.warpper{
@apply flex justify-center items-center min-h-screen;
.modal{
@apply inline-block align-bottom bg-white relative;
max-width: 60rem;
padding: 3.2rem;
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.24);
border-radius: 1.2rem;
.title{
padding: 0 0.8rem 0 0.8rem;
font-size: 3.2rem;
line-height: 4rem;
}
.close{
@apply absolute;
&:hover{
cursor: pointer;
}
top:4.4rem;
right: 4.4rem;
}
}
}
}

View File

@ -0,0 +1,40 @@
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 {
onClose: () => void
visible: boolean
children: React.ReactNode
title?: string
maxWidth?:string
}
const ModalCommon = ({ onClose, visible, children, title="Modal",maxWidth }: Props) => {
const modalRef = useRef<HTMLDivElement>(null)
const clickOutSide = () => {
onClose && onClose()
}
useOnClickOutside(modalRef, clickOutSide)
return (
<>
{visible && (
<div className={s.background}>
<div className={s.warpper}>
<div className={s.modal} ref={modalRef} style={{maxWidth}}>
<div className={s.top}>
<div className={s.title}>{title}</div>
<div className={s.close} onClick={clickOutSide}>
<Close />
</div>
</div>
{children}
</div>
</div>
</div>
)}
</>
)
}
export default ModalCommon

View File

@ -0,0 +1,63 @@
.productCardWarpper{
max-width: 20.8rem;
min-height: 31.8rem;
padding: 1.2rem 1.2rem 0 1.2rem;
margin-bottom: 1px;
@apply flex flex-col justify-between;
.cardTop{
@apply relative;
height: 13.8rem;
width: 100%;
.productImage{
height: 100%;
width: 100%;
@apply flex justify-center items-center;
img{
@apply inline;
}
&:hover{
cursor: pointer;
}
}
.productLabel{
@apply absolute left-0 bottom-0;
}
}
.cardMid{
min-height: 10.4rem;
@apply flex flex-col justify-between;
.cardMidTop{
.productname{
font-weight: bold;
line-height: 2.4rem;
font-size: 1.6rem;
color: var(--text-active);
&:hover{
cursor: pointer;
}
}
.productWeight{
font-size: 1.2rem;
line-height: 2rem;
letter-spacing: 0.01em;
color: var(--text-base);
}
}
.cardMidBot{
padding-top: 0.8rem;
@apply flex justify-between items-center border-t border-solid border-line;
.productPrice{
@apply font-bold;
font-size: 2rem;
line-height: 2.8rem;
letter-spacing: -0.01em;
}
}
}
.cardBot{
min-height: 4rem;
@apply flex justify-between items-center;
.cardButton{
}
}
}

View File

@ -0,0 +1,60 @@
import Link from 'next/link'
import React from 'react'
import { ProductProps } from 'src/utils/types.utils'
import ButtonCommon from '../ButtonCommon/ButtonCommon'
import ButtonIconBuy from '../ButtonIconBuy/ButtonIconBuy'
import ItemWishList from '../ItemWishList/ItemWishList'
import LabelCommon from '../LabelCommon/LabelCommon'
import s from './ProductCard.module.scss'
export interface ProductCardProps extends ProductProps {
buttonText?: string
}
const ProductCard = ({
category,
name,
weight,
price,
buttonText = 'Buy Now',
imageSrc,
}: ProductCardProps) => {
return (
<div className={s.productCardWarpper}>
<div className={s.cardTop}>
<Link href="#">
<div className={s.productImage}>
<img src={imageSrc} alt="image" />
</div>
</Link>
<div className={s.productLabel}>
<LabelCommon shape="half">{category}</LabelCommon>
</div>
</div>
<div className={s.cardMid}>
<div className={s.cardMidTop}>
<Link href="#">
<div className={s.productname}>{name} </div>
</Link>
<div className={s.productWeight}>{weight}</div>
</div>
<div className={s.cardMidBot}>
<div className={s.productPrice}>{price}</div>
<div className={s.wishList}>
<ItemWishList />
</div>
</div>
</div>
<div className={s.cardBot}>
<div className={s.cardIcon}>
<ButtonIconBuy />
</div>
<div className={s.cardButton}>
<ButtonCommon type="light">{buttonText}</ButtonCommon>
</div>
</div>
</div>
)
}
export default ProductCard

View File

@ -0,0 +1,16 @@
@import '../../../styles/utilities';
.productCardWarpper {
@apply spacing-horizontal;
@screen xl {
:global(.customArrow) {
@screen lg {
&:global(.leftArrow) {
left: calc(-6.4rem - 2rem);
}
&:global(.rightArrow) {
right: calc(-6.4rem - 2rem);
}
}
}
}
}

View File

@ -0,0 +1,44 @@
import { TOptionsEvents } from 'keen-slider'
import React from 'react'
import CarouselCommon, {
CarouselCommonProps,
} from '../CarouselCommon/CarouselCommon'
import ProductCard, { ProductCardProps } from '../ProductCard/ProductCard'
import s from "./ProductCarousel.module.scss"
interface ProductCarouselProps
extends Omit<CarouselCommonProps<ProductCardProps>, 'Component'|"option"> {
option?:TOptionsEvents
}
const OPTION_DEFAULT: TOptionsEvents = {
slidesPerView: 2,
mode: 'free',
breakpoints: {
'(min-width: 640px)': {
slidesPerView: 3,
},
'(min-width: 768px)': {
slidesPerView: 4,
},
'(min-width: 1024px)': {
slidesPerView: 4.5,
},'(min-width: 1280px)': {
slidesPerView: 5.5,
},
},
}
const ProductCarousel = ({ option, data, ...props }: ProductCarouselProps) => {
return (
<div className={s.productCardWarpper}>
<CarouselCommon<ProductCardProps>
data={data}
Component={ProductCard}
{...props}
option={{ ...OPTION_DEFAULT, ...option }}
/>
</div>
)
}
export default ProductCarousel

View File

@ -0,0 +1,31 @@
.recipeCardWarpper{
max-width: 39.2rem;
min-height: 34rem;
@apply inline-flex flex-col justify-start;
.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 ;
display: -webkit-box;
-webkit-line-clamp: 3; /* number of lines to show */
-webkit-box-orient: vertical;
}
}

View File

@ -0,0 +1,23 @@
import Link from 'next/link'
import React from 'react'
import { RecipeProps } from 'src/utils/types.utils'
import s from './RecipeCard.module.scss'
export interface RecipeCardProps extends RecipeProps {}
const RecipeCard = ({ imageSrc, title, description }: RecipeCardProps) => {
return (
<div className={s.recipeCardWarpper}>
<Link href="#">
<div className={s.image}>
<img src={imageSrc} alt="image recipe" />
</div>
</Link>
<Link href="#">
<div className={s.title}>{title}</div>
</Link>
<div className={s.description}>{description}</div>
</div>
)
}
export default RecipeCard

View File

@ -0,0 +1,16 @@
@import '../../../styles/utilities';
.recipeCardWarpper {
@apply spacing-horizontal;
@screen xl {
:global(.customArrow) {
@screen lg {
&:global(.leftArrow) {
left: calc(-6.4rem - 2rem);
}
&:global(.rightArrow) {
right: calc(-6.4rem - 2rem);
}
}
}
}
}

View File

@ -0,0 +1,46 @@
import { TOptionsEvents } from 'keen-slider'
import React from 'react'
import CarouselCommon, {
CarouselCommonProps,
} from '../CarouselCommon/CarouselCommon'
import RecipeCard, { RecipeCardProps } from '../RecipeCard/RecipeCard'
import s from "./RecipeCarousel.module.scss"
interface RecipeCarouselProps
extends Omit<CarouselCommonProps<RecipeCardProps>, '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 RecipeCarousel = ({ option, data, ...props }: RecipeCarouselProps) => {
return (
<div className={s.recipeCardWarpper}>
<CarouselCommon<RecipeCardProps>
data={data}
Component={RecipeCard}
{...props}
option={{ ...OPTION_DEFAULT, ...option }}
/>
</div>
)
}
export default RecipeCarousel

View File

@ -3,6 +3,10 @@ export { default as Layout } from './Layout/Layout'
export { default as CarouselCommon } from './CarouselCommon/CarouselCommon' export { default as CarouselCommon } from './CarouselCommon/CarouselCommon'
export { default as QuanittyInput } from './QuanittyInput/QuanittyInput' export { default as QuanittyInput } from './QuanittyInput/QuanittyInput'
export { default as LabelCommon } from './LabelCommon/LabelCommon' export { default as LabelCommon } from './LabelCommon/LabelCommon'
export { default as ProductCard } from './ProductCard/ProductCard'
export { default as ProductCarousel } from './ProductCarousel/ProductCarousel'
export { default as FeaturedProductCard } from './FeaturedProductCard/FeaturedProductCard'
export { default as RecipeCard } from './RecipeCard/RecipeCard'
export { default as Head } from './Head/Head' export { default as Head } from './Head/Head'
export { default as ViewAllItem} from './ViewAllItem/ViewAllItem' export { default as ViewAllItem} from './ViewAllItem/ViewAllItem'
export { default as ItemWishList} from './ItemWishList/ItemWishList' export { default as ItemWishList} from './ItemWishList/ItemWishList'
@ -23,3 +27,4 @@ export { default as MenuDropdown} from './MenuDropdown/MenuDropdown'
export { default as NotiMessage} from './NotiMessage/NotiMessage' export { default as NotiMessage} from './NotiMessage/NotiMessage'
export { default as VideoPlayer} from './VideoPlayer/VideoPlayer' export { default as VideoPlayer} from './VideoPlayer/VideoPlayer'
export { default as SelectCommon} from './SelectCommon/SelectCommon' export { default as SelectCommon} from './SelectCommon/SelectCommon'
export { default as ModalCommon} from './ModalCommon/ModalCommon'

View File

@ -0,0 +1,22 @@
import React from 'react'
interface Props {}
const Close = (props: Props) => {
return (
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.4099 9.00019L16.7099 2.71019C16.8982 2.52188 17.004 2.26649 17.004 2.00019C17.004 1.73388 16.8982 1.47849 16.7099 1.29019C16.5216 1.10188 16.2662 0.996094 15.9999 0.996094C15.7336 0.996094 15.4782 1.10188 15.2899 1.29019L8.99994 7.59019L2.70994 1.29019C2.52164 1.10188 2.26624 0.996094 1.99994 0.996094C1.73364 0.996094 1.47824 1.10188 1.28994 1.29019C1.10164 1.47849 0.995847 1.73388 0.995847 2.00019C0.995847 2.26649 1.10164 2.52188 1.28994 2.71019L7.58994 9.00019L1.28994 15.2902C1.19621 15.3831 1.12182 15.4937 1.07105 15.6156C1.02028 15.7375 0.994141 15.8682 0.994141 16.0002C0.994141 16.1322 1.02028 16.2629 1.07105 16.3848C1.12182 16.5066 1.19621 16.6172 1.28994 16.7102C1.3829 16.8039 1.4935 16.8783 1.61536 16.9291C1.73722 16.9798 1.86793 17.006 1.99994 17.006C2.13195 17.006 2.26266 16.9798 2.38452 16.9291C2.50638 16.8783 2.61698 16.8039 2.70994 16.7102L8.99994 10.4102L15.2899 16.7102C15.3829 16.8039 15.4935 16.8783 15.6154 16.9291C15.7372 16.9798 15.8679 17.006 15.9999 17.006C16.132 17.006 16.2627 16.9798 16.3845 16.9291C16.5064 16.8783 16.617 16.8039 16.7099 16.7102C16.8037 16.6172 16.8781 16.5066 16.9288 16.3848C16.9796 16.2629 17.0057 16.1322 17.0057 16.0002C17.0057 15.8682 16.9796 15.7375 16.9288 15.6156C16.8781 15.4937 16.8037 15.3831 16.7099 15.2902L10.4099 9.00019Z"
fill="#141414"
/>
</svg>
)
}
export default Close

View File

@ -9,3 +9,6 @@ export { default as IconHome } from './IconHome'
export { default as IconShopping } from './IconShopping' export { default as IconShopping } from './IconShopping'
export { default as IconHeart } from './IconHeart' export { default as IconHeart } from './IconHeart'
export { default as IconVector } from './IconVector' export { default as IconVector } from './IconVector'
export { default as ArrowLeft } from './ArrowLeft'
export { default as ArrowRight } from './ArrowRight'
export { default as Close } from './Close'

View File

@ -0,0 +1,14 @@
@import '../../../../styles/utilities';
.colectionCarcoucelWarpper {
padding-top: 5.6rem;
padding-bottom: 6.5rem;
@apply flex flex-col;
.top {
@apply spacing-horizontal flex w-full justify-between;
@screen xl {
.right {
margin-right: 2.476rem;
}
}
}
}

View File

@ -0,0 +1,47 @@
import React from 'react'
import {
CollectionHeading,
ProductCarousel,
ViewAllItem,
} from 'src/components/common'
import { CollectionHeadingProps } from 'src/components/common/CollectionHeading/CollectionHeading'
import { ProductCardProps } from 'src/components/common/ProductCard/ProductCard'
import { QUERY_KEY, ROUTE } from 'src/utils/constanst.utils'
import s from './CollectionCarcousel.module.scss'
interface ColectionCarcouselProps extends CollectionHeadingProps {
data: ProductCardProps[]
itemKey: string
viewAllLink?: string,
category:string
}
const ColectionCarcousel = ({
data,
itemKey,
title,
subtitle,
type,
category
}: ColectionCarcouselProps) => {
return (
<div className={s.colectionCarcoucelWarpper}>
<div className={s.top}>
<div className={s.left}>
<CollectionHeading
type={type}
subtitle={subtitle}
title={title}
></CollectionHeading>
</div>
<div className={s.right}>
<ViewAllItem link={`${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=${category}`}/>
</div>
</div>
<div className={s.bot}>
<ProductCarousel data={data} itemKey={itemKey} />
</div>
</div>
)
}
export default ColectionCarcousel

View File

@ -0,0 +1,145 @@
import React from 'react'
import { CollectionCarcousel } from '..'
import image5 from '../../../../../public/assets/images/image5.png'
import image6 from '../../../../../public/assets/images/image6.png'
import image7 from '../../../../../public/assets/images/image7.png'
import image8 from '../../../../../public/assets/images/image8.png'
interface HomeCollectionProps {}
const dataTest = [
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
{
name: 'Carrot',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image7.src,
},
{
name: 'Salad',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image8.src,
},
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
{
name: 'Carrot',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image7.src,
},
{
name: 'Salad',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image8.src,
},
{
name: 'Tomato',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image5.src,
},
{
name: 'Cucumber',
weight: '250g',
category: 'VEGGIE',
price: 'Rp 27.500',
imageSrc: image6.src,
},
]
const HomeCollection = (props: HomeCollectionProps) => {
return (
<div className="w-full">
<CollectionCarcousel
type="highlight"
data={dataTest}
itemKey="product-1"
title="Fresh Products Today"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
itemKey="product-2"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
itemKey="product-3"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
itemKey="product-4"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
itemKey="product-5"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
<CollectionCarcousel
data={dataTest}
itemKey="product-6"
title="VEGGIE"
subtitle="Last call! Shop deep deals on 100+ bulk picks while you can."
category={"veggie"}
/>
</div>
)
}
export default HomeCollection

View File

@ -0,0 +1,35 @@
@import '../../../../styles/utilities';
.homeRecipeWarpper {
padding-top: 5.6rem;
padding-bottom: 6.5rem;
@apply flex flex-col;
.top {
@apply spacing-horizontal flex w-full justify-between;
@screen xl {
.right {
margin-right: 2.476rem;
}
}
}
.mid{
padding-top: 3.2rem;
padding-bottom: 3.2rem;
@apply flex justify-start spacing-horizontal;
.tab{
font-family: var(--font-heading);
padding: 1.6rem 1.6rem 0.8rem 1.6rem;
font-size: 2.4rem;
line-height: 2.8rem;
@screen md{
font-size: 3.2rem;
line-height: 4rem;
}
outline: none;
&.active{
@apply text-background custom-border-radius bg-primary;
}
}
}
}

View File

@ -0,0 +1,70 @@
import React from 'react'
import { HeadingCommon, ViewAllItem } from 'src/components/common'
import { RecipeCardProps } from 'src/components/common/RecipeCard/RecipeCard'
import RecipeCarousel from 'src/components/common/RecipeCarousel/RecipeCarousel'
import s from './HomeRecipe.module.scss'
import classNames from 'classnames';
import image13 from "../../../../../public/assets/images/image13.png"
import image14 from "../../../../../public/assets/images/image14.png"
import image12 from "../../../../../public/assets/images/image12.png"
interface HomeRecipeProps {
data?: RecipeCardProps[]
itemKey?: string
title?: string
viewAllLink?: string
}
const recipe:RecipeCardProps[] = [{
title: "Special Recipe of Vietnamese Phở",
description:"Alright, before we get to the actual recipe, lets chat for a sec about the ingredients. To make this pho soup recipe, you will need:",
imageSrc: image12.src
},{
title: "Original Recipe of Curry",
description:"Chicken curry is common to several countries including India, countries in Asia and the Caribbean. My favorite of them though is this aromatic Indian...",
imageSrc: image13.src
},{
title: "The Best Recipe of Beef Noodle Soup",
description:"The broth for Bun Bo Hue is prepared by slowly simmering various types of beef and pork bones (ox tail, beef shank, pork neck bones, pork feet,...",
imageSrc: image14.src
},{
title: "Special Recipe of Vietnamese Phở",
description:"Alright, before we get to the actual recipe, lets chat for a sec about the ingredients. To make this pho soup recipe, you will need:",
imageSrc: image12.src
},{
title: "Original Recipe of Curry",
description:"Chicken curry is common to several countries including India, countries in Asia and the Caribbean. My favorite of them though is this aromatic Indian...",
imageSrc: image13.src
},{
title: "The Best Recipe of Beef Noodle Soup",
description:"The broth for Bun Bo Hue is prepared by slowly simmering various types of beef and pork bones (ox tail, beef shank, pork neck bones, pork feet,...",
imageSrc: image14.src
}]
const HomeRecipe = ({ data =recipe, itemKey="home-recipe", title="Special Recipes" }: HomeRecipeProps) => {
return (
<div className={s.homeRecipeWarpper}>
<div className={s.top}>
<div className={s.left}>
<HeadingCommon>{title}</HeadingCommon>
</div>
<div className={s.right}>
<ViewAllItem link="#"/>
</div>
</div>
<div className={s.mid}>
<button className={classNames(s.tab,s.active)}>Noodle</button>
<button className={s.tab}>Curry</button>
<button className={s.tab}>Special Recipes</button>
</div>
<div className={s.bot}>
<RecipeCarousel data={data} itemKey={itemKey} />
</div>
</div>
)
}
export default HomeRecipe

View File

@ -1,7 +1,10 @@
export { default as HomeBanner } from './HomeBanner/HomeBanner' export { default as HomeBanner } from './HomeBanner/HomeBanner'
export { default as CollectionCarcousel } from './CollectionCarcousel/CollectionCarcousel'
export { default as HomeCategories } from './HomeCategories/HomeCategories' export { default as HomeCategories } from './HomeCategories/HomeCategories'
export { default as HomeCTA } from './HomeCTA/HomeCTA' export { default as HomeCTA } from './HomeCTA/HomeCTA'
export { default as HomeSubscribe } from './HomeSubscribe/HomeSubscribe' export { default as HomeSubscribe } from './HomeSubscribe/HomeSubscribe'
export { default as HomeVideo } from './HomeVideo/HomeVideo' export { default as HomeVideo } from './HomeVideo/HomeVideo'
export { default as HomeCollection } from './HomeCollection/HomeCollection'
export { default as HomeRecipe } from './HomeRecipe/HomeRecipe'
export { default as HomeFeature } from './HomeFeature/HomeFeature' export { default as HomeFeature } from './HomeFeature/HomeFeature'
export { default as HomeFeatureItem } from './HomeFeature/HomeFeatureItem' export { default as HomeFeatureItem } from './HomeFeature/components/HomeFeatureItem/HomeFeatureItem'

View File

@ -39,9 +39,6 @@
--font-size: 1.6rem; --font-size: 1.6rem;
--line-height: 2.4rem; --line-height: 2.4rem;
// --font-size: 16px;
// --line-height: 24px;
--font-sans: "Nunito", -apple-system, system-ui, BlinkMacSystemFont, "Helvetica Neue", "Helvetica", sans-serif; --font-sans: "Nunito", -apple-system, system-ui, BlinkMacSystemFont, "Helvetica Neue", "Helvetica", sans-serif;
--font-heading: "Righteous", -apple-system, system-ui, BlinkMacSystemFont, "Helvetica Neue", "Helvetica", sans-serif; --font-heading: "Righteous", -apple-system, system-ui, BlinkMacSystemFont, "Helvetica Neue", "Helvetica", sans-serif;
--font-logo: "Poppins", -apple-system, system-ui, BlinkMacSystemFont, "Helvetica Neue", "Helvetica", sans-serif; --font-logo: "Poppins", -apple-system, system-ui, BlinkMacSystemFont, "Helvetica Neue", "Helvetica", sans-serif;

View File

@ -88,7 +88,7 @@
padding-left: 6.4rem; padding-left: 6.4rem;
padding-right: 6.4rem; padding-right: 6.4rem;
} }
@screen md { @screen lg {
padding-left: 11.2rem; padding-left: 11.2rem;
padding-right: 11.2rem; padding-right: 11.2rem;
} }

23
src/utils/types.utils.ts Normal file
View File

@ -0,0 +1,23 @@
export interface ProductProps {
category: string
name: string
weight: string
price: string
imageSrc: string
}
export interface FeaturedProductProps {
title: string
subTitle: string
originPrice: string
price: string
imageSrc: string
}
export interface RecipeProps {
title: string
description:string
imageSrc: string
}
export type MouseAndTouchEvent = MouseEvent | TouchEvent

View File

@ -0,0 +1,30 @@
import { RefObject, useEffect } from 'react'
import { MouseAndTouchEvent } from './types.utils'
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
ref: RefObject<T>,
callback: (event: MouseAndTouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseAndTouchEvent) => {
const el = ref?.current
// Do nothing if clicking ref's element or descendent elements
if (!el || el.contains(event.target as Node)) {
return
}
callback(event)
}
document.addEventListener(`mousedown`, listener)
document.addEventListener(`touchstart`, listener)
return () => {
document.removeEventListener(`mousedown`, listener)
document.removeEventListener(`touchstart`, listener)
}
// Reload only if ref or handler changes
}, [ref, callback])
}

View File

@ -45,6 +45,14 @@ module.exports = {
'negative-border-line': 'var(--negative-border-line)', 'negative-border-line': 'var(--negative-border-line)',
'negative-light': 'var(--negative-light)', 'negative-light': 'var(--negative-light)',
'line': 'var(--border-line)',
'background': 'var(--background)',
'white': 'var(--white)',
'background-arrow':'var(--background-arrow)',
'disabled': 'var(--text-disabled)',
line: 'var(--border-line)', line: 'var(--border-line)',
background: 'var(--background)', background: 'var(--background)',
white: 'var(--white)', white: 'var(--white)',

7201
yarn.lock

File diff suppressed because it is too large Load Diff