Resolved merge conflict by incorporating both suggestions.

This commit is contained in:
unknown 2021-08-30 09:43:19 +07:00
commit c7291697c3
181 changed files with 5664 additions and 11049 deletions

@ -0,0 +1 @@
Subproject commit 3c7aa8e862bfd8d44719be44c6c0a31ab01524a3

9962
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,7 @@
"react-dom": "^17.0.2",
"react-fast-marquee": "^1.1.4",
"react-merge-refs": "^1.1.0",
"react-player": "^2.9.0",
"react-use-measure": "^2.0.4",
"sass": "^1.38.0",
"swell-js": "^4.0.0-next.0",
@ -74,11 +75,6 @@
"prettier": "^2.3.0",
"typescript": "4.3.4"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": [
"prettier --write",

View File

@ -1,36 +1,27 @@
import { ButtonCommon, Layout, ViewAllItem, ItemWishList, Logo, SelectCommon } from 'src/components/common'
import { IconBuy } from 'src/components/icons'
import { ButonType, ButtonSize, } from 'src/utils/constanst.utils'
import { Layout } from 'src/components/common'
import { HomeBanner, HomeCollection, HomeCTA, HomeSubscribe, HomeVideo, HomeCategories, HomeFeature, HomeRecipe } from 'src/components/modules/home';
const OPTION_SORT = [
{
name: 'By Name',
},
{
name: 'Price (Hight to Low)',
},
{
name: 'On Sale',
}
]
const OPTION_STATES = [
{
name: 'Việt Nam'
},
{
name: 'US'
},
]
export default function Home() {
return (
<>
<<<<<<< HEAD
<div>This is home page</div>
<ViewAllItem link="/all"/>
<ItemWishList />
<Logo />
<SelectCommon option={OPTION_SORT}>Sort by</SelectCommon>
<SelectCommon option={OPTION_STATES} size={"large"} type={"custom"}>States</SelectCommon>
=======
<HomeBanner />
<HomeFeature />
<HomeCategories />
<HomeCollection />
<HomeVideo />
<HomeCTA />
<HomeRecipe />
<HomeSubscribe />
>>>>>>> 08cd011b5ebb28ba4205d167dc07c81e3b9c3072
</>
)
}

146
pages/test.tsx Normal file
View File

@ -0,0 +1,146 @@
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
option={{
slidesPerView: 1,
breakpoints: {
'(min-width: 640px)': {
slidesPerView: 3,
},
'(min-width: 768px)': {
slidesPerView: 4,
},
'(min-width: 1024px)': {
slidesPerView: 4.5,
},
'(min-width: 1280px)': {
slidesPerView: 5.5,
},
},
}}
/>
</>
)
}
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

@ -0,0 +1 @@
<svg width="10" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.194 1.006a.625.625 0 00-.888 0L3.65 5.67 1.694 3.706a.639.639 0 00-.888.919l2.4 2.4a.625.625 0 00.888 0l5.1-5.1a.625.625 0 000-.919z" fill="#FBFBFB"/></svg>

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

BIN
src/assets/imgs/gpay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

BIN
src/assets/imgs/visa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

View File

@ -0,0 +1,17 @@
.authorWarper{
@apply flex flex-row items-center;
.authorImage{
width:3.2rem;
height:3.2rem;
border-radius:3.2rem;
}
.authorName{
margin-left:1rem;
color:var(--text-label);
font-family: var(--font-sans);
font-size: 1.2rem;
line-height: 2rem;
font-feature-settings: 'salt' on;
}
}

View File

@ -0,0 +1,21 @@
import React from 'react';
import s from './Author.module.scss';
import classNames from 'classnames';
import Image from "next/image";
interface Props {
image:any,
name: string
}
const Author = ({image,name}:Props) =>{
return (
<div className={classNames(s.authorWarper)}>
<Image className={classNames(s.authorImage)} src={image} alt=""/>
<div className={classNames(s.authorName)}>{name}</div>
</div>
)
}
export default Author;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,52 @@
@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;
}
}
}
}

View 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 './Banner.module.scss'
interface Props {
imgLink: string,
title: string,
subtitle: string,
buttonLabel?: string,
linkButton?: string,
size?: 'small' | 'large',
}
const Banner = memo(({ imgLink, title, subtitle, buttonLabel = LANGUAGE.BUTTON_LABEL.SHOP_NOW, linkButton = ROUTE.HOME, size = 'large' }: 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>
)
})
export default Banner

View File

