Merge pull request #13 from KieIO/m2-datnguyen
Feat: HomeRecipe, ProductCollection
@ -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
@ -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
|
BIN
public/assets/images/image10.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/images/image11.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
public/assets/images/image12.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
public/assets/images/image13.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
public/assets/images/image14.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
public/assets/images/image5.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
public/assets/images/image6.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
public/assets/images/image7.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
public/assets/images/image8.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
public/assets/images/image9.png
Normal file
After Width: | Height: | Size: 15 KiB |
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
.navigation_wrapper{
|
|
||||||
@apply relative;
|
|
||||||
min-height: theme("caroucel.arrow-height") ;
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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> {
|
||||||
isArrow?:Boolean
|
data: T[]
|
||||||
itemKey:String
|
Component: React.ComponentType<T>
|
||||||
|
isArrow?: Boolean
|
||||||
|
isDot?: Boolean
|
||||||
|
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
|
||||||
{data?.map((props,index) => (
|
ref={sliderRef}
|
||||||
|
className={classNames('keen-slider', keenClassname, {
|
||||||
|
[s.isPadding]: isPadding,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
.custom_arrow{
|
.navigationWrapper{
|
||||||
width: 64px;
|
:global(.customArrow) {
|
||||||
height: 64px;
|
width: 64px;
|
||||||
&:focus{
|
height: 64px;
|
||||||
outline: none;
|
&:focus{
|
||||||
}
|
outline: none;
|
||||||
@apply absolute top-1/2 bg-background-arrow transform -translate-y-1/2 flex justify-center items-center transition duration-100;
|
}
|
||||||
&.left{
|
@apply absolute top-1/2 bg-background-arrow transform -translate-y-1/2 flex justify-center items-center transition duration-100;
|
||||||
@apply left-0;
|
&.leftArrow{
|
||||||
}
|
@apply left-0;
|
||||||
&.right{
|
}
|
||||||
@apply right-0;
|
&.rightArrow{
|
||||||
}
|
@apply right-0;
|
||||||
&.isDisabled{
|
}
|
||||||
@apply hidden ;
|
&.isDisabled{
|
||||||
|
@apply hidden ;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
21
src/components/common/CarouselCommon/CustomDot/CustomDot.tsx
Normal 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
|
@ -1,3 +1,5 @@
|
|||||||
.subtitle {
|
.subtitle {
|
||||||
|
font-size: var(--font-size);
|
||||||
|
line-height: var(--line-height);
|
||||||
margin-top: .4rem;
|
margin-top: .4rem;
|
||||||
}
|
}
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
@ -11,5 +11,7 @@
|
|||||||
}
|
}
|
||||||
&.center {
|
&.center {
|
||||||
@apply text-center;
|
@apply text-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
28
src/components/common/ModalCommon/ModalCommon.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
src/components/common/ModalCommon/ModalCommon.tsx
Normal 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
|
63
src/components/common/ProductCard/ProductCard.module.scss
Normal 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{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
60
src/components/common/ProductCard/ProductCard.tsx
Normal 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
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
src/components/common/ProductCarousel/ProductCarousel.tsx
Normal 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
|
31
src/components/common/RecipeCard/RecipeCard.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
23
src/components/common/RecipeCard/RecipeCard.tsx
Normal 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
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
src/components/common/RecipeCarousel/RecipeCarousel.tsx
Normal 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
|
@ -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'
|
||||||
|
22
src/components/icons/Close.tsx
Normal 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
|
@ -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'
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
145
src/components/modules/home/HomeCollection/HomeCollection.tsx
Normal 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
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
src/components/modules/home/HomeRecipe/HomeRecipe.tsx
Normal 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, let’s 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, let’s 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
|
@ -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'
|
||||||
|
@ -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;
|
||||||
|
@ -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
@ -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
|
30
src/utils/useClickOutSide.ts
Normal 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])
|
||||||
|
}
|
@ -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)',
|
||||||
|