Merge branch 'common' into m5-sonnguyen

This commit is contained in:
sonnguyenkieio
2021-09-09 15:53:14 +07:00
committed by GitHub
318 changed files with 3478 additions and 5979 deletions

View File

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

@@ -1,47 +1,24 @@
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'
import CarouselCommon from '../CarouselCommon/CarouselCommon'
import BannerItem, { BannerItemProps } from './BannerItem/BannerItem'
interface Props {
imgLink: string,
title: string,
subtitle: string,
buttonLabel?: string,
linkButton?: string,
size?: 'small' | 'large',
data: BannerItemProps[],
}
const Banner = memo(({ imgLink, title, subtitle, buttonLabel = LANGUAGE.BUTTON_LABEL.SHOP_NOW, linkButton = ROUTE.HOME, size = 'large' }: Props) => {
const option = {
slidesPerView: 1,
breakpoints: {}
}
const Banner = memo(({ data }: 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>
<CarouselCommon<BannerItemProps>
data={data}
itemKey="banner"
Component={BannerItem}
option={option}
isDot = {true}
/>
)
})

View File

@@ -0,0 +1,52 @@
@import "../../../../styles/utilities";
.bannerItem {
@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 './BannerItem.module.scss'
export interface BannerItemProps {
imgLink: string,
title: string,
subtitle: string,
buttonLabel?: string,
linkButton?: string,
size?: 'small' | 'large',
}
const BannerItem = memo(({ imgLink, title, subtitle, buttonLabel = LANGUAGE.BUTTON_LABEL.SHOP_NOW, linkButton = ROUTE.HOME, size = 'large' }: BannerItemProps) => {
return (
<div className={classNames({
[s.bannerItem]: 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 BannerItem

View File

@@ -1,6 +1,11 @@
@import '../../../styles/utilities';
.breadcrumbCommon {
@apply spacing-horizontal-left;
color: var(--text-base);
}
<<<<<<< HEAD
=======
.currentItem {
cursor: default;
}
>>>>>>> a9f9f06eb9dee2a1ddefe907ff804237a78c5210
}

View File

@@ -1,64 +1,37 @@
import React from 'react'
import { ROUTE } from 'src/utils/constanst.utils'
import s from './BreadcrumbCommon.module.scss'
import BreadcrumbItem from './components/BreadcrumbItem/BreadcrumbItem'
import BreadcrumbSeparator from './components/BreadcrumbSeparator/BreadcrumbSeparator'
interface BreadcrumbCommonProps {
crumbs: { link:string, name:string }[];
crumbs: { link: string, name: string }[];
showHomePage?: boolean;
}
const BreadcrumbCommon = ({ crumbs, showHomePage=true } : BreadcrumbCommonProps) => {
const BreadcrumbCommon = ({ crumbs, showHomePage = true }: BreadcrumbCommonProps) => {
return (
<section className={s.breadcrumbCommon}>
{
showHomePage && crumbs[0].link==="/" && crumbs.map((crumb, i) => {
if (i === 0) {
return (
<BreadcrumbItem key={crumb.name} text={crumb.name} href={crumb.link} />
)
}
if (i === crumbs.length-1) {
return (
<BreadcrumbSeparator key={crumb.name}>
<span>{crumb.name}</span>
</BreadcrumbSeparator>
)
}
return (
<BreadcrumbSeparator key={crumb.name}>
<BreadcrumbItem text={crumb.name} href={crumb.link} />
</BreadcrumbSeparator>
)
})
}
{
!showHomePage && crumbs.map((crumb, i) => {
if (i === 0) {
return
}
if (i === 1) {
return (
<BreadcrumbItem key={crumb.name} text={crumb.name} href={crumb.link} />
)
}
if (i === crumbs.length-1) {
return (
<BreadcrumbSeparator key={crumb.name}>
<span>{crumb.name}</span>
</BreadcrumbSeparator>
)
}
return (
<BreadcrumbSeparator key={crumb.name}>
<BreadcrumbItem text={crumb.name} href={crumb.link} />
</BreadcrumbSeparator>
)
})
showHomePage && <BreadcrumbItem key='Home' text='Home' href={ROUTE.HOME} />
}
</section>
{
crumbs.length > 0 && <>
{
crumbs.slice(0, crumbs.length - 1).map((crumb) => (
< BreadcrumbSeparator key={crumb.name}>
<BreadcrumbItem text={crumb.name} href={crumb.link} />
</BreadcrumbSeparator>
))}
< BreadcrumbSeparator>
<span className={s.currentItem}>{crumbs[crumbs.length - 1].name}</span>
</BreadcrumbSeparator>
</>
}
</section >
)
}

View File

@@ -0,0 +1,5 @@
.breadcrumbItem {
&:hover {
color: var(--primary);
}
}

View File

@@ -1,5 +1,6 @@
import React from 'react'
import Link from 'next/link'
import s from './BreadcrumbItem.module.scss'
interface BreadcrumbItemProps {
text: string;
@@ -9,7 +10,7 @@ interface BreadcrumbItemProps {
const BreadcrumbItem = ({ text, href }: BreadcrumbItemProps) => {
return (
<Link href={href}>
<a>{text}</a>
<a className={s.breadcrumbItem}>{text}</a>
</Link>
)
}

View File

@@ -1,7 +1,7 @@
import React from 'react'
interface BreadcrumbSeparatorProps {
children?: React.ReactNode;
children?: React.ReactNode
}
const BreadcrumbSeparator = ({ children }: BreadcrumbSeparatorProps) => {

View File

@@ -7,6 +7,9 @@
align-items: center;
padding: 1rem 2rem;
@screen md {
padding: 0.8rem 1.6rem;
}
@screen lg {
padding: 0.8rem 3.2rem;
}
&:disabled {
@@ -84,11 +87,14 @@
padding: 1rem;
}
@screen md {
padding: 1.6rem 4.8rem;
padding: 1.6rem 3.2rem;
&.onlyIcon {
padding: 1.6rem;
}
}
@screen lg {
padding: 1.6rem 4.8rem;
}
&.loading {
&::before {
width: 2.4rem;

View File

@@ -0,0 +1,34 @@
@import "../../../styles/utilities";
.cardBlogWarpper {
@apply inline-flex flex-col justify-start;
max-width: 39.2rem;
min-height: 34.4rem;
.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 ;
color: var(--text-label);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
}

View File

@@ -0,0 +1,31 @@
import Link from 'next/link'
import React from 'react'
import { ROUTE } from 'src/utils/constanst.utils'
import { BlogProps } from 'src/utils/types.utils'
import s from './CardBlog.module.scss'
export interface BlogCardProps extends BlogProps {
// todo: edit when intergrate API
}
const CardBlog = ({ imageSrc, title, description, slug }: BlogCardProps) => {
return (
<div className={s.cardBlogWarpper}>
<Link href={`${ROUTE.BLOG_DETAIL}/${slug}`}>
<a>
<div className={s.image}>
<img src={imageSrc} alt="image cardblog" />
</div>
</a>
</Link>
<Link href={`${ROUTE.BLOG_DETAIL}/${slug}`}>
<a>
<div className={s.title}>{title}</div>
</a>
</Link>
<div className={s.description}>{description}</div>
</div>
)
}
export default CardBlog

View File

@@ -8,15 +8,22 @@
:global(.customArrow) {
width: 64px;
height: 64px;
border-radius: .8rem;
&: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;
@apply hidden left-0;
@screen md {
@apply flex
}
}
&:global(.rightArrow) {
@apply right-0;
@apply hidden right-0;
@screen md {
@apply flex;
}
}
&:global(.isDisabledArrow) {
@apply hidden;

View File

@@ -28,7 +28,6 @@ const CarouselCommon = <T,>({
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,

View File

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

View File

@@ -0,0 +1,12 @@
@import '../../../styles/utilities';
.cartDrawer {
@apply flex flex-col h-full;
.body {
@apply overflow-y-auto overflow-x-hidden h-full custom-scroll;
}
.bottom {
padding-top: 1.6rem;
}
}

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { PRODUCT_CART_DATA_TEST } from 'src/utils/demo-data';
import { DrawerCommon } from '..';
import s from './CartDrawer.module.scss';
import CartCheckoutButton from './components/CartCheckoutButton/CartCheckoutButton';
import CartMessage from './components/CartMessage/CartMessage';
import CartRecommendation from './components/CartRecommendation/CartRecommendation';
import ProductsInCart from './components/ProductsInCart/ProductsInCart';
interface Props {
visible: boolean
onClose: () => void
}
const CartDrawer = ({ visible, onClose }: Props) => {
return (
<DrawerCommon
title={`Your cart (${PRODUCT_CART_DATA_TEST.length})`}
visible={visible}
onClose={onClose}>
<div className={s.cartDrawer}>
<div className={s.body}>
<ProductsInCart data={PRODUCT_CART_DATA_TEST}/>
<CartRecommendation />
</div>
<div className={s.bottom}>
<CartMessage />
<CartCheckoutButton />
</div>
</div>
</DrawerCommon>
)
}
export default CartDrawer;

View File

@@ -0,0 +1,6 @@
.cartCheckoutButton {
padding: 1.6rem;
button {
width: 100%;
}
}

View File

@@ -0,0 +1,13 @@
import React, { memo } from 'react';
import { ButtonCommon } from 'src/components/common';
import s from './CartCheckoutButton.module.scss';
const CartCheckoutButton = memo(() => {
return (
<div className={s.cartCheckoutButton}>
<ButtonCommon size='large'>Check out - Rp 120.500</ButtonCommon>
</div>
)
})
export default CartCheckoutButton;

View File

@@ -0,0 +1,16 @@
@import "../../../../../styles/utilities";
.cartMessage {
@apply flex bg-info;
padding: 1.2rem 1.6rem;
.text {
color: var(--white);
font-weight: bold;
margin-right: 0.8rem;
}
.icon {
svg path {
fill: var(--text-placeholder);
}
}
}

View File

@@ -0,0 +1,18 @@
import React, { memo } from 'react';
import { IconInfo } from 'src/components/icons';
import s from './CartMessage.module.scss';
const CartMessage = memo(() => {
return (
<div className={s.cartMessage}>
<div className={s.text}>
You save - Rp 150
</div>
<div className={s.icon}>
<IconInfo />
</div>
</div>
)
})
export default CartMessage;

View File

@@ -0,0 +1,26 @@
@import '../../../../../styles/utilities';
.cartRecommendation {
@apply w-full bg-background-gray;
.top {
@apply flex justify-between items-center;
padding: 1.6rem;
.heading {
@apply font-bold text-active sm-headline;
}
}
.productCardWarpper {
padding-left: 1.6rem;
:global(.customArrow) {
@apply bg-line;
@screen lg {
&:global(.leftArrow) {
left: calc(-6.4rem - 2rem);
}
&:global(.rightArrow) {
right: calc(-6.4rem - 2rem);
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
import { TOptionsEvents } from 'keen-slider';
import React from 'react';
import { CarouselCommon, ViewAllItem } from 'src/components/common';
import ProductCard, { ProductCardProps } from 'src/components/common/ProductCard/ProductCard';
import { ROUTE } from 'src/utils/constanst.utils';
import { PRODUCT_DATA_TEST } from 'src/utils/demo-data';
import s from './CartRecommendation.module.scss';
const option: TOptionsEvents = {
slidesPerView: 2,
mode: 'free',
breakpoints: {
'(min-width: 640px)': {
slidesPerView: 1,
},
'(min-width: 768px)': {
slidesPerView: 2.5,
}
},
}
const CartRecommendation = () => {
return (
<div className={s.cartRecommendation}>
<div className={s.top}>
<div className={s.heading}>
Recommendation
</div>
<ViewAllItem link={ROUTE.PRODUCTS} />
</div>
<div className={s.productCardWarpper}>
<CarouselCommon<ProductCardProps>
data={PRODUCT_DATA_TEST}
Component={ProductCard}
itemKey="cart-recommendation"
option={option}
/>
</div>
</div>
)
}
export default CartRecommendation;

View File

@@ -0,0 +1,54 @@
@import "../../../../../styles/utilities";
.productCartItem {
@apply grid;
grid-template-columns: 2fr 1fr;
padding-bottom: 1.6rem;
padding-top: 1.6rem;
.info {
@apply flex;
.imgWrap {
width: 11rem;
height: 7.5rem;
margin-right: 1.6rem;
img {
object-fit: contain;
}
}
.detail {
min-height: 9rem;
.name {
&:hover {
color: var(--primary);
}
}
.price {
margin-top: 0.8rem;
.old {
margin-bottom: 0.8rem;
.number {
margin-right: 0.8rem;
color: var(--text-label);
text-decoration: line-through;
}
}
.current {
@apply text-active font-bold sm-headline;
}
}
}
}
.actions {
@apply flex flex-col justify-between items-end;
margin-left: 1.6rem;
.iconDelete {
@apply cursor-pointer;
&:hover {
svg path {
fill: var(--negative);
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import Link from 'next/link'
import { QuanittyInput } from 'src/components/common';
import { IconDelete } from 'src/components/icons';
import { ROUTE } from 'src/utils/constanst.utils';
import { ProductProps } from 'src/utils/types.utils';
import ImgWithLink from '../../../ImgWithLink/ImgWithLink';
import LabelCommon from '../../../LabelCommon/LabelCommon';
import s from './ProductCartItem.module.scss';
export interface ProductCartItempProps extends ProductProps {
quantity: number,
}
const ProductCartItem = ({ name, slug, weight, price, oldPrice, discount, imageSrc, quantity }: ProductCartItempProps) => {
return (
<div className={s.productCartItem}>
<div className={s.info}>
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
<a href="">
<div className={s.imgWrap}>
<ImgWithLink src={imageSrc} alt={name} />
</div>
</a>
</Link>
<div className={s.detail}>
<Link href={`${ROUTE.PRODUCT_DETAIL}/${slug}`}>
<a>
<div className={s.name}>
{name} {weight ? `(${weight})` : ''}
</div>
</a>
</Link>
<div className={s.price}>
{
oldPrice &&
<div className={s.old}>
<span className={s.number}>{oldPrice}</span>
<LabelCommon type='discount'>{discount}</LabelCommon>
</div>
}
<div className={s.current}>{price}</div>
</div>
</div>
</div>
<div className={s.actions}>
<div className={s.iconDelete}>
<IconDelete />
</div>
<QuanittyInput size='small' initValue={quantity} />
</div>
</div>
)
}
export default ProductCartItem;

View File

@@ -0,0 +1,9 @@
.productsInCart {
padding: 1.6rem 1.6rem 0 1.6rem;
list-style: none;
li {
&:not(:last-child) {
border-bottom: 1px solid var(--border-line);
}
}
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import ProductCartItem, { ProductCartItempProps } from '../ProductCartItem/ProductCartItem';
import s from './ProductsInCart.module.scss';
interface Props {
data: ProductCartItempProps[]
}
const ProductsInCart = ({ data }: Props) => {
return (
<ul className={s.productsInCart}>
{
data.map(item => <li key={item.name}>
<ProductCartItem
name={item.name}
slug={item.slug}
weight={item.weight}
price={item.price}
oldPrice={item.oldPrice}
discount={item.discount}
imageSrc={item.imageSrc}
quantity={item.quantity}
/>
</li>)
}
</ul>
)
}
export default ProductsInCart;

View File

@@ -0,0 +1,66 @@
@import "../../../../styles/utilities";
.collapseWrapper {
@apply border-t border-b;
border-color: var(--border-line);
max-width: 80.4rem;
min-height: 4rem;
&.isActive {
.title {
@apply pb-0;
}
.contentContainer {
@apply block;
animation: ContentAnimationIn 0.5s ease-out;
}
.toggle {
&:before {
transform: rotate(180deg);
}
&:after {
transform: rotate(180deg);
}
}
}
&:hover {
cursor: pointer;
}
}
.title {
@apply outline-none flex justify-between font-heading items-center pt-16 pb-16;
font-size: 3.2rem;
line-height: 4rem;
letter-spacing: -0.01em;
.toggle {
height: 2.2rem;
width: 2.2rem;
position: relative;
&:before,
&:after {
@apply absolute h-2;
content: "";
border-radius: 0.8rem;
background: var(--text-active);
top: 40%;
width: 2.2rem;
transition: transform 500ms ease;
}
&:before {
transform-origin: center;
transform: rotate(90deg);
}
}
}
.contentContainer {
@apply hidden pb-16;
}
@keyframes ContentAnimationIn {
0% {
opacity: 0;
transform: translateY(-1.6rem);
}
100% {
opacity: 1;
transform: none;
}
}

View File

@@ -0,0 +1,37 @@
import s from './CollapseChild.module.scss'
import { useState } from 'react'
import classNames from 'classnames'
import CollapseContent from './CollapseContent/CollapseContent'
interface CollapseProps{
title?: string,
content: Array<string>,
isToggle?: boolean,
}
const CollapseChild = ({title, content, isToggle=false}: CollapseProps) => {
const [isActive, changeActive] = useState(isToggle)
const handleToggle = () => {
changeActive(!isActive)
}
return(
<div className={classNames({
[s.collapseWrapper] : true,
[s.isActive] : isActive
})}
onClick = { handleToggle }
>
<div className={s.title}>
<h4>{title}</h4>
<div className={s.toggle}></div>
</div>
<div className={s.contentContainer}>
{
content.map(item => <CollapseContent key={item} content={item} />)
}
</div>
</div>
)
}
export default CollapseChild

View File

@@ -0,0 +1,3 @@
.content {
margin-top: 1.6rem;
}

View File

@@ -0,0 +1,15 @@
import s from './CollapseContent.module.scss'
interface CollapseContentProps{
content: string
}
const CollapseContent = ({content}: CollapseContentProps) => {
return (
<div className={s.content}>
{content}
</div>
)
}
export default CollapseContent

View File

@@ -0,0 +1,19 @@
import CollapseChild from './CollapseChild/CollapseChild'
interface CollapseCommonProps{
data: {title: string, content: Array<string>}[],
}
const CollapseCommon = ({data}: CollapseCommonProps) => {
return (
<section>
{
data.map(item =>
<CollapseChild key={item.title} title={item.title} content={item.content}/>
)
}
</section>
)
}
export default CollapseCommon

View File

@@ -0,0 +1,52 @@
@import "../../../styles/utilities";
.drawerCommon {
@apply fixed flex justify-end transition-all duration-200;
overflow: hidden;
top: 0;
right: 0;
height: 100vh;
width: 90%;
box-shadow: -3px 0 10px rgba(0, 0, 0, 0.15);
z-index: 20000;
.inner {
@apply flex flex-col bg-white;
width: fit-content;
height: 100vh;
width: 100%;
margin-right: 0;
@screen md {
max-width: 52rem;
}
.top {
@apply flex justify-between items-center;
padding: 1.6rem;
.heading {
@apply sm-headline;
}
.iconClose {
@apply cursor-pointer transition-all duration-200;
&:hover {
svg path {
fill: var(--primary);
}
}
}
}
}
.content {
overflow-y: auto;
height: 100%;
}
&.hide {
transform: translateX(110%);
}
@screen md {
width: unset;
.inner {
min-width: 48rem;
}
}
}

View File

@@ -0,0 +1,36 @@
import React, { useRef } from 'react';
import s from './DrawerCommon.module.scss';
import classNames from 'classnames';
import { IconClose } from 'src/components/icons';
interface Props {
visible: boolean
title?: string
children?: React.ReactNode
onClose: () => void
}
const DrawerCommon = ({ title, visible, children, onClose }: Props) => {
return (
<div className={classNames({
[s.drawerCommon]: true,
[s.hide]: !visible
})}>
<div className={s.inner}>
<div className={s.top}>
<h4 className={s.heading}>
{title}
</h4>
<div className={s.iconClose} onClick={onClose}>
<IconClose />
</div>
</div>
<div className={s.content}>
{children}
</div>
</div>
</div>
)
}
export default DrawerCommon;

View File

@@ -1,8 +1,9 @@
import classNames from 'classnames'
import React, { memo, useEffect, useState } from 'react'
import { useModalCommon } from 'src/components/hooks/useModalCommon'
import { useModalCommon } from 'src/components/hooks'
import { isMobile } from 'src/utils/funtion.utils'
import ModalAuthenticate from '../ModalAuthenticate/ModalAuthenticate'
import ModalCreateUserInfo from '../ModalCreateUserInfo/ModalCreateUserInfo'
import HeaderHighLight from './components/HeaderHighLight/HeaderHighLight'
import HeaderMenu from './components/HeaderMenu/HeaderMenu'
import HeaderSubMenu from './components/HeaderSubMenu/HeaderSubMenu'
@@ -13,6 +14,7 @@ import s from './Header.module.scss'
const Header = memo(() => {
const [isFullHeader, setIsFullHeader] = useState<boolean>(true)
const { visible: visibleModalAuthen, closeModal: closeModalAuthen, openModal: openModalAuthen } = useModalCommon({ initialValue: false })
const { visible: visibleModalInfo, closeModal: closeModalInfo, openModal: openModalInfo } = useModalCommon({ initialValue: false })
useEffect(() => {
window.addEventListener('scroll', handleScroll)
@@ -35,12 +37,15 @@ const Header = memo(() => {
<header className={classNames({ [s.header]: true, [s.full]: isFullHeader })}>
<HeaderHighLight isShow={isFullHeader} />
<div className={s.menu}>
<HeaderMenu isFull={isFullHeader} openModalAuthen={openModalAuthen} />
<HeaderMenu isFull={isFullHeader}
openModalAuthen={openModalAuthen}
openModalInfo={openModalInfo} />
<HeaderSubMenu isShow={isFullHeader} />
</div>
</header>
<HeaderSubMenuMobile />
<ModalAuthenticate visible={visibleModalAuthen} closeModal={closeModalAuthen} />
<ModalCreateUserInfo demoVisible={visibleModalInfo} demoCloseModal={closeModalInfo}/>
</>
)
})

View File

@@ -42,7 +42,7 @@
@apply hidden;
@screen md {
@apply flex items-center list-none;
li {
> li {
@apply flex justify-center items-center w-full;
&:not(:last-child) {
margin-right: 4.8rem;
@@ -52,13 +52,23 @@
}
a {
@appy no-underline;
&.iconFovourite {
&:hover {
opacity: 0.8;
}
&.iconFavourite {
svg path {
fill: var(--negative);
}
}
}
.btnCart {
&:hover {
svg path {
fill: var(--primary);
opacity: 0.8;
}
}
}
}
}
}

View File

@@ -5,20 +5,26 @@ 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 Logo from '../../../Logo/Logo'
import s from './HeaderMenu.module.scss'
interface Props {
children?: any,
isFull: boolean,
openModalAuthen: () => void,
openModalInfo: () => void,
}
const HeaderMenu = memo(({ isFull, openModalAuthen }: Props) => {
const HeaderMenu = memo(({ isFull, openModalAuthen, openModalInfo }: Props) => {
const optionMenu = useMemo(() => [
{
onClick: openModalAuthen,
name: 'Login (Demo)',
},
{
onClick: openModalInfo,
name: 'Create User Info (Demo)',
},
{
link: ROUTE.ACCOUNT,
name: 'Account',
@@ -34,7 +40,7 @@ const HeaderMenu = memo(({ isFull, openModalAuthen }: Props) => {
<section className={classNames({ [s.headerMenu]: true, [s.full]: isFull })}>
<div className={s.left}>
<div className={s.top}>
<div>Online Grocery</div>
<Logo/>
<button className={s.iconCart}>
<IconBuy />
</button>
@@ -46,14 +52,14 @@ const HeaderMenu = memo(({ isFull, openModalAuthen }: Props) => {
<ul className={s.menu}>
<li>
<Link href={`${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.ORDER}`}>
<a >
<a>
<IconHistory />
</a>
</Link>
</li>
<li>
<Link href={`${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.FAVOURITE}`}>
<a className={s.iconFovourite}>
<a className={s.iconFavourite}>
<IconHeart />
</a>
</Link>
@@ -62,7 +68,7 @@ const HeaderMenu = memo(({ isFull, openModalAuthen }: Props) => {
<MenuDropdown options={optionMenu} isHasArrow={false}><IconUser /></MenuDropdown>
</li>
<li>
<button>
<button className={s.btnCart}>
<IconBuy />
</button>
</li>

View File

@@ -0,0 +1,9 @@
.imgWithLink {
position: relative;
min-width: 5rem;
width: 100%;
height: 100%;
img {
object-fit: cover;
}
}

View File

@@ -0,0 +1,18 @@
import React from 'react'
import s from './ImgWithLink.module.scss'
import Image from 'next/image'
export interface ImgWithLinkProps {
src: string,
alt?: string,
}
const ImgWithLink = ({ src, alt }: ImgWithLinkProps) => {
return (
<div className={s.imgWithLink}>
<Image src={src} alt={alt} layout="fill" className={s.imgWithLink} />
</div>
)
}
export default ImgWithLink

View File

@@ -1,51 +1,94 @@
@import "../../../styles/utilities";
.inputWrap {
@apply flex items-center relative;
.icon {
@apply absolute;
content: "";
left: 1.6rem;
margin-right: 1.6rem;
svg path {
fill: currentColor;
.inputInner {
@apply flex items-center relative;
.icon {
@apply absolute flex justify-center items-center;
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;
.icon + .inputCommon {
padding-left: 4.8rem;
}
&::placeholder {
@apply text-label;
}
&.custom {
@apply custom-border-radius;
border: 1px solid transparent;
background: var(--gray);
.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);
}
}
}
&.bgTransparent {
background: rgb(227, 242, 233, 0.3);
color: var(--white);
&::placeholder {
color: var(--white);
&.preserve {
@apply flex-row-reverse;
.icon {
left: unset;
right: 1.6rem;
margin-left: 1.6rem;
margin-right: 0;
svg path {
fill: var(--text-label);
}
}
.icon + .inputCommon {
padding-left: 1.6rem;
padding-right: 4.8rem;
}
}
&.success {
.icon {
svg path {
fill: var(--primary);
}
}
}
&.error {
.icon {
svg path {
fill: var(--negative);
}
}
input {
border-color: var(--negative) !important;
}
}
}
.errorMessage {
@apply caption;
color: var(--negative);
margin-top: 0.4rem;
}
}

View File

@@ -1,5 +1,6 @@
import classNames from 'classnames';
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import React, { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { IconCheck, IconError, IconPassword, IconPasswordCross } from 'src/components/icons';
import { KEY } from 'src/utils/constanst.utils';
import s from './InputCommon.module.scss';
@@ -14,14 +15,29 @@ interface Props {
styleType?: 'default' | 'custom',
backgroundTransparent?: boolean,
icon?: React.ReactNode,
isIconSuffix?: boolean,
isShowIconSuccess?: boolean,
error?: string,
onChange?: (value: string | number) => void,
onEnter?: (value: string | number) => void,
}
const InputCommon = forwardRef<Ref, Props>(({ value, placeholder, type, styleType = 'default', icon, backgroundTransparent = false,
isIconSuffix, isShowIconSuccess, error,
onChange, onEnter }: Props, ref) => {
const inputElementRef = useRef<HTMLInputElement>(null);
const iconElement = useMemo(() => {
if (error) {
return <span className={s.icon}><IconError /> </span>
} else if (isShowIconSuccess) {
return <span className={s.icon}><IconCheck /> </span>
} else if (icon) {
return <span className={s.icon}>{icon} </span>
}
return <></>
}, [icon, error, isShowIconSuccess])
useImperativeHandle(ref, () => ({
focus: () => {
inputElementRef.current?.focus();
@@ -44,23 +60,33 @@ const InputCommon = forwardRef<Ref, Props>(({ value, placeholder, type, styleTyp
}
return (
<div className={s.inputWrap}>
<div className={classNames({
[s.inputWrap]: true,
})}>
<div className={classNames({
[s.inputInner]: true,
[s.preserve]: isIconSuffix,
[s.success]: isShowIconSuccess,
[s.error]: !!error,
})}>
{iconElement}
<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>
{
icon && <span className={s.icon}>{icon}</span>
error && <div className={s.errorMessage}>{error}</div>
}
<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>
)

View File

@@ -0,0 +1,10 @@
.iconPassword {
all: unset;
cursor: pointer;
&:focus {
outline: none;
}
&:focus-visible {
outline: 2px solid var(--text-active);
}
}

View File

@@ -0,0 +1,40 @@
import React, { useState } from 'react';
import { IconPassword, IconPasswordCross } from 'src/components/icons';
import { Inputcommon } from '..';
import s from './InputPassword.module.scss';
interface Props {
children?: React.ReactNode,
value?: string | number,
placeholder?: string,
styleType?: 'default' | 'custom',
error?: string,
onChange?: (value: string | number) => void,
onEnter?: (value: string | number) => void,
}
const InputPassword = ({ value, placeholder, styleType = 'default', error,
onChange, onEnter }: Props) => {
const [isShowPassword, setIsShowPassword] = useState<boolean>(false)
const toggleShowPassword = () => {
setIsShowPassword(!isShowPassword)
}
return (
<Inputcommon
value={value}
type={isShowPassword ? 'text' : 'password'}
styleType={styleType}
error={error}
placeholder={placeholder}
icon={<button className={s.iconPassword} onClick={toggleShowPassword}>
{isShowPassword ? <IconPassword /> : <IconPasswordCross />}
</button>}
isIconSuffix={true}
onChange={onChange}
onEnter={onEnter}
/>
)
}
export default InputPassword

View File

@@ -1,6 +1,8 @@
import { CommerceProvider } from '@framework'
import { useRouter } from 'next/router'
import { FC } from 'react'
import { useModalCommon } from 'src/components/hooks'
import { CartDrawer } from '..'
import Footer from '../Footer/Footer'
import Header from '../Header/Header'
import s from './Layout.module.scss'
@@ -13,12 +15,24 @@ interface Props {
// note: demo code
const Layout: FC<Props> = ({ children }) => {
const { locale = 'en-US' } = useRouter()
const { visible: visibleCartDrawer, openModal, closeModal: closeCartDrawer } = useModalCommon({ initialValue: false })
const toggle = () => {
if (visibleCartDrawer) {
closeCartDrawer()
} else {
openModal()
}
}
return (
<CommerceProvider locale={locale}>
<div className={s.mainLayout}>
<Header />
<main >{children}</main>
<button onClick={toggle}>toggle card: {visibleCartDrawer.toString()}</button>
<CartDrawer
visible={visibleCartDrawer}
onClose={closeCartDrawer} />
<Footer />
</div>
</CommerceProvider>

View File

@@ -0,0 +1,23 @@
@import "../../../../styles/utilities";
.infoProducts {
@apply flex justify-between items-center spacing-horizontal;
.top {
.sub {
display: none;
}
}
@screen lg {
@apply block;
margin-right: 4rem;
padding: 0;
.top {
margin-bottom: 3.2rem;
.sub {
display: block;
margin-top: 0.4rem;
}
}
}
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { ROUTE } from 'src/utils/constanst.utils';
import HeadingCommon from '../../HeadingCommon/HeadingCommon';
import ViewAllItem from '../../ViewAllItem/ViewAllItem';
import s from './InfoProducts.module.scss'
interface Props {
title: string,
subtitle?: string,
}
const InfoProducts = ({ title, subtitle }: Props) => {
return (
<div className={s.infoProducts}>
<div className={s.top}>
<HeadingCommon>{title}</HeadingCommon>
<div className={s.sub}>
{subtitle}
</div>
</div>
<ViewAllItem link={ROUTE.PRODUCTS} />
</div>
);
};
export default InfoProducts;

View File

@@ -0,0 +1,48 @@
@import "../../../styles/utilities";
.listProductWithInfo {
background-color: var(--background);
border-top: 1rem solid var(--gray);
border-bottom: 1rem solid var(--gray);
padding-top: 6rem;
padding-bottom: 6rem;
@screen lg {
@apply flex spacing-horizontal-left;
padding-top: 5.6rem;
padding-bottom: 5.6rem;
border: none;
background-color: var(--background-gray);
}
.productsWrap {
@apply spacing-horizontal-left;
@screen lg {
max-width: 75%;
@apply custom-border-radius-lg bg-white;
padding: 4rem .8rem;
:global(.customArrow) {
@screen lg {
&:global(.leftArrow) {
left: calc(-6.4rem + 3rem);
}
&:global(.rightArrow) {
right: calc(-6.4rem + 3rem);
}
}
}
}
@screen xl {
padding: 4rem 2.4rem;
max-width: 80%;
:global(.customArrow) {
@screen lg {
&:global(.leftArrow) {
left: calc(-6.4rem + 1rem);
}
&:global(.rightArrow) {
right: calc(-6.4rem + 1rem);
}
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
import { TOptionsEvents } from 'keen-slider';
import React from 'react';
import CarouselCommon from '../CarouselCommon/CarouselCommon';
import ProductCard, { ProductCardProps } from '../ProductCard/ProductCard';
import InfoProducts from './InfoProducts/InfoProducts';
import s from './ListProductWithInfo.module.scss';
interface Props {
data: ProductCardProps[],
title: string,
subtitle?: string,
}
const OPTION_DEFAULT: TOptionsEvents = {
slidesPerView: 2,
mode: 'free',
breakpoints: {
'(min-width: 640px)': {
slidesPerView: 3,
},
'(min-width: 768px)': {
slidesPerView: 4,
},
'(min-width: 1024px)': {
slidesPerView: 3,
},
'(min-width: 1280px)': {
slidesPerView: 4.5,
},
},
}
const ListProductWithInfo = ({ data, title, subtitle }: Props) => {
return (
<div className={s.listProductWithInfo}>
<InfoProducts
title={title}
subtitle={subtitle}
/>
<div className={s.productsWrap}>
<CarouselCommon<ProductCardProps>
data={data}
Component={ProductCard}
itemKey={title}
option={OPTION_DEFAULT}
/>
</div>
</div>
);
};
export default ListProductWithInfo;

View File

@@ -1,7 +1,7 @@
@import '../../../styles/utilities';
@import "../../../styles/utilities";
.logo {
display: flex;
@apply flex justify-center items-center;
.eclipse {
width: 3.2rem;
height: 3.2rem;
@@ -10,9 +10,9 @@
margin-right: 1.2rem;
}
.content {
@apply font-logo;
@apply font-logo font-bold;
font-size: 16px;
line-height: 32px;
letter-spacing: -0.02em;
}
}
}

View File

@@ -1,14 +1,18 @@
import s from './Logo.module.scss'
import Link from 'next/link'
import { ROUTE } from 'src/utils/constanst.utils'
const Logo = () => {
return(
<div className={s.logo}>
<div className={s.eclipse}>
</div>
<div className={s.content}>
ONLINE GROCERY
</div>
</div>
return (
<Link href={ROUTE.HOME}>
<a className={s.logo}>
<div className={s.eclipse}>
</div>
<div className={s.content}>
ONLINE GROCERY
</div>
</a>
</Link>
)
}

View File

@@ -17,10 +17,21 @@
}
.label {
all: unset;
@apply flex justify-end items-center transition-all duration-200;
svg path {
width: fit-content;
}
&:focus,
&:active {
color: var(--primary);
svg path {
fill: currentColor;
}
}
&:focus-visible {
outline: 2px solid #000;
}
}
&.arrow {
@@ -63,8 +74,9 @@
@apply rounded list-none bg-white;
border: 1px solid var(--text-active);
margin-top: 0.4rem;
li {
> li {
@apply block w-full transition-all duration-200 cursor-pointer text-active;
white-space: nowrap;
button {
all: unset;
color: currentColor;
@@ -78,7 +90,7 @@
@apply block;
}
&:hover {
@apply bg-primary-lightest;
@apply bg-gray;
color: var(--primary);
}
}

View File

@@ -16,9 +16,9 @@ const MenuDropdown = ({ options, children, isHasArrow = true, align }: Props) =>
[s.menuDropdown]: true,
[s.arrow]: isHasArrow,
})}>
<span className={s.label}>
<button className={s.label}>
{children}
</span>
</button>
<section className={classNames({
[s.menu]: true,
[s.left]: align === 'left',

View File

@@ -0,0 +1,40 @@
@import "../../../styles/utilities";
.menuFilterWrapper{
@screen md {
@apply hidden;
}
.menuFilterHeading{
@apply sub-headline font-bold ;
color: var(--text-active);
font-feature-settings: 'salt' on;
margin: 0.8rem 0;
}
.menuFilterList{
@apply flex flex-wrap justify-start relative;
margin-bottom: 3rem;
box-sizing: border-box;
&::after{
@apply absolute;
top: 110%;
content: "";
width: 100%;
border-bottom: 1px solid var(--border-line);
}
li{
margin: 1rem 0;
padding:0;
div{
padding: 0.8rem 1.6rem;
margin-right: 0.8rem;
background-color: var(--gray);
border-radius: 0.8rem;
&.active {
color:white;
background-color: var(--primary);
}
}
}
}
}

View File

@@ -0,0 +1,48 @@
import classNames from 'classnames'
import { useEffect, useState } from 'react';
import s from './MenuFilter.module.scss'
interface Props {
children?: any,
heading?:string,
categories:{name:string,link:string}[],
type:string,
onChangeValue?: (value: Object) => void
}
const MenuFilter = ({heading,categories,type,onChangeValue}:Props)=> {
const [active, setActive] = useState<string>('');
function handleClick(link:string){
setActive(link);
if(active === link){
setActive('');
}
}
useEffect(()=>{
let href = active?.split("=");
const linkValue = href[1];
onChangeValue && onChangeValue({[type]:linkValue});
},[active])
return (
<section className={s.menuFilterWrapper}>
<h2 className={s.menuFilterHeading}>{heading}</h2>
<ul className={s.menuFilterList}>
{
categories.map(item => <li key={item.name}>
<div onClick={()=> handleClick(item.link)} className={classNames({ [s.active]: item.link === active? true: false })}>
{item.name}
</div>
</li>)
}
</ul>
</section>
)
}
export default MenuFilter

View File

@@ -0,0 +1,29 @@
@import "../../../styles/utilities";
.menuNavigationWrapper{
.menuNavigationHeading{
@screen md {
@apply sub-headline font-bold ;
color: var(--text-active);
font-feature-settings: 'salt' on;
margin: 1.6rem 0;
}
}
.menuNavigationList{
@screen md {
li{
margin: 0.8rem 0;
a{
display:block;
width:100%;
color:var(--text-base);
&:hover {
@apply text-primary;
}
&.active {
@apply text-primary;
}
}
}
}
}
}

View File

@@ -0,0 +1,34 @@
import classNames from 'classnames'
import Link from 'next/link'
import { useRouter } from 'next/router'
import s from './MenuNavigation.module.scss'
interface Props {
children?: any,
heading:string,
categories:{name:string,link:string}[]
}
const MenuNavigation = ({heading,categories}:Props)=> {
const router = useRouter()
return (
<section className={s.menuNavigationWrapper}>
<h2 className={s.menuNavigationHeading}>{heading}({categories.length})</h2>
<ul className={s.menuNavigationList}>
{
categories.map(item => <li key={item.name}
>
<Link href={item.link}>
<a className={classNames({ [s.active]: router.asPath === item.link})}>
{item.name}
</a>
</Link>
</li>)
}
</ul>
</section>
)
}
export default MenuNavigation

View File

@@ -0,0 +1,45 @@
@import "../../../styles/utilities";
.menuNavigationProductListDesktop{
@screen sm-only {
@apply hidden;
}
}
.menuNavigationProductListMobile{
@apply hidden;
&.isShow{
@apply block;
@screen md {
@apply hidden;
}
}
.menuNavigationProductModal{
background: rgba(0, 0, 0, 0.5);
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 10000;
.content{
@apply spacing-horizontal;
margin-top: 3rem;
padding-top: 2rem ;
padding-bottom: 5rem;
background-color: white;
overflow: auto;
height: 100%;
border-radius: 2.4rem 2.4rem 0 0;
.head{
@apply flex justify-between;
h3{
@apply heading-3 font-bold;
color:var(--text-base);
}
}
button{
margin-top: 2rem;
width: 100%;
}
}
}
}

View File

@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import {ButtonCommon} from 'src/components/common';
import s from './MenuNavigationProductList.module.scss';
import MenuSort from './MenuSort/MenuSort';
import {LANGUAGE} from 'src/utils/language.utils';
import classNames from 'classnames'
import MenuFilter from '../MenuFilter/MenuFilter';
import MenuNavigation from '../MenuNavigation/MenuNavigation';
import IconHide from 'src/components/icons/IconHide';
interface Props{
categories:{name:string,link:string}[],
brands:{name:string,link:string}[],
featured:{name:string,link:string}[],
}
const MenuNavigationProductList = ({categories,brands,featured}:Props)=>{
const [dataSort,setDataSort] = useState({});
const [isShow,setIsShow] = useState(true);
function handleValue(value:Object){
setDataSort({...dataSort,...value});
}
function filter(){
console.log(dataSort)
}
function hideMenu(){
if(isShow === true){
setIsShow(false);
}
}
return(
<>
<div className={s.menuNavigationProductListDesktop}>
<MenuNavigation categories={categories} heading="Categories"/>
<MenuNavigation categories={brands} heading="Brands"/>
<MenuNavigation categories={featured} heading="Featured"/>
</div>
<div className={classNames({ [s.menuNavigationProductListMobile] :true,[s.isShow]: isShow})}>
<div className={s.menuNavigationProductModal}>
<div className={s.content}>
<div className={s.head}>
<h3>FILTER</h3>
<div onClick={hideMenu}><IconHide/></div>
</div>
<MenuFilter categories={categories} heading="Categories" type="category" onChangeValue={handleValue}/>
<MenuFilter categories={brands} heading="Brand" type="brand" onChangeValue={handleValue}/>
<MenuFilter categories={featured} heading="Featured" type="featured" onChangeValue={handleValue}/>
<MenuSort heading="SORT BY" type="sort" onChangeValue={handleValue}/>
<ButtonCommon size="large" onClick={filter}>{LANGUAGE.BUTTON_LABEL.CONFIRM}</ButtonCommon>
</div>
</div>
</div>
</>
)
}
export default MenuNavigationProductList

View File

@@ -0,0 +1,46 @@
@import "../../../../styles/utilities";
.menuSortWrapper{
@screen md {
@apply hidden;
}
.menuSortHeading{
@apply sub-headline font-bold ;
color: var(--text-active);
font-feature-settings: 'salt' on;
margin: 0.8rem 0;
}
.menuSortList{
box-sizing: border-box;
&::after{
@apply absolute;
top: 110%;
content: "";
width: 100%;
border-bottom: 1px solid var(--border-line);
}
li{
div{
height: 4.8rem;
line-height: 4.8rem;
padding: 0 1.6rem;
margin-right: 0.8rem;
border-radius: 0.8rem;
&.active {
@apply font-bold relative;
color:var(--text-active);
background-color: var(--primary-lightest);
&::after{
@apply absolute;
content:"";
background-image: url('/assets/svg/check.svg');
right: 1.6rem;
top: calc(50% - 24px/2);
width: 2.4rem;
height: 2.4rem;
}
}
}
}
}
}

View File

@@ -0,0 +1,67 @@
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { QUERY_KEY, ROUTE } from 'src/utils/constanst.utils';
import s from './MenuSort.module.scss';
interface Props {
children?: any,
heading:string,
type:string,
onChangeValue?: (value: Object) => void
}
const SORT = [
{
name: 'By Name',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=by-name`,
},
{
name: 'Price(High to Low)',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=high-to-low`,
},
{
name: 'Price (Low to High)',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=low-to-high`,
},
{
name: 'On Sale',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.SORTBY}=on-sale`,
},
];
const MenuSort = ({heading,type,onChangeValue}:Props)=> {
const [active, setActive] = useState<string>('');
function handleClick(link:string){
setActive(link);
if(active === link){
setActive('');
}
}
useEffect(()=>{
let href = active?.split("=");
const linkValue = href[1];
onChangeValue && onChangeValue({[type]:linkValue});
},[active])
return (
<section className={classNames(s.menuSortWrapper)}>
<h2 className={classNames(s.menuSortHeading)}>{heading}</h2>
<ul className={s.menuSortList}>
{
SORT.map(item => <li key={item.name}>
<div onClick={()=> handleClick(item.link)} className={classNames({ [s.active]: item.link === active? true: false })}>
{item.name}
</div>
</li>)
}
</ul>
</section>
)
}
export default MenuSort

View File

@@ -1,17 +1,12 @@
@import '../../../../styles/utilities';
.formAuthen {
@apply bg-white w-full;
@apply bg-white w-full u-form;
.inner {
@screen md {
max-width: 52rem;
width: 60rem;
margin: auto;
}
.body {
> div {
&:not(:last-child) {
margin-bottom: 1.6rem;
}
}
}
.others {
@apply font-bold text-center;
margin-top: 4rem;

View File

@@ -1,12 +1,11 @@
import Link from 'next/link'
import React, { useRef, useEffect } from 'react'
import { Inputcommon, ButtonCommon } from 'src/components/common'
import React, { useEffect, useRef } from 'react'
import { ButtonCommon, Inputcommon, InputPassword } 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'
import s from '../FormAuthen.module.scss'
import SocialAuthen from '../SocialAuthen/SocialAuthen'
import styles from './FormLogin.module.scss'
interface Props {
isHide: boolean,
@@ -23,14 +22,13 @@ const FormLogin = ({ onSwitch, isHide }: Props) => {
}, [isHide])
return (
<section className={classNames({
[s.formAuthen]: true,
// [styles.hide]: isHide
})}>
<section className={s.formAuthen}>
<div className={s.inner}>
<div className={s.body}>
<Inputcommon placeholder='Email Address' type='email' ref={emailRef} />
<Inputcommon placeholder='Password' type='password' />
<Inputcommon placeholder='Email Address' type='email' ref={emailRef} />
{/* <Inputcommon placeholder='Email Address' type='email' ref={emailRef}
isShowIconSuccess={true} isIconSuffix={true} /> */}
<InputPassword placeholder='Password'/>
</div>
<div className={styles.bottom}>
<Link href={ROUTE.FORGOT_PASSWORD}>

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react'
import { ButtonCommon, Inputcommon } from 'src/components/common'
import { ButtonCommon, Inputcommon, InputPassword } from 'src/components/common'
import s from '../FormAuthen.module.scss'
import styles from './FormRegister.module.scss'
import SocialAuthen from '../SocialAuthen/SocialAuthen'
@@ -24,12 +24,11 @@ const FormRegister = ({ onSwitch, isHide }: Props) => {
<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' />
<InputPassword placeholder='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>

View File

@@ -8,7 +8,7 @@
@apply flex justify-center items-center min-h-screen;
.modal{
@apply inline-block align-bottom bg-white relative;
max-width: 60rem;
max-width: 66.4rem;
padding: 3.2rem;
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.24);
border-radius: 1.2rem;
@@ -17,7 +17,7 @@
}
.title{
@apply font-heading heading-3;
padding: 0 0.8rem 0 0.8rem;
padding: 0 1.6rem 0 0.8rem;
}
.close{
@apply absolute;

View File

@@ -2,7 +2,7 @@ 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 {
export interface ModalCommonProps {
onClose: () => void
visible: boolean
children: React.ReactNode
@@ -10,7 +10,7 @@ interface Props {
maxWidth?:string
}
const ModalCommon = ({ onClose, visible, children, title="Modal",maxWidth }: Props) => {
const ModalCommon = ({ onClose, visible, children, title="Modal",maxWidth }: ModalCommonProps) => {
const modalRef = useRef<HTMLDivElement>(null)
const clickOutSide = () => {
onClose && onClose()

View File

@@ -0,0 +1,4 @@
.footer{
margin-top: 4rem;
@apply flex justify-end items-center;
}

View File

@@ -0,0 +1,34 @@
import React from 'react'
import ButtonCommon from '../ButtonCommon/ButtonCommon'
import ModalCommon, { ModalCommonProps } from '../ModalCommon/ModalCommon'
import s from './ModalConfirm.module.scss'
interface ModalConfirmProps extends ModalCommonProps {
okText?: String
cancelText?: String
onOk?: () => void
onCancel?: () => void
}
const ModalConfirm = ({
okText = 'Ok',
cancelText = 'cancel',
onOk,
onCancel,
children,
title = 'Confirm',
...props
}: ModalConfirmProps) => {
return (
<ModalCommon {...props} title={title}>
{children}
<div className={s.footer}>
<div className="mr-4">
<ButtonCommon onClick={onCancel} type="light"> {cancelText}</ButtonCommon>
</div>
<ButtonCommon onClick={onOk}>{okText}</ButtonCommon>
</div>
</ModalCommon>
)
}
export default ModalConfirm

View File

@@ -0,0 +1,19 @@
@import "../../../styles/utilities";
.formUserInfo {
@apply u-form;
.inner {
@screen md {
width: 60rem;
margin: auto;
}
.bottom {
@apply grid grid-cols-2;
margin-top: 4rem;
grid-gap: 1.6rem;
> button {
@apply w-full;
}
}
}
}

View File

@@ -0,0 +1,46 @@
import classNames from 'classnames';
import Link from 'next/link';
import React, { useRef } from 'react';
import { useModalCommon } from 'src/components/hooks/useModalCommon';
import { CustomInputCommon } from 'src/utils/type.utils';
import { Inputcommon } from '..';
import ButtonCommon from '../ButtonCommon/ButtonCommon';
import ModalCommon from '../ModalCommon/ModalCommon';
import s from './ModalCreateUserInfo.module.scss';
// todo: remove
interface Props {
demoVisible: boolean,
demoCloseModal: () => void,
}
const ModalCreateUserInfo = ({ demoVisible: visible, demoCloseModal: closeModal }: Props) => {
// const { visible, closeModal } = useModalCommon({ initialValue: false})
const firstInputRef = useRef<CustomInputCommon>(null)
return (
<ModalCommon visible={visible} onClose={closeModal} title='Enter your Information'>
<div className={s.formUserInfo}>
<div className={s.inner}>
<div className={s.body}>
<Inputcommon placeholder='Street Address' ref={firstInputRef} />
<Inputcommon placeholder='City' />
<div className={s.line}>
{/* todo: select, not input */}
<Inputcommon placeholder='State' />
<Inputcommon placeholder='Zip code' />
</div>
<Inputcommon placeholder='Phone (delivery contact)' />
</div>
<div className={s.bottom}>
<ButtonCommon size='large' onClick={closeModal} type='light'>Skip</ButtonCommon>
<ButtonCommon size='large'>Submit</ButtonCommon>
</div>
</div>
</div>
</ModalCommon>
);
}
export default ModalCreateUserInfo;

View File

@@ -0,0 +1,4 @@
.footer{
margin-top: 4rem;
@apply flex justify-end items-center;
}

View File

@@ -0,0 +1,27 @@
import React from 'react'
import ButtonCommon from '../ButtonCommon/ButtonCommon'
import ModalCommon, { ModalCommonProps } from '../ModalCommon/ModalCommon'
import s from './ModalInfo.module.scss'
interface ModalInfoProps extends ModalCommonProps {
okText?: String
onOk?: () => void
}
const ModalInfo = ({
okText = 'Ok',
onOk,
children,
title = 'Confirm',
...props
}: ModalInfoProps) => {
return (
<ModalCommon {...props} title={title}>
{children}
<div className={s.footer}>
<ButtonCommon onClick={onOk}>{okText}</ButtonCommon>
</div>
</ModalCommon>
)
}
export default ModalInfo

View File

@@ -0,0 +1,22 @@
.warpper{
.item{
@apply inline-flex items-center justify-center cursor-pointer;
background-color: var(--gray);
margin: 0 0.4rem;
width: 3.2rem;
height: 3.2rem;
&.active{
@apply border border-solid;
border-color: var(--text-active);
background-color: var(--white);
}
&.disable{
svg{
path{
fill: var(--disabled)
}
}
@apply text-disabled cursor-not-allowed;
}
}
}

View File

@@ -0,0 +1,75 @@
import classNames from 'classnames'
import React, { useEffect, useState } from 'react'
import { ArrowLeftSmall, ArrowRightSmall } from 'src/components/icons'
import { DEFAULT_PAGE_SIZE } from 'src/utils/constanst.utils'
import PaginationItem from './components/PaginationItem'
import s from './PaginationCommon.module.scss'
interface PaginationCommonProps {
defaultCurrent?: number
pageSize?: number
total: number
onChange?: (page: number, pageSize: number) => void
}
const PaginationCommon = ({
total,
pageSize=DEFAULT_PAGE_SIZE,
defaultCurrent,
onChange,
}: PaginationCommonProps) => {
const [pageNum, setPageNum] = useState<number>(0)
const [currentPage, setCurrentPage] = useState<number>(0)
useEffect(() => {
setPageNum(Math.ceil(total / pageSize))
}, [total, pageSize])
useEffect(() => {
if (defaultCurrent) {
setCurrentPage(defaultCurrent)
}
}, [defaultCurrent])
const onPageClick = (page: number) => {
setCurrentPage(page)
onChange && onChange(page, pageSize)
}
const onPrevClick = () => {
setCurrentPage(currentPage - 1 < 0 ? 0 : currentPage - 1)
}
const onNextClick = () => {
setCurrentPage((currentPage + 1) > (pageNum - 1) ? (pageNum - 1) : currentPage + 1)
}
return (
<div className={s.warpper}>
<div
className={classNames(s.item, { [`${s.disable}`]: currentPage <= 0 })}
onClick={onPrevClick}
>
<ArrowLeftSmall disable={currentPage <= 0} />
</div>
{[...Array(pageNum).keys()].map((index) => {
return (
<PaginationItem
page={index}
onClick={onPageClick}
key={index}
active={index === currentPage}
/>
)
})}
<div
className={classNames(s.item, {
[s.disable]: currentPage >= pageNum - 1,
})}
onClick={onNextClick}
>
<ArrowRightSmall disable={currentPage >= pageNum} />
</div>
</div>
)
}
export default PaginationCommon

View File

@@ -0,0 +1,21 @@
import classNames from 'classnames'
import React from 'react'
import s from "../PaginationCommon.module.scss"
interface PaginationItemProps {
onClick:(page:number)=>void
page:number
active:boolean
}
const PaginationItem = ({onClick, page, active}: PaginationItemProps) => {
const onPageClick = () => {
onClick && onClick(page)
}
return (
<div onClick={onPageClick} className={classNames(s.item,{[`${s.active}`]:active})}>
{page+1}
</div>
)
}
export default PaginationItem

View File

@@ -2,8 +2,12 @@
max-width: 20.8rem;
min-height: 31.8rem;
padding: 1.2rem 1.2rem 0 1.2rem;
margin: auto;
margin-bottom: 1px;
@apply flex flex-col justify-between;
&.notSell {
@apply justify-center;
}
.cardTop{
@apply relative;
height: 13.8rem;
@@ -29,8 +33,6 @@
.cardMidTop{
.productname{
font-weight: bold;
line-height: 2.4rem;
font-size: 1.6rem;
color: var(--text-active);
&:hover{
cursor: pointer;
@@ -57,7 +59,8 @@
.cardBot{
min-height: 4rem;
@apply flex justify-between items-center;
.cardButton{
.cardIcon{
margin-right: 0.8rem;
}
}
}

View File

@@ -6,6 +6,7 @@ import ButtonIconBuy from '../ButtonIconBuy/ButtonIconBuy'
import ItemWishList from '../ItemWishList/ItemWishList'
import LabelCommon from '../LabelCommon/LabelCommon'
import s from './ProductCard.module.scss'
import ProductNotSell from './ProductNotSell/ProductNotSell'
export interface ProductCardProps extends ProductProps {
buttonText?: string
@@ -18,7 +19,15 @@ const ProductCard = ({
price,
buttonText = 'Buy Now',
imageSrc,
isNotSell,
}: ProductCardProps) => {
if (isNotSell) {
return <div className={`${s.productCardWarpper} ${s.notSell}`}>
<ProductNotSell name={name} imageSrc={imageSrc} />
</div>
}
return (
<div className={s.productCardWarpper}>
<div className={s.cardTop}>

View File

@@ -0,0 +1,27 @@
@import "../../../../styles/utilities";
.imgWrap {
img {
opacity: 0.5;
}
}
.name {
@apply text-label cursor-default font-bold;
}
.info {
@apply flex justify-center items-center custom-border-radius bg-info-light text-center;
padding: .8rem 1.6rem;
margin-top: 1.6rem;
color: var(--info);
svg {
@apply u-icon;
path {
fill: currentColor;
}
}
.text {
margin-left: 0.8rem;
}
}

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { IconInfo } from 'src/components/icons';
import ImgWithLink from '../../ImgWithLink/ImgWithLink';
import s from './ProductNotSell.module.scss';
export interface Props {
name: string,
imageSrc: string,
}
const ProductNotSell = ({ name, imageSrc }: Props) => {
return (
<>
<div className={s.imgWrap}>
<ImgWithLink src={imageSrc} alt={name} />
</div>
<div className={s.name}>{name}</div>
<div className={s.info}>
<IconInfo />
<div className={s.text}>
Not Sell
</div>
</div>
</>
);
};
export default ProductNotSell;

View File

@@ -0,0 +1,11 @@
.wrapper{
.list{
// max-width: 109.4rem;
@apply flex flex-wrap justify-around;
}
.pagination{
padding-top: 4.8rem;
// max-width: 109.4rem;
@apply flex justify-center items-center ;
}
}

View File

@@ -0,0 +1,30 @@
import React, { useState } from 'react'
import PaginationCommon from '../PaginationCommon/PaginationCommon'
import ProductCard, { ProductCardProps } from '../ProductCard/ProductCard'
import s from "./ProductList.module.scss"
interface ProductListProps {
data: ProductCardProps[]
}
const ProductList = ({data}: ProductListProps) => {
const [currentPage, setCurrentPage] = useState(0)
const onPageChange = (page:number) => {
setCurrentPage(page)
}
return (
<div className={s.wrapper}>
<div className={s.list}>
{
data.slice(currentPage*20,(currentPage+1)*20).map((product,index)=>{
return <ProductCard {...product} key={index}/>
})
}
</div>
<div className={s.pagination}>
<PaginationCommon total={data.length} pageSize={20} onChange={onPageChange}/>
</div>
</div>
)
}
export default ProductList

View File

@@ -1,7 +1,7 @@
import React, { ChangeEvent, useEffect, useState } from 'react'
import s from './QuanittyInput.module.scss'
import classNames from 'classnames'
import { Minus, Plus } from '@components/icons'
import { IconMinus, IconPlus } from '../../icons'
interface QuanittyInputProps
extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
@@ -64,7 +64,7 @@ const QuanittyInput = ({
return (
<div className={classNames(s.quanittyInputWarper, { [s[size]]: size })}>
<div className={s.minusIcon} onClick={onMinusClick}>
<Minus />
<IconMinus />
</div>
<input
{...props}
@@ -74,7 +74,7 @@ const QuanittyInput = ({
className={s.quanittyInput}
/>
<div className={s.plusIcon} onClick={onPlusClick}>
<Plus />
<IconPlus />
</div>
</div>
)

View File

@@ -6,6 +6,9 @@
width: 100%;
max-height: 22rem;
border-radius: 2.4rem;
img {
border-radius: 2.4rem;
}
&:hover{
cursor: pointer;
}

View File

@@ -0,0 +1,23 @@
import React from 'react'
import { ProductCardProps } from '../ProductCard/ProductCard'
import RecipeDetailInfo from './components/RecipeDetailInfo/RecipeDetailInfo'
import RecipeIngredient from './components/RecipeIngredient/RecipeIngredient'
import s from './RecipeDetail.module.scss'
interface Props {
className?: string
children?: any,
ingredients: ProductCardProps[],
}
const RecipeDetail = ({ ingredients }: Props) => {
return (
<section className={s.recipeDetail}>
<RecipeDetailInfo />
<RecipeIngredient data={ingredients} />
</section >
)
}
export default RecipeDetail

View File

@@ -0,0 +1,19 @@
.recipeBriefInfo {
@apply flex;
.item {
@apply flex;
&:not(:last-child) {
margin-right: 2.4rem;
}
svg {
width: 2rem;
height: 2rem;
path {
fill: var(--text-label);
}
}
.content {
margin-left: 0.8rem;
}
}
}

View File

@@ -0,0 +1,29 @@
import React from 'react'
import { IconLocation, IconPeople, IconTime } from 'src/components/icons'
import s from './RecipeBriefInfo.module.scss'
interface Props {
className?: string
children?: any,
}
const RecipeBriefInfo = ({ }: Props) => {
return (
<section className={s.recipeBriefInfo}>
<div className={s.item}>
<IconTime />
<div className={s.content}>15 minutes</div>
</div>
<div className={s.item}>
<IconPeople />
<div className={s.content}>4 People</div>
</div>
<div className={s.item}>
<IconLocation />
<div className={s.content}>15 minutes</div>
</div>
</section >
)
}
export default RecipeBriefInfo

View File

@@ -0,0 +1,61 @@
@import "../../../../../styles/utilities";
.recipeDetailInfo {
@apply spacing-horizontal;
margin: 5.6rem auto;
@screen md {
@apply flex;
}
.img {
width: fit-content;
margin-top: 0;
@screen sm-only {
margin-bottom: 2rem;
}
@screen lg {
max-width: 60rem;
}
img {
@apply w-full;
object-fit: contain;
max-height: 64rem;
border-radius: 2.4rem;
@screen md {
max-height: 90rem;
}
}
}
.recipeInfo {
@screen md {
max-width: 39rem;
margin-left: 4.8rem;
}
@screen lg {
margin-left: 11.2rem;
}
.top {
margin-bottom: 4.8rem;
.name {
@apply heading-1 font-heading;
margin-bottom: 1.6rem;
}
}
.detail {
.item {
&:not(:last-child) {
margin-bottom: 2.4rem;
}
.heading {
@apply heading-3 font-heading;
margin-bottom: 0.8rem;
}
.content {
list-style: disc;
margin-left: 2rem;
}
}
}
}
}

View File

@@ -0,0 +1,59 @@
import React from 'react'
import RecipeBriefInfo from '../RecipeBriefInfo/RecipeBriefInfo'
import s from './RecipeDetailInfo.module.scss'
interface Props {
className?: string
children?: any
}
const RecipeDetailInfo = ({ }: Props) => {
return (
<section className={s.recipeDetailInfo}>
<div className={s.img}>
<img src="https://user-images.githubusercontent.com/76729908/131634880-8ae1437b-d3f8-421e-a546-d5a4f9a28e5f.png" alt="Recipe" />
</div>
<div className={s.recipeInfo}>
<div className={s.top}>
<h1 className={s.name}>
Crispy Fried Calamari
</h1>
<RecipeBriefInfo />
</div>
<div className={s.detail}>
<div className={s.item}>
<h3 className={s.heading}>Ingredients</h3>
<ul className={s.content}>
<li>Canola oil for frying</li>
<li>1 pound clean squid bodies cut in 1/4 inch rings and dried with a paper towel</li>
<li>2 cups flour</li>
<li>1/2 teaspoon kosher salt</li>
<li>1/2 teaspoon garlic powder</li>
<li>1/8 teaspoon coarse ground black pepper</li>
<li>1 lemon cut into wedges</li>
</ul>
</div>
<div className={s.item}>
<h3 className={s.heading}>Preparation</h3>
<ul className={s.content}>
<li>1In a large pot or dutch oven add three inches of oil and bring to 350 degrees.</li>
<li>Add the flour, salt, garlic powder and pepper to a large bowl and stir to combine.</li>
<li>Toss the squid pieces in the flour then into the hot oil.</li>
<li>Fry the squid for 1-2 minutes. You want the color to stay pale like in the pictures.</li>
<li>Remove to a cookie sheet to drain (do not add paper towels as it will steam the calamari and make it soft.)</li>
<li>Serve with lemon wedges.</li>
</ul>
</div>
<div className={s.item}>
<h3 className={s.heading}>Link</h3>
<a href="https://dinnerthendessert.com/crispy-fried-calamari" target="_blank" rel="noopener noreferrer">https://dinnerthendessert.com/crispy-fried-calamari</a>
</div>
</div>
</div>
</section >
)
}
export default RecipeDetailInfo

View File

@@ -0,0 +1,21 @@
@import "../../../../../styles/utilities";
.recipeIngredient {
padding: 6rem 0;
@screen md {
padding: 5.6rem 0;
}
.top {
@apply flex justify-between items-center spacing-horizontal;
}
.bottom {
@apply flex justify-center items-center spacing-horizontal;
margin-top: 4rem;
button {
width: 100%;
@screen md {
width: 39rem;
}
}
}
}

View File

@@ -0,0 +1,33 @@
import React from 'react'
import ButtonCommon from 'src/components/common/ButtonCommon/ButtonCommon'
import HeadingCommon from 'src/components/common/HeadingCommon/HeadingCommon'
import { ProductCardProps } from 'src/components/common/ProductCard/ProductCard'
import ProductCarousel from 'src/components/common/ProductCarousel/ProductCarousel'
import ViewAllItem from 'src/components/common/ViewAllItem/ViewAllItem'
import { ROUTE } from 'src/utils/constanst.utils'
import s from './RecipeIngredient.module.scss'
interface Props {
className?: string
children?: any,
data: ProductCardProps[],
}
const RecipeIngredient = ({ data }: Props) => {
return (
<section className={s.recipeIngredient}>
<div className={s.top}>
<HeadingCommon>Ingredients</HeadingCommon>
<div>
<ViewAllItem link={ROUTE.PRODUCTS} />
</div>
</div>
<ProductCarousel data={data} itemKey="recipe-ingredient" />
<div className={s.bottom}>
<ButtonCommon type='ghost' size='large'>Buy all</ButtonCommon>
</div>
</section>
)
}
export default RecipeIngredient

View File

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

View File

@@ -0,0 +1,46 @@
import { TOptionsEvents } from 'keen-slider'
import React from 'react'
import CarouselCommon, {
CarouselCommonProps,
} from '../../CarouselCommon/CarouselCommon'
import BlogCard, { BlogCardProps } from 'src/components/common/CardBlog/CardBlog'
import s from "./BlogPostCarousel.module.scss"
interface BlogPostCarouselProps
extends Omit<CarouselCommonProps<BlogCardProps>, '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 BlogPostCarousel = ({ option, data, ...props }: BlogPostCarouselProps) => {
return (
<div className={s.blogCardWarpper}>
<CarouselCommon<BlogCardProps>
data={data}
Component={BlogCard}
{...props}
option={{ ...OPTION_DEFAULT, ...option }}
/>
</div>
)
}
export default BlogPostCarousel

View File

@@ -0,0 +1,19 @@
@import '../../../styles/utilities';
.blogPostWarpper {
&.cream{
background-color: #F5F4F2;
}
padding-top: 5.6rem;
padding-bottom: 5.2rem;
@apply flex flex-col;
.top {
@apply spacing-horizontal flex w-full justify-between;
padding-bottom: 3.2rem;
@screen xl {
.right {
margin-right: 2.476rem;
}
}
}
}

View File

@@ -0,0 +1,75 @@
import image15 from '../../../../public/assets/images/image15.png'
import image16 from '../../../../public/assets/images/image16.png'
import image17 from '../../../../public/assets/images/image17.png'
import classNames from 'classnames'
import React from 'react'
import { HeadingCommon, ViewAllItem } from 'src/components/common'
import { BlogCardProps } from 'src/components/common/CardBlog/CardBlog'
import BlogPostCarousel from './BlogPostCarousel/BlogPostCarousel'
import s from './RelevantBlogPosts.module.scss'
import { ROUTE } from 'src/utils/constanst.utils';
interface RelevantProps {
data?: BlogCardProps[],
itemKey?: string,
title?: string,
viewAllLink?: string,
bgcolor?: "default" | "cream"
}
const recipe:BlogCardProps[] = [
{
title: "Want to Lose Weight? Here are 10 DEBM Diet Guidelines for Beginners",
slug: 'have-a-nice-lunch',
description:"The DEBM diet stands for "+'"Delicious Happy Fun Diet"'+". This diet was popularized by Robert...",
imageSrc: image15.src,
},{
title: "9 Ways to Make an Aloe Vera Mask at Home",
slug: 'have-a-nice-lunch',
description:"Aloe vera or aloe vera is a green plant, has thorns on the side of the skin with yellowish patches and...",
imageSrc: image16.src,
},{
title: "Don't Buy Wrong, Here Are 7 Ways to Choose a Ripe Dragon Fruit",
slug: 'have-a-nice-lunch',
description:"Dragon fruit is a type of fruit that is a favorite for many people because of its delicious and fresh...",
imageSrc: image17.src,
},{
title: "Want to Lose Weight? Here are 10 DEBM Diet Guidelines for Beginners",
slug: 'have-a-nice-lunch',
description:"The DEBM diet stands for "+'"Delicious Happy Fun Diet"'+". This diet was popularized by Robert...",
imageSrc: image15.src,
},{
title: "9 Ways to Make an Aloe Vera Mask at Home",
slug: 'have-a-nice-lunch',
description:"Aloe vera or aloe vera is a green plant, has thorns on the side of the skin with yellowish patches and...",
imageSrc: image16.src,
},{
title: "Don't Buy Wrong, Here Are 7 Ways to Choose a Ripe Dragon Fruit",
slug: 'have-a-nice-lunch',
description:"Dragon fruit is a type of fruit that is a favorite for many people because of its delicious and fresh...",
imageSrc: image17.src,
}]
const RelevantBlogPosts = ({ data = recipe, itemKey="detail-relevant", title="Relevant Blog Posts", bgcolor = "default" }: RelevantProps) => {
return (
<div className={classNames({
[s.blogPostWarpper] : true,
[s[bgcolor]] : bgcolor,
})}
>
<div className={s.top}>
<div className={s.left}>
<HeadingCommon>{title}</HeadingCommon>
</div>
<div className={s.right}>
<ViewAllItem link={ROUTE.BLOGS}/>
</div>
</div>
<div className={s.bot}>
<BlogPostCarousel data={data} itemKey={itemKey} />
</div>
</div>
)
}
export default RelevantBlogPosts

View File

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

View File

@@ -5,11 +5,10 @@ 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 ScrollToTop = ({ visibilityHeight=450 }: ScrollToTopProps) => {
const [scrollPosition, setSrollPosition] = useState(0);
const [showScrollToTop, setShowScrollToTop] = useState("hide");
@@ -26,7 +25,7 @@ const ScrollToTop = ({ target, visibilityHeight=450 }: ScrollToTopProps) => {
};
function handleScrollUp() {
target.current.scrollIntoView({ behavior: "smooth" });
window.scrollTo(0, 0);
}
function addEventScroll() {
@@ -34,7 +33,7 @@ const ScrollToTop = ({ target, visibilityHeight=450 }: ScrollToTopProps) => {
}
useEffect(() => {
addEventScroll()
addEventScroll();
});
return (

View File

@@ -1,32 +1,90 @@
@import "../../../styles/utilities";
.select{
@apply rounded-lg border-solid;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 1.2rem 1.6rem;
&.base{
width: 18.6rem;
height: 4.8rem;
.select {
background-color: var(--white);
.selectTrigger {
svg {
@apply transition-all duration-200;
}
}
&.large{
&.base {
width: 20.6rem;
.selectTrigger {
width: 20.6rem;
padding: 1.2rem 1.6rem;
}
}
&.large {
width: 34.25rem;
height: 5.6rem;
.selectTrigger {
width: 34.25rem;
padding: 1.6rem 1.6rem;
}
}
&.default{
@apply border;
&.default {
.selectTrigger {
@apply border-solid border border-current;
}
}
&.custom{
@apply border-2;
border-color: var(--border-line);
color: var(--text-label);
&.custom {
.selectTrigger {
@apply border-2;
border-color: var(--border-line);
color: var(--text-label);
}
}
.option{
&:hover{
background-color: black;
&:hover {
cursor: pointer;
.hoverWrapper {
@apply block;
animation: SelectAnimation 0.2s ease-out;
}
.selectTrigger {
svg {
transform: rotate(180deg);
}
}
}
}
.selectTrigger {
@apply outline-none flex justify-between;
color: var(--text-active);
border-radius: 0.8rem;
}
.hoverWrapper {
@apply hidden outline-none absolute z-10;
padding-top: 0.6rem;
.selectOptionWrapper {
border-radius: 0.8rem;
background-color: var(--white);
padding: 0.4rem 0rem 0.4rem 0rem;
&.base {
width: 20.6rem;
}
&.large {
width: 34.25rem;
}
&.default {
@apply border-solid border border-current;
}
&.custom {
@apply border-2;
border-color: var(--border-line);
color: var(--text-label);
}
}
&:hover {
@apply block;
}
}
@keyframes SelectAnimation {
0% {
opacity: 0;
transform: translateY(1.6rem);
}
100% {
opacity: 1;
transform: none;
}
}

View File

@@ -1,26 +1,57 @@
import s from './SelectCommon.module.scss'
import classNames from 'classnames'
import { useState } from 'react'
import { IconVectorDown } from 'src/components/icons'
import SelectOption from './SelectOption/SelectOption'
interface Props {
placeHolder? : string,
placeholder? : string,
size?: 'base' | 'large',
type?: 'default' | 'custom',
option: {name: string}[],
option: {name: string, value: string}[],
onChange?: (value: string) => void,
}
const SelectCommon = ({ type = 'default', size = 'base', option, placeHolder }: Props) => {
const SelectCommon = ({ type = 'default', size = 'base', option, placeholder, onChange}: Props) => {
const [selectedName, setSelectedName] = useState(placeholder)
const [selectedValue, setSelectedValue] = useState('')
const changeSelectedName = (item:string, value: string) => {
setSelectedValue(value)
setSelectedName(item)
onChange && onChange(value)
}
return(
<select className={classNames({
[s.select] : true,
[s[type]]: !!type,
[s[size]]: !!size,
})}
>
<option disabled selected hidden>{placeHolder}</option>
{
option.map(item => <option className={s.option} value={item.name}> {item.name} </option>)
}
</select>
<>
<div className={classNames({
[s.select] : true,
[s[size]] : !!size,
[s[type]] : !!type,
})}
>
<div className={classNames({
[s.selectTrigger] : true,
})}
>{selectedName}<IconVectorDown /></div>
<div className={s.hoverWrapper}>
<div className={classNames({
[s.selectOptionWrapper] : true,
[s[type]] : !!type,
[s[size]] : !!size,
})}
>
{
option.map(item =>
<SelectOption key={item.value} itemName={item.name} value={item.value} onClick={changeSelectedName} size={size} selected={(selectedValue === item.value)} />
)
}
</div>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,21 @@
@import "../../../../styles/utilities";
.selectOption {
@apply outline-none;
background-color: var(--white);
&.base{
width: 20.4rem;
padding: 0.8rem 1.6rem;
}
&.large{
width: 33.75rem;
padding: 0.8rem 1.6rem;
}
&:hover{
background-color: var(--gray);
color: var(--primary);
}
&.isChoose{
background-color: var(--gray);
}
}

View File

@@ -0,0 +1,27 @@
import s from './SelectOption.module.scss'
import classNames from 'classnames'
interface Props{
onClick: (name: string, value: string) => void,
itemName: string,
size: 'base' | 'large',
value: string,
selected?: boolean,
}
const SelectOption = ({onClick, itemName, size, value, selected} : Props) => {
const changeName = () => {
onClick(itemName, value)
}
return(
<div className={classNames({
[s.selectOption] : true,
[s[size]] : !!size,
[s.isChoose] : selected ,
})}
onClick = {changeName}
>{itemName}</div>
)
}
export default SelectOption

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