@ -5,12 +5,29 @@
display: flex;
justify-content: center;
align-items: center;
padding: 1.6rem 3.2rem;
padding: 1rem 2rem;
@screen md {
padding: 0.8rem 3.2rem;
}
&:disabled {
filter: brightness(0.9);
cursor: not-allowed;
color: var(--disabled);
}
&:hover {
@apply shadow-md;
&:not(:disabled) {
filter: brightness(1.05);
}
}
&:focus {
outline: none;
filter: brightness(1.05);
}
&:focus-visible {
outline: 2px solid var(--text-active);
}
&.loading {
&::before {
content: "";
@ -24,20 +41,6 @@
margin-right: 0.8rem;
}
}
&:hover {
@apply shadow-md;
&:not(:disabled) {
filter: brightness(1.05);
}
}
&:focus {
outline: none;
filter: brightness(1.05);
}
&:focus-visible {
outline: 2px solid var(--text-active);
}
&.light {
@apply text-base bg-white;
@ -48,8 +51,44 @@
}
}
}
&.lightBorderNone {
@apply bg-white text-primary;
&.loading {
&::before {
border-top-color: var(--primary);
}
}
}
&.ghost {
@apply bg-white text-primary;
border: 1px solid var(--primary);
&.loading {
&::before {
border-top-color: var(--text-active);
}
}
}
&.onlyIcon {
padding: 0.8rem;
.icon {
margin: 0;
}
}
&.large {
padding: 3.2rem 4.8rem;
padding: 1rem 1.5rem;
&.onlyIcon {
padding: 1rem;
}
@screen md {
padding: 1.6rem 4.8rem;
&.onlyIcon {
padding: 1.6rem;
}
}
&.loading {
&::before {
width: 2.4rem;

View File

@ -1,21 +1,20 @@
import classNames from 'classnames'
import React, { memo } from 'react'
import { ButonType, ButtonSize } from 'src/utils/constanst.utils'
import s from './ButtonCommon.module.scss'
interface Props {
children?: React.ReactNode,
type?: ButonType,
size?: ButtonSize,
icon?: any,
type?: 'primary' | 'light' | 'ghost' | 'lightBorderNone',
size?: 'default' | 'large',
icon?: React.ReactNode,
isIconSuffix?: boolean,
loading?: boolean,
disabled?: boolean,
onClick?: () => void,
}
const ButtonCommon = memo(({ type = ButonType.primary, size = ButtonSize.default,
icon, loading, disabled, isIconSuffix, children, onClick }: Props) => {
const ButtonCommon = memo(({ type = 'primary', size = 'default', loading = false, isIconSuffix = false,
icon, disabled, children, onClick }: Props) => {
return (
<button className={classNames({
[s.buttonCommon]: true,
@ -23,6 +22,7 @@ const ButtonCommon = memo(({ type = ButonType.primary, size = ButtonSize.default
[s[size]]: !!size,
[s.loading]: loading,
[s.preserve]: isIconSuffix,
[s.onlyIcon]: icon && !children,
})}
disabled={disabled}
onClick={onClick}

View File

@ -0,0 +1,26 @@
import React, { memo } from 'react'
import { IconBuy } from 'src/components/icons'
import ButtonCommon from '../ButtonCommon/ButtonCommon'
interface Props {
type?: 'primary' | 'light' | 'ghost',
size?: 'default' | 'large',
loading?: boolean,
disabled?: boolean,
onClick?: () => void,
}
const ButtonIconBuy = memo(({ type = 'light', size = 'default', loading = false, disabled, onClick }: Props) => {
return (
<ButtonCommon
type={type}
size={size}
loading={loading}
disabled={disabled}
onClick={onClick}
icon={<IconBuy />}
/>
)
})
export default ButtonIconBuy

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

@ -0,0 +1,103 @@
import { useKeenSlider } from 'keen-slider/react'
import React, { useEffect } from 'react'
import 'keen-slider/keen-slider.min.css'
import { CustomCarouselArrow } from './CustomArrow/CustomCarouselArrow'
import s from './CarouselCommon.module.scss'
import { TOptionsEvents } from 'keen-slider'
import classNames from 'classnames'
import CustomDot from './CustomDot/CustomDot'
export interface CarouselCommonProps<T> {
data: T[]
Component: React.ComponentType<T>
isArrow?: Boolean
isDot?: Boolean
itemKey: String
option: TOptionsEvents
keenClassname?: string
isPadding?: boolean
}
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 [dotActive, setDotActive] = React.useState<number>(0)
const [dotArr, setDotArr] = React.useState<number[]>([])
const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({
...sliderOption,
slidesPerView,
slideChanged(s) {
setCurrentSlide(s.details().relativeSlide)
},
})
useEffect(() => {
if(isDot && slider && data){
let array:number[]
let number = data.length - Math.floor(slider.details().slidesPerView - 1)
if(number<1){
number = 1
}
array = [...Array(number).keys()]
setDotArr(array)
}
}, [isDot,slider,data])
const handleRightArrowClick = () => {
slider.next()
}
const handleLeftArrowClick = () => {
slider.prev()
}
const onDotClick = (index:number) => {
slider.moveToSlideRelative(index)
}
return (
<div className={s.navigationWrapper}>
<div
ref={sliderRef}
className={classNames('keen-slider', keenClassname, {
[s.isPadding]: isPadding,
})}
>
{data?.map((props, index) => (
<div className="keen-slider__slide" key={`${itemKey}-${index}`}>
<Component {...props} />
</div>
))}
</div>
{slider && isArrow && (
<>
<CustomCarouselArrow
side="right"
onClick={handleRightArrowClick}
/>
<CustomCarouselArrow
side="left"
onClick={handleLeftArrowClick}
/>
</>
)}
{slider && isDot && (
<div className="dots">
{dotArr.map((index) => {
return (
<CustomDot key={`dot-${index}`} index={index} dotActive={currentSlide} onClick={onDotClick}/>
)
})}
</div>
)}
</div>
)
}
export default CarouselCommon

View File

@ -0,0 +1,20 @@
.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 ;
}
}
}

View File

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

@ -0,0 +1,71 @@
@import '../../../styles/utilities';
.checkboxCommonWarper{
@apply flex flex-col;
.checkboxItem{
display: block;
position: relative;
cursor: pointer;
border-radius: 0.4rem;
width:50%;
&:hover .checkboxInput ~ .checkMark {
background-color: #ccc;
}
.checkboxInput{
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
&:checked ~ .checkMark:after {
display: block;
}
}
.checkMark {
border-radius: 0.4rem;
position: absolute;
top: 0;
left: 0;
height: 2rem;
width: 2rem;
background-color:var(--positive);
&:after {
left: 25.74%;
bottom: 34.6%;
width: 0.878rem;
height: 0.7rem;
color:white;
content: "";
background-image:url('/assets/svg/checkmark.svg');
background-size:cover;
background-repeat: no-repeat;
position: absolute;
display: none;
}
}
&~ .checkMark{
background-color: #ccc;
}
&:checked ~ .checkMark {
background-color: #2196F3;
}
&:checked ~ .checkMark:after {
display: block;
}
&:hover .checkboxInput ~ .checkMark {
background-color: #ccc;
}
}
.checkboxText{
margin-left:3rem;
font-size:1.2rem;
line-height: 2rem;
font-family: var(--font-sans);
color:var(--text-base);
letter-spacing: 0.01em;
}
}

View File

@ -0,0 +1,40 @@
import React,{ChangeEvent,useState,useEffect} from 'react';
import s from './CheckboxCommon.module.scss';
import classNames from 'classnames';
interface CheckboxProps extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'onChange'
>{
onChange?: (value: boolean) => void,
defaultChecked?: boolean
}
const CheckboxCommon = ({onChange,defaultChecked = true,...props}: CheckboxProps) =>{
const [value, setValue] = useState<boolean>(true);
useEffect(()=>{
onChange && onChange(value)
},[value])
const onValueChange = (e: ChangeEvent<HTMLInputElement>)=>{
let value =e.target.checked;
setValue(value);
}
return (
<div className={classNames(s.checkboxCommonWarper)}>
<label className={classNames(s.checkboxItem)}>
<input id="check" defaultChecked={defaultChecked} className={s.checkboxInput} type="checkbox" onChange={onValueChange}/>
<span className={s.checkMark}></span>
</label>
<div className={classNames(s.checkboxText)}>
<label htmlFor="check"> Billing address is same as shipping </label>
</div>
</div>
)
}
export default CheckboxCommon;

View File

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

View File

@ -0,0 +1,22 @@
import React from 'react'
import s from './CollectionHeading.module.scss'
import HeadingCommon from '../HeadingCommon/HeadingCommon'
export interface CollectionHeadingProps {
type?: 'default' | 'highlight' | 'light';
title: string;
subtitle: string;
}
const CollectionHeading = ({ type = 'default', title, subtitle }: CollectionHeadingProps) => {
return (
<section>
<HeadingCommon type={type}>{title}</HeadingCommon>
<div className={s.subtitle}>{subtitle}</div>
</section>
)
}
export default CollectionHeading

View File

@ -0,0 +1,9 @@
.dateTime{
color:var(--text-label);
text-transform: uppercase;
font-size: 1.2rem;
line-height: 2rem;
letter-spacing: 0.01em;
font-feature-settings: 'salt' on;
font-family: var(--font-sans);
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import s from './DateTime.module.scss';
import classNames from 'classnames';
interface Props {
date:string,
}
const DateTime = ({date}:Props) =>{
return (
<div className={classNames(s.dateTime)}>{date}</div>
)
}
export default DateTime;

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

@ -0,0 +1,31 @@
@import "../../../styles/utilities";
.footer {
@apply spacing-horizontal;
padding-top: 4rem;
padding-bottom: 2rem;
margin-bottom: 10rem;
.footerMenu {
padding-bottom: 4rem;
}
.menu {
@apply flex flex-wrap;
}
@screen md {
margin-bottom: 0;
padding-bottom: 4rem;
padding-left: 3.2rem;
padding-right: 3.2rem;
.footerMenu {
@apply flex;
padding-bottom: 8rem;
.menu {
@apply flex-nowrap justify-between;
}
}
}
@screen lg {
@apply spacing-horizontal;
}
}

View File

@ -0,0 +1,85 @@
import React from 'react'
import { ROUTE } from 'src/utils/constanst.utils'
import FooterColumn from './components/FooterColumn/FooterColumn'
import FooterSocial from './components/FooterSocial/FooterSocial'
import s from './Footer.module.scss'
const FOOTER_COLUMNS = [
{
title: 'Company',
items: [
{
name: 'All Product',
link: ROUTE.PRODUCTS,
},
{
name: 'About Us',
link: ROUTE.ABOUT,
},
{
name: 'Bussiness',
link: ROUTE.BUSSINESS,
}
]
},
{
title: 'Resources',
items: [
{
name: 'Contact Us',
link: ROUTE.CONTACT,
},
{
name: 'FAQ',
link: ROUTE.FAQ,
},
{
name: 'Customer Service',
link: ROUTE.CUSTOMER_SERVICE,
},
]
},
{
title: 'Quick Links',
items: [
{
name: 'Terms & Conditions',
link: ROUTE.TERM_CONDITION,
},
{
name: 'Privacy Policy',
link: ROUTE.TERM_CONDITION,
},
{
name: 'Blog',
link: ROUTE.TERM_CONDITION,
},
]
}
]
interface Props {
className?: string
children?: any
}
const Footer = ({ }: Props) => {
return (
<footer className={s.footer}>
<div className={s.footerMenu}>
<section className={s.menu}>
{FOOTER_COLUMNS.map(item => <FooterColumn
key={item.title}
title={item.title}
items={item.items} />)}
</section>
<FooterSocial />
</div>
<div>
© 2021 Online Grocery
</div>
</footer>
)
}
export default Footer

View File

@ -0,0 +1,33 @@
@import "../../../../../styles/utilities";
.footerColumn {
width: 50%;
margin-bottom: 4rem;
@screen md {
padding-right: 6.4rem;
width: unset;
margin-bottom: 0;
}
@screen lg {
padding-right: 12.8rem;
}
.title {
@apply sm-headline text-active;
margin-bottom: 2.4rem;
}
ul {
list-style: none;
li {
&:not(:last-child) {
margin-bottom: 1.6rem;
}
a {
@apply transition-all duration-200 no-underline;
&:hover {
color: var(--primary);
}
}
}
}
}

View File

@ -0,0 +1,38 @@
import Link from 'next/link';
import React from 'react';
import s from './FooterColumn.module.scss'
interface Props {
title: string,
items: { link: string, name: string, isOpenNewTab?: boolean }[],
}
const FooterColumn = ({ title, items }: Props) => {
return (
<section className={s.footerColumn}>
<h4 className={s.title}>
{title}
</h4>
<ul>
{
items.map(item => <li key={item.name}>
{
item.isOpenNewTab ?
<a href={item.link} target="_blank" rel="noopener noreferrer">
{item.name}
</a>
:
<Link href={item.link}>
<a >
{item.name}
</a>
</Link>
}
</li>)
}
</ul>
</section>
);
};
export default FooterColumn;

View File

@ -0,0 +1,43 @@
@import "../../../../../styles/utilities";
.footerSocial {
.title {
@apply sm-headline text-active;
margin-bottom: 2.4rem;
}
.socialMedia,
.payment {
@apply list-none flex items-center;
}
.socialMedia {
li {
@apply transition-all duration-200;
margin-right: 1.6rem;
&:last-child {
margin-right: 0;
}
a {
@apply no-underline;
}
&:hover {
svg path {
fill: var(--primary);
}
}
}
}
.payment {
margin-top: 3.2rem;
li {
margin-right: 1.6rem;
width: 4rem;
img {
width: 100%;
object-fit: contain;
}
&:last-child {
margin-right: 0;
}
}
}
}

View File

@ -0,0 +1,75 @@
import React from 'react';
import IconFacebook from 'src/components/icons/IconFacebook';
import IconInstagram from 'src/components/icons/IconInstagram';
import IconTwitter from 'src/components/icons/IconTwitter';
import IconYoutube from 'src/components/icons/IconYoutube';
import { SOCIAL_LINKS } from 'src/utils/constanst.utils';
import IconVisa from '../../../../../assets/imgs/visa.png';
import IconMasterCard from '../../../../../assets/imgs/mastercard.png';
import IconGooglePlay from '../../../../../assets/imgs/gpay.png';
import IconApplePay from '../../../../../assets/imgs/apple_pay.png';
import s from './FooterSocial.module.scss';
const SOCIAL_MENU = [
{
icon: <IconFacebook />,
link: SOCIAL_LINKS.FB,
},
{
icon: <IconTwitter />,
link: SOCIAL_LINKS.TWITTER,
},
{
icon: <IconYoutube />,
link: SOCIAL_LINKS.YOUTUBE,
},
{
icon: <IconInstagram />,
link: SOCIAL_LINKS.IG,
},
]
const PAYMENT_METHODS = [
{
icon: IconVisa.src,
name: 'Visa'
},
{
icon: IconMasterCard.src,
name: 'Master Card'
},
{
icon: IconGooglePlay.src,
name: 'GooglePay'
},
{
icon: IconApplePay.src,
name: 'Apple Pay'
},
]
const FooterSocial = () => {
return (
<section className={s.footerSocial}>
<div className={s.title}>Social</div>
<ul className={s.socialMedia}>
{
SOCIAL_MENU.map(item => <li key={item.link}>
<a href={item.link} target="_blank" rel="noopener noreferrer">
{item.icon}
</a>
</li>)
}
</ul>
<ul className={s.payment}>
{
PAYMENT_METHODS.map(item => <li key={item.name}>
<img src={item.icon} alt={item.name} />
</li>)
}
</ul>
</section>
);
};
export default FooterSocial;

View File

@ -1,20 +1,17 @@
@import "../../../styles/utilities";
.header {
.btn {
// @apply font-bold py-2 px-4 rounded;
@apply sticky bg-white shadow-md;
top: 0;
z-index: 9999;
margin-bottom: 3.2rem;
&.full {
@apply shadow-none;
border: 1px solid var(--border-line);
}
.btnBlue {
@apply bg-primary hover:bg-warning text-label font-bold py-2 px-4 custom-border-radius;
}
.link {
color: theme("colors.warning");
}
.heading {
@apply text-base font-heading;
}
.paragraph {
@apply topline;
.menu {
padding-left: 3.2rem;
padding-right: 3.2rem;
}
.logo {
@apply font-logo;

View File

@ -1,19 +1,48 @@
import { FC } from 'react'
import classNames from 'classnames'
import React, { memo, useEffect, useState } from 'react'
import { useModalCommon } from 'src/components/hooks/useModalCommon'
import { isMobile } from 'src/utils/funtion.utils'
import ModalAuthenticate from '../ModalAuthenticate/ModalAuthenticate'
import HeaderHighLight from './components/HeaderHighLight/HeaderHighLight'
import HeaderMenu from './components/HeaderMenu/HeaderMenu'
import HeaderSubMenu from './components/HeaderSubMenu/HeaderSubMenu'
import HeaderSubMenuMobile from './components/HeaderSubMenuMobile/HeaderSubMenuMobile'
import s from './Header.module.scss'
interface Props {
className?: string
children?: any
}
const Header: FC<Props> = ({ }: Props) => {
const Header = memo(() => {
const [isFullHeader, setIsFullHeader] = useState<boolean>(true)
const { visible: visibleModalAuthen, closeModal: closeModalAuthen, openModal: openModalAuthen } = useModalCommon({ initialValue: false })
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
const handleScroll = () => {
if (!isMobile()) {
if (window.scrollY === 0) {
setIsFullHeader(true)
} else {
setIsFullHeader(false)
}
}
}
return (
<div className={s.header}>
This is Header
<h1 className={s.heading}>This is heading</h1>
<div className={s.logo}>This is logo text</div>
</div>
<>
<header className={classNames({ [s.header]: true, [s.full]: isFullHeader })}>
<HeaderHighLight isShow={isFullHeader} />
<div className={s.menu}>
<HeaderMenu isFull={isFullHeader} openModalAuthen={openModalAuthen} />
<HeaderSubMenu isShow={isFullHeader} />
</div>
</header>
<HeaderSubMenuMobile />
<ModalAuthenticate visible={visibleModalAuthen} closeModal={closeModalAuthen} />
</>
)
}
})
export default Header

View File

@ -0,0 +1,39 @@
@import "../../../../../styles/utilities";
.headerHighLight {
@apply hidden;
@screen md {
transform: translateY(-10rem);
height: 0;
&.show {
@apply flex justify-between items-center spacing-horizontal bg-primary caption;
animation: showHeaderHightlight 0.2s;
height: unset;
transform: none;
padding-top: 0.8rem;
padding-bottom: 0.8rem;
color: var(--white);
.menu {
@apply flex items-center list-none;
padding: 0.8rem 0;
li {
&:not(:last-child) {
margin-right: 3.2rem;
}
a {
@appy no-underline;
}
}
}
}
}
}
@keyframes showHeaderHightlight {
0% {
transform: translateY(-4rem);
}
100% {
transform: none;
}
}

View File

@ -0,0 +1,49 @@
import classNames from 'classnames'
import Link from 'next/link'
import { memo, useEffect, useRef } from 'react'
import { ROUTE } from 'src/utils/constanst.utils'
import s from './HeaderHighLight.module.scss'
const MENU = [
{
name: 'Delivery & Policy',
link: ROUTE.PRIVACY_POLICY,
},
{
name: 'Blog',
link: ROUTE.BLOGS,
},
{
name: 'About Us',
link: ROUTE.ABOUT,
},
]
interface Props {
children?: any,
isShow: boolean,
}
const HeaderHighLight = memo(({ isShow }: Props) => {
return (
<section className={classNames({ [s.headerHighLight]: true, [s.show]: isShow })}>
<div>
Free Shipping on order $49+ / Express $99+
</div>
<ul className={s.menu}>
{
MENU.map(item => <li key={item.name}>
<Link href={item.link}>
<a >
{item.name}
</a>
</Link>
</li>)
}
</ul>
</section>
)
})
export default HeaderHighLight

View File

@ -0,0 +1,65 @@
@import "../../../../../styles/utilities";
.headerMenu {
padding-top: 1.6rem;
padding-bottom: 0.8rem;
@screen md {
@apply flex justify-between items-center;
padding-top: 0.8rem;
padding-bottom: 0.8rem;
&.full {
padding-top: 2.4rem;
padding-bottom: 2.4rem;
}
}
.left {
.top {
@apply flex justify-between items-center;
.iconCart {
}
}
.inputSearch {
margin-top: 2.4rem;
@screen lg {
min-width: 51.2rem;
max-width: 50%;
}
}
@screen md {
@apply flex items-center;
.top {
.iconCart {
@apply hidden;
}
}
.inputSearch {
margin-left: 4.8rem;
margin-top: 0;
}
}
}
.menu {
@apply hidden;
@screen md {
@apply flex items-center list-none;
li {
@apply flex justify-center items-center w-full;
&:not(:last-child) {
margin-right: 4.8rem;
@screen lg {
margin-right: 6.4rem;
}
}
a {
@appy no-underline;
&.iconFovourite {
svg path {
fill: var(--negative);
}
}
}
}
}
}
}

View File

@ -0,0 +1,74 @@
import classNames from 'classnames'
import Link from 'next/link'
import { memo, useMemo } from 'react'
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 s from './HeaderMenu.module.scss'
interface Props {
children?: any,
isFull: boolean,
openModalAuthen: () => void,
}
const HeaderMenu = memo(({ isFull, openModalAuthen }: Props) => {
const optionMenu = useMemo(() => [
{
onClick: openModalAuthen,
name: 'Login (Demo)',
},
{
link: ROUTE.ACCOUNT,
name: 'Account',
},
{
link: '/',
name: 'Logout',
},
], [openModalAuthen])
return (
<section className={classNames({ [s.headerMenu]: true, [s.full]: isFull })}>
<div className={s.left}>
<div className={s.top}>
<div>Online Grocery</div>
<button className={s.iconCart}>
<IconBuy />
</button>
</div>
<div className={s.inputSearch}>
<InputSearch />
</div>
</div>
<ul className={s.menu}>
<li>
<Link href={`${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.ORDER}`}>
<a >
<IconHistory />
</a>
</Link>
</li>
<li>
<Link href={`${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.FAVOURITE}`}>
<a className={s.iconFovourite}>
<IconHeart />
</a>
</Link>
</li>
<li>
<MenuDropdown options={optionMenu} isHasArrow={false}><IconUser /></MenuDropdown>
</li>
<li>
<button>
<IconBuy />
</button>
</li>
</ul>
</section>
)
})
export default HeaderMenu

View File

@ -0,0 +1,3 @@
.headerNoti {
@apply flex items-center;
}

View File

@ -0,0 +1,16 @@
import React from 'react';
import NotiMessage from 'src/components/common/NotiMessage/NotiMessage';
import { IconInfo } from 'src/components/icons';
import s from './HeaderNoti.module.scss';
const HeaderNoti = () => {
return (
<NotiMessage>
<div className={s.headerNoti}>
<IconInfo />&nbsp;<span>You can buy fresh products after <b>11pm</b> or <b>8am</b></span>
</div>
</NotiMessage>
);
};
export default HeaderNoti;

View File

@ -0,0 +1,42 @@
@import "../../../../../styles/utilities";
.headerSubMenu {
@apply hidden;
@screen md {
transform: translateY(-10rem);
height: 0;
&.show {
@apply block;
padding-bottom: 2.4rem;
transform: none;
height: unset;
@screen lg {
@apply flex justify-between items-center;
}
.menu {
@apply flex items-center list-none;
margin-bottom: 2.4rem;
@screen lg {
margin-bottom: 0;
}
li {
&:not(:last-child) {
margin-right: 2.4rem;
@screen lg {
margin-right: 4rem;
}
}
a {
@appy no-underline;
}
&:hover {
@apply text-primary;
}
&.active {
@apply text-primary;
}
}
}
}
}
}

View File

@ -0,0 +1,88 @@
import classNames from 'classnames'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { memo } from 'react'
import MenuDropdown from 'src/components/common/MenuDropdown/MenuDropdown'
import { ProductFeature, QUERY_KEY, ROUTE } from 'src/utils/constanst.utils'
import HeaderNoti from './HeaderNoti/HeaderNoti'
import s from './HeaderSubMenu.module.scss'
const MENU = [
{
name: 'New Items',
link: `${ROUTE.PRODUCTS}?${QUERY_KEY.FEATURED}=${ProductFeature.NewItem}`,
},
{
name: 'Sales',
link: `${ROUTE.PRODUCTS}?${QUERY_KEY.FEATURED}=${ProductFeature.Sales}`,
},
{
name: 'Best Sellers',
link: `${ROUTE.PRODUCTS}?${QUERY_KEY.FEATURED}=${ProductFeature.BestSellers}`,
},
{
name: 'About Us',
link: ROUTE.ABOUT,
},
{
name: 'Blog',
link: ROUTE.BLOGS,
},
]
// note: hard code, remove later
const CATEGORY = [
{
name: 'Veggie',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=veggie`,
},
{
name: 'Seafood',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=seafood`,
},
{
name: 'Frozen',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=frozen`,
},
{
name: 'Coffee Bean',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=coffee-bean`,
},
{
name: 'Sauce',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=sauce`,
},
]
interface Props {
children?: any,
isShow: boolean,
}
const HeaderSubMenu = memo(({ isShow }: Props) => {
const router = useRouter()
return (
<section className={classNames({ [s.headerSubMenu]: true, [s.show]: isShow })}>
<ul className={s.menu}>
{/* todo: handle active item */}
<li>
<MenuDropdown options={CATEGORY} align="left">Categories</MenuDropdown>
</li>
{
MENU.map(item => <li key={item.name}
className={classNames({ [s.active]: router.asPath === item.link })}>
<Link href={item.link}>
<a >
{item.name}
</a>
</Link>
</li>)
}
</ul>
<HeaderNoti />
</section>
)
})
export default HeaderSubMenu

View File

@ -0,0 +1,51 @@
@import "../../../../../styles/utilities";
.headerSubMenuMobile {
@apply fixed w-full bg-white;
bottom: 0;
left: 0;
padding: 2rem 1rem;
border-top: 1px solid var(--border-line);
box-shadow: -5px 6px 10px rgba(0, 0, 0, 0.2);
z-index: 9999;
.menu {
@apply grid grid-cols-4;
li {
a {
@apply transition-all duration-200 no-underline;
&:hover {
color: var(--primary);
}
}
.menuItem {
@apply flex flex-col justify-center items-center sm-label;
.icon {
position: relative;
margin-bottom: 0.5rem;
svg path {
fill: currentColor;
}
}
&.active {
@apply text-primary;
}
&.dot {
.icon {
&::after {
@apply absolute bg-negative rounded-full;
content: "";
top: 0;
right: 0;
$size: 1rem;
width: $size;
height: $size;
}
}
}
}
}
}
@screen md {
@apply hidden;
}
}

View File

@ -0,0 +1,66 @@
import classNames from 'classnames'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { memo } from 'react'
import { IconHeart, IconHome, IconShopping, IconUser } from 'src/components/icons'
import { ACCOUNT_TAB, QUERY_KEY, ROUTE } from 'src/utils/constanst.utils'
import s from './HeaderSubMenuMobile.module.scss'
const OPTION_MENU = [
{
link: ROUTE.HOME,
name: 'Home',
icon: <IconHome />,
isMarked: true,
},
{
link: ROUTE.PRODUCTS,
name: 'Shopping',
icon: <IconShopping />,
isMarked: false,
},
{
link: `${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.FAVOURITE}`,
name: 'Favourites',
icon: <IconHeart />,
isMarked: false,
},
{
link: ROUTE.ACCOUNT,
name: 'Account',
icon: <IconUser />,
isMarked: false,
},
]
interface Props {
children?: any
}
const HeaderSubMenuMobile = memo(({ }: Props) => {
const router = useRouter()
return (
<header className={s.headerSubMenuMobile}>
<ul className={s.menu}>
{
OPTION_MENU.map(item => <li key={item.name}>
<Link href={item.link}>
<a >
<div className={classNames({
[s.menuItem]: true,
[s.dot]: item.isMarked,
[s.active]: router.pathname === item.link, // todo: handle active item
})}>
<span className={s.icon}>{item.icon}</span>
<span className={s.label}>{item.name}</span>
</div>
</a>
</Link>
</li>)
}
</ul>
</header>
)
})
export default HeaderSubMenuMobile

View File

@ -0,0 +1,17 @@
@import '../../../styles/utilities';
.headingCommon {
@apply heading-2 font-heading text-left;
&.highlight {
color: var(--negative);
}
&.light {
color: var(--white);
}
&.center {
@apply text-center;
}
}

View File

@ -0,0 +1,23 @@
import React from 'react'
import classNames from 'classnames'
import s from './HeadingCommon.module.scss'
interface HeadingCommonProps {
type?: 'highlight' | 'light' | 'default';
align?: 'center' | 'left';
children: string;
}
const HeadingCommon = ({ type='default', align='left', children }: HeadingCommonProps) => {
return (
<h2 className={classNames(s.headingCommon, {
[s[type]]: type,
[s[align]]: align
})}
>{children}</h2>
)
}
export default HeadingCommon

View File

@ -0,0 +1,51 @@
@import "../../../styles/utilities";
.inputWrap {
@apply flex items-center relative;
.icon {
@apply absolute;
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;
}
&::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);
}
}
}
}

View File

@ -0,0 +1,69 @@
import classNames from 'classnames';
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { KEY } from 'src/utils/constanst.utils';
import s from './InputCommon.module.scss';
type Ref = {
focus: () => void
} | null;
interface Props {
children?: React.ReactNode,
value?: string | number,
placeholder?: string,
type?: 'text' | 'number' | 'email' | 'password',
styleType?: 'default' | 'custom',
backgroundTransparent?: boolean,
icon?: React.ReactNode,
onChange?: (value: string | number) => void,
onEnter?: (value: string | number) => void,
}
const InputCommon = forwardRef<Ref, Props>(({ value, placeholder, type, styleType = 'default', icon, backgroundTransparent = false,
onChange, onEnter }: Props, ref) => {
const inputElementRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputElementRef.current?.focus();
},
getValue: () => {
const value = inputElementRef.current?.value || ''
return value
}
}));
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange && onChange(e.target.value)
}
const handleKeyDown = (e: any) => {
if (e.key === KEY.ENTER && onEnter) {
const value = inputElementRef.current?.value || ''
onEnter(value)
}
}
return (
<div className={s.inputWrap}>
{
icon && <span className={s.icon}>{icon}</span>
}
<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>
)
})
export default InputCommon

View File

@ -0,0 +1,22 @@
import React from 'react';
import { IconSearch } from 'src/components/icons';
import { LANGUAGE } from 'src/utils/language.utils';
import { Inputcommon } from '..';
interface Props {
onChange?: (value: string | number) => void,
onEnter?: (value: string | number) => void,
}
const InputSearch = ({ onChange, onEnter }: Props) => {
return (
<Inputcommon placeholder={LANGUAGE.PLACE_HOLDER.SEARCH}
styleType='custom'
icon={<IconSearch />}
onChange={onChange}
onEnter={onEnter}
/>
)
}
export default InputSearch

View File

@ -0,0 +1,43 @@
.labelCommonWarper{
display: inline-flex;
align-items: flex-start;
font-weight: bold;
letter-spacing: 0.01em;
@apply text-white text-right;
&.defaultSize{
min-height: 2rem;
line-height: 2rem;
font-size: 1.2rem;
padding: 0 0.8rem;
}
&.largeSize{
max-height: 2.4rem;
line-height: 2.4rem;
font-size: 1.6rem;
padding: 0 1.8rem;
}
&.defaultType{
@apply bg-positive-dark;
}
&.discountType{
@apply bg-negative;
}
&.waitingType{
@apply bg-warning;
}
&.deliveringType{
@apply bg-info;
}
&.deliveredType{
@apply bg-positive;
}
&.defaultShape{
border-radius: 0.4rem;
}
&.halfShape{
border-radius: 0px 1.4rem 1.4rem 0px;
}
&.roundShape{
border-radius: 1.4rem;
}
}

View File

@ -0,0 +1,30 @@
import classNames from 'classnames'
import React from 'react'
import s from './LabelCommon.module.scss'
interface LabelCommonProps extends React.HTMLAttributes<HTMLDivElement> {
size?: 'default' | 'large'
shape?: 'half' | 'round' | 'default'
type?: 'default' | 'discount' | 'waiting' | 'delivering' | 'delivered'
color?: string
}
const LabelCommon = ({
size = 'default',
type = 'default',
shape = "default",
children,
}: LabelCommonProps) => {
return (
<div
className={classNames(s.labelCommonWarper, {
[s[`${size}Size`]]: size,
[s[`${type}Type`]]: type,
[s[`${shape}Shape`]]: shape,
})}
>
{children}
</div>
)
}
export default LabelCommon

View File

@ -0,0 +1,8 @@
.mainLayout {
display: flex;
flex-direction: column;
min-height: 100vh;
> main {
flex: 1;
}
}

View File

@ -1,7 +1,9 @@
import { FC, useRef, useEffect } from 'react'
import Header from '../Header/Header'
import { CommerceProvider } from '@framework'
import { useRouter } from 'next/router'
import { FC } from 'react'
import Footer from '../Footer/Footer'
import Header from '../Header/Header'
import s from './Layout.module.scss'
interface Props {
className?: string
@ -14,8 +16,11 @@ const Layout: FC<Props> = ({ children }) => {
return (
<CommerceProvider locale={locale}>
<Header />
<main>{children}</main>
<div className={s.mainLayout}>
<Header />
<main >{children}</main>
<Footer />
</div>
</CommerceProvider>
)

View File

@ -1,17 +1,18 @@
@import '../../../styles/utilities';
.logo{
.logo {
display: flex;
.eclipse{
.eclipse {
width: 3.2rem;
height: 3.2rem;
background-color: var(--primary);
border-radius: 50%;
margin-right: 1.2rem;
}
.conTent{
.content {
@apply font-logo;
line-height: 3.2rem;
letter-spacing: -0.02rem;
font-size: 16px;
line-height: 32px;
letter-spacing: -0.02em;
}
}

View File

@ -1,15 +1,11 @@
import s from './Logo.module.scss'
interface Props {
}
const Logo = ({}: Props) => {
const Logo = () => {
return(
<div className={s.logo}>
<div className={s.eclipse}>
</div>
<div className={s.conTent}>
<div className={s.content}>
ONLINE GROCERY
</div>
</div>

View File

@ -0,0 +1,98 @@
@import "../../../styles/utilities";
.menuDropdown {
@apply relative cursor-pointer;
width: fit-content;
&:hover {
.label {
color: var(--primary);
svg path {
fill: currentColor;
}
}
.menu {
@apply block;
animation: menuDropdownAnimation 0.2s ease-out;
}
}
.label {
@apply flex justify-end items-center transition-all duration-200;
svg path {
width: fit-content;
}
}
&.arrow {
.label {
margin-right: 1.6rem;
}
&::after {
@apply inline-block absolute transition-all duration-100;
content: "";
top: 35%;
right: 0;
border: solid currentColor;
border-width: 0 2px 2px 0;
padding: 2px;
transform: rotate(45deg);
}
&:hover {
&::after {
@apply border-primary;
transform: rotate(-135deg);
}
}
}
.menu {
@apply hidden absolute;
content: "";
right: 0;
top: 2rem;
height: max-content;
min-width: 19.2rem;
z-index: 100;
&.left {
left: 0;
}
&:hover {
@apply block shadow-md;
}
.menuIner {
@apply rounded list-none bg-white;
border: 1px solid var(--text-active);
margin-top: 0.4rem;
li {
@apply block w-full transition-all duration-200 cursor-pointer text-active;
button {
all: unset;
color: currentColor;
@apply text-left w-full;
}
button,
a {
padding: 0.8rem 1.6rem;
}
a {
@apply block;
}
&:hover {
@apply bg-primary-lightest;
color: var(--primary);
}
}
}
}
}
@keyframes menuDropdownAnimation {
0% {
opacity: 0;
transform: translateY(1.6rem);
}
100% {
opacity: 1;
transform: none;
}
}

View File

@ -0,0 +1,47 @@
import classNames from 'classnames';
import Link from 'next/link';
import React from 'react';
import s from './MenuDropdown.module.scss';
interface Props {
children?: React.ReactNode,
options: { link?: string, name: string, onClick?: () => void }[],
isHasArrow?: boolean,
align?: 'left'
}
const MenuDropdown = ({ options, children, isHasArrow = true, align }: Props) => {
return (
<div className={classNames({
[s.menuDropdown]: true,
[s.arrow]: isHasArrow,
})}>
<span className={s.label}>
{children}
</span>
<section className={classNames({
[s.menu]: true,
[s.left]: align === 'left',
})} >
<ul className={s.menuIner}>
{
options.map(item => <li key={item.name}>
{item.onClick ?
<button onClick={item.onClick}>
{item.name}
</button>
:
<Link href={item.link || ''}>
<a >
{item.name}
</a>
</Link>}
</li>)
}
</ul>
</section>
</div>
);
};
export default MenuDropdown;

View File

@ -0,0 +1,10 @@
.formAuthenticate {
@apply overflow-hidden;
.inner {
@apply grid grid-cols-2 overflow-hidden transition-all duration-200;
width: 200%;
&.register {
transform: translateX(-50%);
}
}
}

View File

@ -0,0 +1,36 @@
import classNames from 'classnames'
import React, { useState } from 'react'
import ModalCommon from '../ModalCommon/ModalCommon'
import FormLogin from './components/FormLogin/FormLogin'
import FormRegister from './components/FormRegister/FormRegister'
import s from './ModalAuthenticate.module.scss'
interface Props {
visible: boolean,
closeModal: () => void,
}
const ModalAuthenticate = ({ visible, closeModal }: Props) => {
const [isLogin, setIsLogin] = useState<boolean>(true)
const onSwitch = () => {
setIsLogin(!isLogin)
}
return (
<ModalCommon visible={visible} onClose={closeModal} title={isLogin ? 'Sign In' : 'Create Account'}>
<section className={s.formAuthenticate}>
<div className={classNames({
[s.inner]: true,
[s.register]: !isLogin,
})}>
<FormLogin isHide={!isLogin} onSwitch={onSwitch} />
<FormRegister isHide={isLogin} onSwitch={onSwitch} />
</div>
</section>
</ModalCommon>
)
}
export default ModalAuthenticate

View File

@ -0,0 +1,35 @@
.formAuthen {
@apply bg-white w-full;
.inner {
@screen md {
max-width: 52rem;
margin: auto;
}
.body {
> div {
&:not(:last-child) {
margin-bottom: 1.6rem;
}
}
}
.others {
@apply font-bold text-center;
margin-top: 4rem;
span {
@apply text-active;
margin-right: 0.8rem;
}
button {
all: unset;
@apply text-primary cursor-pointer;
&:focus-visible {
outline: 2px solid #000;
}
&:focus {
outline: none;
}
}
}
}
}

View File

@ -0,0 +1,9 @@
.bottom {
@apply flex justify-between items-center;
margin: 4rem auto;
.forgotPassword {
@apply font-bold;
color: var(--primary);
}
}

View File

@ -0,0 +1,53 @@
import Link from 'next/link'
import React, { useRef, useEffect } from 'react'
import { Inputcommon, ButtonCommon } 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'
interface Props {
isHide: boolean,
onSwitch: () => void
}
const FormLogin = ({ onSwitch, isHide }: Props) => {
const emailRef = useRef<CustomInputCommon>(null)
useEffect(() => {
if (!isHide) {
emailRef.current?.focus()
}
}, [isHide])
return (
<section className={classNames({
[s.formAuthen]: 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' />
</div>
<div className={styles.bottom}>
<Link href={ROUTE.FORGOT_PASSWORD}>
<a href="" className={styles.forgotPassword}>
Forgot Password?
</a>
</Link>
<ButtonCommon size='large'>Sign in</ButtonCommon>
</div>
<SocialAuthen />
<div className={s.others}>
<span>Don't have an account?</span>
<button onClick={onSwitch}>Create Account</button>
</div>
</div>
</section>
)
}
export default FormLogin

View File

@ -0,0 +1,15 @@
@import "../../../../../styles/utilities";
.formRegister {
.passwordNote {
@apply text-center caption text-label;
margin-top: 0.8rem;
}
.bottom {
@apply flex justify-between items-center w-full;
margin: 4rem auto;
button {
@apply w-full;
}
}
}

View File

@ -0,0 +1,50 @@
import React, { useEffect, useRef } from 'react'
import { ButtonCommon, Inputcommon } from 'src/components/common'
import s from '../FormAuthen.module.scss'
import styles from './FormRegister.module.scss'
import SocialAuthen from '../SocialAuthen/SocialAuthen'
import classNames from 'classnames'
import { CustomInputCommon } from 'src/utils/type.utils'
interface Props {
isHide: boolean,
onSwitch: () => void
}
const FormRegister = ({ onSwitch, isHide }: Props) => {
const emailRef = useRef<CustomInputCommon>(null)
useEffect(() => {
if (!isHide) {
emailRef.current?.focus()
}
}, [isHide])
return (
<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' />
<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>
</div>
<div className={styles.bottom}>
<ButtonCommon size='large'>Create Account</ButtonCommon>
</div>
<SocialAuthen />
<div className={s.others}>
<span>Already an account?</span>
<button onClick={onSwitch}>Sign In</button>
</div>
</div>
</section>
)
}
export default FormRegister

View File

@ -0,0 +1,36 @@
@import "../../../../../styles/utilities";
.socialAuthen {
.captionText {
@apply relative text-center;
margin-bottom: 4rem;
span {
@apply relative bg-white uppercase text-label caption;
padding: 0 0.8rem;
z-index: 10;
}
&::after {
@apply absolute bg-line;
content: "";
width: 100%;
height: 1px;
top: 50%;
left: 0;
transform: translateY(-50%);
}
}
.btns {
@apply grid grid-cols-3;
grid-gap: 1.6rem;
.buttonWithIcon {
@apply flex items-center;
.label {
@apply hidden;
@screen md {
@apply inline-block;
margin-left: 0.8rem;
}
}
}
}
}

View File

@ -0,0 +1,37 @@
import React from 'react'
import ButtonCommon from 'src/components/common/ButtonCommon/ButtonCommon'
import { IconApple, IconFacebookColor, IconGoogleColor } from 'src/components/icons'
import s from './SocialAuthen.module.scss'
const SocialAuthen = () => {
return (
<section className={s.socialAuthen}>
<div className={s.captionText}>
<span>
OR CONTINUE WITH
</span>
</div>
<div className={s.btns}>
<ButtonCommon type='light' size='large'>
<span className={s.buttonWithIcon}>
<IconFacebookColor /><span className={s.label}>Facebook</span>
</span>
</ButtonCommon>
<ButtonCommon type='light' size='large'>
<span className={s.buttonWithIcon}>
<IconApple />
<span className={s.label}>Apple</span>
</span>
</ButtonCommon>
<ButtonCommon type='light' size='large'>
<span className={s.buttonWithIcon}>
<IconGoogleColor />
<span className={s.label}>Google</span>
</span>
</ButtonCommon>
</div>
</section>
)
}
export default SocialAuthen

View File

@ -0,0 +1,32 @@
@import '../../../styles/utilities';
.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;
.top {
margin-bottom: 4rem;
}
.title{
@apply font-heading heading-3;
padding: 0 0.8rem 0 0.8rem;
}
.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,9 @@
@import "../../../styles/utilities";
.notiMessage {
@apply caption bg-info-light;
width: fit-content;
color: var(--info-dark);
padding: 0.4rem 1.6rem;
border-radius: 3rem;
}

View File

@ -0,0 +1,16 @@
import React from 'react'
import s from './NotiMessage.module.scss'
interface Props {
children?: React.ReactNode
}
const NotiMessage = ({ children }: Props) => {
return (
<div className={s.notiMessage}>
{children}
</div>
)
}
export default NotiMessage

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,39 @@
@import '../../../styles/utilities';
.quanittyInputWarper{
border-color: theme("textColor.active");
@apply border border-solid inline-flex justify-between items-center custom-border-radius;
.plusIcon, .minusIcon{
&:hover{
cursor: pointer;
}
}
&.default{
max-width: 18.4rem;
min-height: 4rem;
.plusIcon, .minusIcon{
margin: 0.8rem;
width: 2.4rem;
height: 2.4rem;
}
}
&.small{
max-width: 10rem;
min-height: 2.8rem;
.plusIcon, .minusIcon{
margin: 0 0.6rem;
// width: 1rem;
// height: 1rem;
}
}
.quanittyInput{
@apply bg-background outline-none w-1/2 text-center h-full font-bold;
font-size: 20px;
line-height: 28px;
color: theme("textColor.active");
&::-webkit-inner-spin-button, &::-webkit-inner-spin-button{
-webkit-appearance: none;
margin: 0;
}
}
}

View File

@ -0,0 +1,83 @@
import React, { ChangeEvent, useEffect, useState } from 'react'
import s from './QuanittyInput.module.scss'
import classNames from 'classnames'
import { Minus, Plus } from '@components/icons'
interface QuanittyInputProps
extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'onChange' | 'min' | 'max' | 'step' | "type" | "size"
> {
size?: 'default' | 'small'
onChange?: (value: number) => void
initValue?: number
min?: number
max?: number
step?: number
}
const QuanittyInput = ({
size = 'default',
onChange,
initValue = 0,
min,
max,
step = 1,
...props
}: QuanittyInputProps) => {
const [value, setValue] = useState<number>(0)
useEffect(() => {
onChange && onChange(value)
}, [value])
useEffect(() => {
initValue && setValue(initValue)
}, [initValue])
const onPlusClick = () => {
if (max && value + step > max) {
setValue(max)
} else {
setValue(value + step)
}
}
const onMinusClick = () => {
if (min && value - step < min) {
setValue(min)
} else {
setValue(value - step)
}
}
const onValueChange = (e: ChangeEvent<HTMLInputElement>) => {
let value = Number(e.target.value) || 0
if (min && value < min) {
setValue(min)
} else if (max && value > max) {
setValue(max)
} else {
setValue(value)
}
}
return (
<div className={classNames(s.quanittyInputWarper, { [s[size]]: size })}>
<div className={s.minusIcon} onClick={onMinusClick}>
<Minus />
</div>
<input
{...props}
type="number"
value={value}
onChange={onValueChange}
className={s.quanittyInput}
/>
<div className={s.plusIcon} onClick={onPlusClick}>
<Plus />
</div>
</div>
)
}
export default QuanittyInput

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

@ -0,0 +1,15 @@
import React, { MutableRefObject } from 'react'
interface ScrollTargetProps {
refScrollUp: MutableRefObject<HTMLDivElement>;
}
const ScrollTarget = ({ refScrollUp } : ScrollTargetProps) => {
return (
<div ref={refScrollUp}></div>
)
}
export default ScrollTarget

View File

@ -0,0 +1,24 @@
@import '../../../styles/utilities';
.scrollToTop {
@apply hidden;
@screen md {
&.show {
@apply block rounded-lg fixed cursor-pointer;
right: 11.2rem;
bottom: 21.6rem;
width: 6.4rem;
height: 6.4rem;
background-color: var(--border-line);
}
&.hide {
@apply hidden;
}
}
.scrollToTopBtn {
@apply outline-none w-full h-full;
}
}

View File

@ -0,0 +1,54 @@
import React, { useState, useEffect, MutableRefObject } from 'react'
import classNames from 'classnames'
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 [scrollPosition, setSrollPosition] = useState(0);
const [showScrollToTop, setShowScrollToTop] = useState("hide");
function handleVisibleButton() {
const position = window.pageYOffset;
setSrollPosition(position);
if (scrollPosition > visibilityHeight) {
return setShowScrollToTop("show")
} else if (scrollPosition < visibilityHeight) {
return setShowScrollToTop("hide");
}
};
function handleScrollUp() {
target.current.scrollIntoView({ behavior: "smooth" });
}
function addEventScroll() {
window.addEventListener("scroll", handleVisibleButton);
}
useEffect(() => {
addEventScroll()
});
return (
<div className={classNames(s.scrollToTop, {
[s[`${showScrollToTop}`]]: showScrollToTop
})}
onClick={handleScrollUp}
>
<button className={s.scrollToTopBtn}>
<ArrowUp />
</button>
</div>
)
}
export default ScrollToTop

View File

@ -0,0 +1,19 @@
import React from 'react';
import ReactPlayer from 'react-player/lazy'
interface Props {
url: string,
controls?: boolean,
muted?: boolean,
}
const VideoPlayer = ({ url, controls, muted }: Props) => {
return (
<ReactPlayer
url={url}
controls={controls}
muted={muted} />
);
};
export default VideoPlayer;

View File

@ -1,18 +1,16 @@
@import '../../../styles/utilities';
@import "../../../styles/utilities";
.viewAll{
.viewAll {
display: flex;
color: theme("colors.primary");
.content{
.content {
color: var(--primary);
margin: 0.8rem 0.8rem 0.8rem 1.6rem;
font-weight: bold;
}
.vector{
.vector {
margin: 0.8rem 0rem 0.8rem 0rem;
svg path{
fill: theme("colors.primary");
svg path {
fill: var(--primary);
}
}
}

Some files were not shown because too many files have changed in this diff Show More