Resolved merge conflict by incorporating both suggestions.
1
grocery-vercel-commerce
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 3c7aa8e862bfd8d44719be44c6c0a31ab01524a3
|
9962
package-lock.json
generated
@ -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",
|
||||
|
@ -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
@ -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
|
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 |
1
public/assets/svg/checkmark.svg
Normal 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 |
BIN
src/assets/imgs/apple_pay.png
Normal file
After Width: | Height: | Size: 467 B |
BIN
src/assets/imgs/gpay.png
Normal file
After Width: | Height: | Size: 607 B |
BIN
src/assets/imgs/mastercard.png
Normal file
After Width: | Height: | Size: 519 B |
BIN
src/assets/imgs/visa.png
Normal file
After Width: | Height: | Size: 640 B |
17
src/components/common/Author/Author.module.scss
Normal 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;
|
||||
}
|
||||
}
|
21
src/components/common/Author/Author.tsx
Normal 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;
|
BIN
src/components/common/Author/img/author.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
52
src/components/common/Banner/Banner.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
src/components/common/Banner/Banner.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import classNames from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import React, { memo } from 'react'
|
||||
import { IconArrowRight } from 'src/components/icons'
|
||||
import { ROUTE } from 'src/utils/constanst.utils'
|
||||
import { LANGUAGE } from 'src/utils/language.utils'
|
||||
import ButtonCommon from '../ButtonCommon/ButtonCommon'
|
||||
import s from './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
|
@ -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;
|
||||
|
@ -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}
|
||||
|
26
src/components/common/ButtonIconBuy/ButtonIconBuy.tsx
Normal 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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
103
src/components/common/CarouselCommon/CarouselCommon.tsx
Normal 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
|
@ -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 ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
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
|
@ -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;
|
||||
}
|
||||
}
|
40
src/components/common/CheckboxCommon/CheckboxCommon.tsx
Normal 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;
|
@ -0,0 +1,5 @@
|
||||
.subtitle {
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-height);
|
||||
margin-top: .4rem;
|
||||
}
|
@ -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
|
9
src/components/common/DateTime/DateTime.module.scss
Normal 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);
|
||||
}
|
15
src/components/common/DateTime/DateTime.tsx
Normal 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;
|
@ -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
|
31
src/components/common/Footer/Footer.module.scss
Normal 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;
|
||||
}
|
||||
}
|
85
src/components/common/Footer/Footer.tsx
Normal 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
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
||||
|
@ -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) => {
|
||||
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>
|
||||
)
|
||||
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 (
|
||||
<>
|
||||
<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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -0,0 +1,3 @@
|
||||
.headerNoti {
|
||||
@apply flex items-center;
|
||||
}
|
@ -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 /> <span>You can buy fresh products after <b>11pm</b> or <b>8am</b></span>
|
||||
</div>
|
||||
</NotiMessage>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderNoti;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
23
src/components/common/HeadingCommon/HeadingCommon.tsx
Normal 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
|
51
src/components/common/InputCommon/InputCommon.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
69
src/components/common/InputCommon/InputCommon.tsx
Normal 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
|
22
src/components/common/InputSearch/InputSearch.tsx
Normal 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
|
43
src/components/common/LabelCommon/LabelCommon.module.scss
Normal 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;
|
||||
}
|
||||
}
|
30
src/components/common/LabelCommon/LabelCommon.tsx
Normal 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
|
8
src/components/common/Layout/Layout.module.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.mainLayout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
> main {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
@ -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}>
|
||||
<div className={s.mainLayout}>
|
||||
<Header />
|
||||
<main >{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</CommerceProvider>
|
||||
|
||||
)
|
||||
|
@ -9,9 +9,10 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
98
src/components/common/MenuDropdown/MenuDropdown.module.scss
Normal 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;
|
||||
}
|
||||
}
|
47
src/components/common/MenuDropdown/MenuDropdown.tsx
Normal 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;
|
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
.bottom {
|
||||
@apply flex justify-between items-center;
|
||||
margin: 4rem auto;
|
||||
.forgotPassword {
|
||||
@apply font-bold;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
32
src/components/common/ModalCommon/ModalCommon.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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
|
@ -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;
|
||||
}
|
16
src/components/common/NotiMessage/NotiMessage.tsx
Normal 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
|
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
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
83
src/components/common/QuanittyInput/QuanittyInput.tsx
Normal 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
|
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
|
15
src/components/common/ScrollToTop/ScrollTarget.tsx
Normal 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
|
24
src/components/common/ScrollToTop/ScrollToTop.module.scss
Normal 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;
|
||||
}
|
||||
}
|
54
src/components/common/ScrollToTop/ScrollToTop.tsx
Normal 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
|
19
src/components/common/VideoPlayer/VideoPlayer.tsx
Normal 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;
|
@ -1,18 +1,16 @@
|
||||
@import '../../../styles/utilities';
|
||||
@import "../../../styles/utilities";
|
||||
|
||||
.viewAll {
|
||||
display: flex;
|
||||
color: theme("colors.primary");
|
||||
.content {
|
||||
color: var(--primary);
|
||||
margin: 0.8rem 0.8rem 0.8rem 1.6rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.vector {
|
||||
margin: 0.8rem 0rem 0.8rem 0rem;
|
||||
svg path {
|
||||
fill: theme("colors.primary");
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|