🔀 merge: Merge branch 'common' of https://github.com/KieIO/grocery-vercel-commerce into m1-tan

:%s
This commit is contained in:
lytrankieio123
2021-08-26 18:41:08 +07:00
103 changed files with 18153 additions and 3718 deletions

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,26 @@
display: flex;
justify-content: center;
align-items: center;
padding: 1.6rem 3.2rem;
padding: 1.2rem 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 +38,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 +48,38 @@
}
}
}
&.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(--primary);
}
}
}
&.onlyIcon {
padding: 0.8rem;
.icon {
margin: 0;
}
}
&.large {
padding: 3.2rem 4.8rem;
padding: 1.6rem 4.8rem;
&.onlyIcon {
padding: 1.6rem;
}
&.loading {
&::before {
width: 2.4rem;
@@ -58,6 +88,8 @@
}
}
&.preserve {
flex-direction: row-reverse;
.icon {
@@ -67,6 +99,10 @@
.icon {
margin: 0 1.6rem 0 0;
}
.label,
.icon {
svg path {
fill: currentColor;
}

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,5 @@
.navigation_wrapper{
@apply relative;
min-height: theme("caroucel.arrow-height") ;
}

View File

@@ -0,0 +1,57 @@
import { useKeenSlider } from 'keen-slider/react'
import React from 'react'
import 'keen-slider/keen-slider.min.css'
import { CustomCarouselArrow } from './CustomArrow/CustomCarouselArrow';
import s from "./CaroucelCommon.module.scss"
interface CarouselCommonProps {
children?: React.ReactNode
data?: any[]
Component: React.ComponentType<any>
isArrow?:Boolean
itemKey:String
}
const CarouselCommon = ({ data, Component,itemKey }: CarouselCommonProps) => {
const [currentSlide, setCurrentSlide] = React.useState(0)
const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({
slidesPerView: 1,
initial: 0,
slideChanged(s) {
setCurrentSlide(s.details().relativeSlide)
},
})
const handleRightArrowClick = () => {
slider.next()
}
const handleLeftArrowClick = () => {
slider.prev()
}
return (
<div className={s.navigation_wrapper}>
<div ref={sliderRef} className="keen-slider">
{data?.map((props,index) => (
<div className="keen-slider__slide" key={`${itemKey}-${index}`}>
<Component {...props} />
</div>
))}
</div>
{slider && (
<>
<CustomCarouselArrow
side="right"
onClick={handleRightArrowClick}
isDisabled={currentSlide === slider.details().size - 1}
/>
<CustomCarouselArrow
side="left"
onClick={handleLeftArrowClick}
isDisabled={currentSlide === 0}
/>
</>
)}
</div>
)
}
export default CarouselCommon

View File

@@ -0,0 +1,17 @@
.custom_arrow{
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;
&.left{
@apply left-0;
}
&.right{
@apply right-0;
}
&.isDisabled{
@apply hidden ;
}
}

View File

@@ -0,0 +1,24 @@
import classNames from 'classnames'
import React from 'react'
import ArrowLeft from 'src/components/icons/ArrowLeft'
import ArrowRight from 'src/components/icons/ArrowRight'
import s from "./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(`${s.custom_arrow}`, { [`${s[side]}`]: side,[`${s.isDisabled}`]:isDisabled })}
>
{side==='left'?(<ArrowLeft/>):(<ArrowRight/>)}
</button>
)
}

View File

@@ -0,0 +1,3 @@
.subtitle {
margin-top: .4rem;
}

View File

@@ -0,0 +1,22 @@
import React from 'react'
import s from './CollectionHeading.module.scss'
import HeadingCommon from '../HeadingCommon/HeadingCommon'
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,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,22 +1,16 @@
@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;
}
.logo {
@apply font-logo;
.menu {
padding-left: 3.2rem;
padding-right: 3.2rem;
}
}

View File

@@ -1,4 +1,10 @@
import { FC } from 'react'
import classNames from 'classnames'
import React, { memo, useEffect, useState } from 'react'
import { isMobile } from 'src/utils/funtion.utils'
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 {
@@ -6,14 +12,37 @@ interface Props {
children?: any
}
const Header: FC<Props> = ({ }: Props) => {
const Header = memo(({ }: Props) => {
const [isFullHeader, setIsFullHeader] = useState<boolean>(true)
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} />
<HeaderSubMenu isShow={isFullHeader} />
</div>
</header>
<HeaderSubMenuMobile />
</>
)
}
})
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,69 @@
import classNames from 'classnames'
import Link from 'next/link'
import { memo } 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'
const OPTION_MENU = [
{
link: ROUTE.ACCOUNT,
name: 'Account',
},
{
link: '/',
name: 'Logout',
},
]
interface Props {
children?: any,
isFull: boolean,
}
const HeaderMenu = memo(({ isFull }: Props) => {
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={OPTION_MENU} 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,50 @@
@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);
.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,15 @@
@import '../../../styles/utilities';
.headingCommon {
@apply heading-1 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 (
<h1 className={classNames(s.headingCommon, {
[s[type]]: type,
[s[align]]: align
})}
>{children}</h1>
)
}
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,70 @@
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',
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) {
console.log("on enter***")
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

@@ -0,0 +1,90 @@
@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: .4rem;
li {
@apply block w-full transition-all duration-200 cursor-pointer text-active;
padding: 0.8rem 1.6rem;
&:hover {
@apply bg-primary-lightest;
color: var(--primary);
}
a {
@apply block;
}
}
}
}
}
@keyframes menuDropdownAnimation {
0% {
opacity: 0;
transform: translateY(1.6rem);
}
100% {
opacity: 1;
transform: none;
}
}

View File

@@ -0,0 +1,42 @@
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 }[],
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}>
<Link href={item.link}>
<a >
{item.name}
</a>
</Link>
</li>)
}
</ul>
</section>
</div>
);
};
export default MenuDropdown;

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,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,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,6 +1,21 @@
export { default as ButtonCommon } from './ButtonCommon/ButtonCommon'
export { default as Layout } from './Layout/Layout'
export { default as CarouselCommon } from './CarouselCommon/CarouselCommon'
export { default as QuanittyInput } from './QuanittyInput/QuanittyInput'
export { default as LabelCommon } from './LabelCommon/LabelCommon'
export { default as Head } from './Head/Head'
export { default as ViewAllItem} from './ViewAllItem/ViewAllItem'
export { default as ItemWishList} from './ItemWishList/ItemWishList'
export { default as Logo} from './Logo/Logo'
export { default as Logo} from './Logo/Logo'
export { default as Inputcommon} from './InputCommon/InputCommon'
export { default as HeadingCommon } from './HeadingCommon/HeadingCommon'
export { default as CollectionHeading } from './CollectionHeading/CollectionHeading'
export { default as ScrollToTop } from './ScrollToTop/ScrollToTop'
export { default as ScrollTarget } from './ScrollToTop/ScrollTarget'
export { default as InputSearch} from './InputSearch/InputSearch'
export { default as ButtonIconBuy} from './ButtonIconBuy/ButtonIconBuy'
export { default as Banner} from './Banner/Banner'
export { default as Footer} from './Footer/Footer'
export { default as MenuDropdown} from './MenuDropdown/MenuDropdown'
export { default as NotiMessage} from './NotiMessage/NotiMessage'
export { default as VideoPlayer} from './VideoPlayer/VideoPlayer'