瀏覽代碼

Merge branch 'hamburger-menu' of ntasicc/coffee into master

products-stripe
lazarkostic 3 年之前
父節點
當前提交
71c0d6a87a

+ 9
- 1
components/hero/Hero.jsx 查看文件

@@ -18,6 +18,7 @@ const Hero = () => {
>
<Box
sx={{
minWidth: '50%',
width: { xs: '100%', md: '50%' },
height: '100%',
display: 'flex',
@@ -76,6 +77,7 @@ const Hero = () => {
}}
>
<Button
disableRipple
sx={{
backgroundColor: '#CBA213',
mr: { md: 4 },
@@ -90,6 +92,7 @@ const Hero = () => {
Explore the Shop
</Button>
<Button
disableRipple
sx={{
display: { xs: 'none', sm: 'flex' },
textTransform: 'none',
@@ -114,7 +117,12 @@ const Hero = () => {
backgroundColor: 'white',
}}
>
<Box sx={{ mt: 10, ml: -12 }}>
<Box
sx={{ ml: { md: -12 } }}
display="flex"
justifyContent="center"
alignItems="center"
>
<Image
src="/images/Hero-Image.png"
alt="profile"

+ 3
- 2
components/layout/base-layout/Layout.jsx 查看文件

@@ -1,12 +1,13 @@
import { Box } from '@mui/material';
import Footer from '../footer/Footer';
import Navbar from '../navbar/Navbar';
import MainNav from '../navbar/MainNav';

function Layout(props) {
return (
<>
<Box sx={{ width: '100%' }}>
<Navbar />
{/* <Navbar /> */}
<MainNav />
<main>{props.children}</main>
<Footer></Footer>
</Box>

+ 110
- 0
components/layout/navbar/DesktopNav.jsx 查看文件

@@ -0,0 +1,110 @@
import Box from '@mui/material/Box';
import Image from 'next/image';
import Link from 'next/link';
import { CART_PAGE, PROFILE_PAGE } from '../../../constants/pages';
import { NavItemDesktop } from './NavItem';
import { items } from './navItems';

const DesktopNav = ({ router, totalQuantity, session, signOutHandler }) => {
return (
<Box sx={{ display: { xs: 'none', md: 'flex' }, width: '100%' }}>
<Box
sx={{
flexGrow: 1,
maxWidth: '50%',
height: 30,
display: 'flex',
justifyContent: 'space-between',
}}
>
{items.map((item) => (
<NavItemDesktop
key={item.id}
router={router}
name={item.name}
url={item.url}
/>
))}
</Box>
<Box
sx={{
flexGrow: 1,
maxWidth: '50%',
height: 30,
display: 'flex',
justifyContent: 'right',
pt: 0.5,
mr: 4,
}}
>
{session?.user?._id && (
<Box
sx={{
mx: 2,
mt: 0.1,
cursor: 'pointer',
}}
onClick={signOutHandler}
>
<Image
src="/images/logout.svg"
alt="profile"
width={18}
height={20}
/>
</Box>
)}
<Box
sx={{
mx: 2,
cursor: 'pointer',
}}
>
<Link key="home" href={PROFILE_PAGE}>
<Image
src="/images/profile.svg"
alt="profile"
width={24}
height={24}
/>
</Link>
</Box>
<Box
sx={{
mr: 6,
ml: 2,
cursor: 'pointer',
}}
>
<Link key="home" href={CART_PAGE}>
<Box>
{totalQuantity !== 0 && (
<Box
sx={{
color: 'white',
zIndex: 3,
width: 20,
height: 20,
borderRadius: 20,
textAlign: 'center',
px: 0.5,
ml: 2.2,
mt: -1,
fontSize: 17,
position: 'absolute',
backgroundColor: 'primary.main',
}}
>
{totalQuantity}
</Box>
)}
<Image src="/images/cart.svg" alt="cart" width={24} height={24} />
</Box>
</Link>
</Box>
</Box>
</Box>
);
};

export default DesktopNav;

+ 99
- 0
components/layout/navbar/MainNav.jsx 查看文件

@@ -0,0 +1,99 @@
import MenuIcon from '@mui/icons-material/Menu';
import AppBar from '@mui/material/AppBar';
import IconButton from '@mui/material/IconButton';
import Toolbar from '@mui/material/Toolbar';
import useMediaQuery from '@mui/material/useMediaQuery';
import React, { useState } from 'react';

//drawer elements used
import { signOut, useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useStore } from '../../../store/cart-context';
import { useUserUpdate } from '../../../store/user-context';
import DesktopNav from './DesktopNav';
import MobileNav from './MobileNav';

export default function MainNav() {
//react useState hook to save the current open/close state of the drawer, normally variables dissapear afte the function was executed
const [open, setState] = useState(false);
const matches = useMediaQuery('(min-width: 900px)');

const router = useRouter();

const { data: session } = useSession();

const { totalQuantity } = useStore();

const { clearUser } = useUserUpdate();

const signOutHandler = async () => {
const data = await signOut({ redirect: false, callbackUrl: '/' });
clearUser();
router.push(data.url);
};

//function that is being called every time the drawer should open or close, the keys tab and shift are excluded so the user can focus between the elements with the keys
const toggleDrawer = (open) => (event) => {
if (
event.type === 'keydown' &&
(event.key === 'Tab' || event.key === 'Shift')
) {
return;
}
//changes the function state according to the value of open
setState(open);
};

useEffect(() => {
if (matches) {
setState(false);
}
}, [matches]);

return (
<AppBar
position="absolute"
sx={{
zIndex: 100,
top: 20,
width: '100%',
backgroundColor: 'transparent',
boxShadow: 'none',
height: 40,
}}
>
<Toolbar sx={{ width: '100%' }}>
<DesktopNav
router={router}
totalQuantity={totalQuantity}
session={session}
signOutHandler={signOutHandler}
/>
<IconButton
edge="start"
color={router.pathname === '/' ? 'inherit' : 'primary'}
aria-label="open drawer"
onClick={toggleDrawer(true)}
sx={{
mr: 2,
display: {
xs: 'block',
md: 'none',
},
}}
>
<MenuIcon />
</IconButton>

{/* The outside of the drawer */}
<MobileNav
session={session}
signOutHandler={signOutHandler}
toggleDrawer={toggleDrawer}
open={open}
/>
</Toolbar>
</AppBar>
);
}

+ 125
- 0
components/layout/navbar/MobileNav.jsx 查看文件

@@ -0,0 +1,125 @@
import CloseIcon from '@mui/icons-material/Close';
import { Box, Button, Divider, Drawer, IconButton } from '@mui/material';
import Image from 'next/image';
import Link from 'next/link';
import { CART_PAGE, PROFILE_PAGE } from '../../../constants/pages';
import { NavItemMobile } from './NavItem';
import { items } from './navItems';

const MobileNav = ({ toggleDrawer, session, signOutHandler, open }) => {
return (
<Drawer
PaperProps={{
sx: { width: '50%' },
}}
anchor="left"
open={open}
onClose={toggleDrawer(false)}
onOpen={toggleDrawer(true)}
>
<Box
sx={{
p: 2,
height: 1,
backgroundColor: '#fff',
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<IconButton disableRipple>
<CloseIcon color="primary" onClick={toggleDrawer(false)} />
</IconButton>

{session?.user?._id && (
<IconButton disableRipple onClick={signOutHandler}>
<Image
src="/images/logout.svg"
alt="profile"
width={18}
height={20}
/>
</IconButton>
)}
</Box>

<Divider sx={{ mb: session?.user?._id ? 0 : 2 }} />

{session?.user?._id && (
<>
<Box
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
>
<NavItemMobile
toggleDrawer={toggleDrawer}
name="Profile"
url={PROFILE_PAGE}
/>
</Box>
<Divider sx={{ mb: 2 }} />
</>
)}

<Box
sx={{ mb: 2 }}
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
>
{items.map((item) => (
<NavItemMobile
key={item.id}
toggleDrawer={toggleDrawer}
name={item.name}
url={item.url}
/>
))}

<NavItemMobile
toggleDrawer={toggleDrawer}
name="Cart"
url={CART_PAGE}
/>
</Box>

<Box
sx={{
display: 'flex',
justifyContent: 'center',
position: 'absolute',
bottom: '0',
left: '50%',
transform: 'translate(-50%, 0)',
}}
>
{!session?.user?._id && (
<>
<Link href="/auth/register">
<Button
onClick={toggleDrawer(false)}
variant="contained"
sx={{ m: 1, width: 0.5 }}
>
Register
</Button>
</Link>
<Link href="/auth">
<Button
onClick={toggleDrawer(false)}
variant="outlined"
sx={{ m: 1, width: 0.5 }}
>
Login
</Button>
</Link>
</>
)}
</Box>
</Box>
</Drawer>
);
};

export default MobileNav;

+ 42
- 0
components/layout/navbar/NavItem.jsx 查看文件

@@ -0,0 +1,42 @@
import { ListItemButton, ListItemText, Typography } from '@mui/material';
import Link from 'next/link';

export const NavItemMobile = ({ toggleDrawer, name, url }) => {
return (
<ListItemButton>
<Link href={url}>
<ListItemText
onClick={toggleDrawer(false)}
primary={
<Typography
sx={{ fontSize: '24px' }}
style={{ color: 'primary.main' }}
>
{name}
</Typography>
}
/>
</Link>
</ListItemButton>
);
};

export const NavItemDesktop = ({ url, router, name }) => {
return (
<Link href={url}>
<Typography
textAlign="center"
sx={{
mx: 'auto',
fontSize: { md: 20, lg: 20 },
fontWeight: 500,
color: router.pathname === '/' ? 'white' : 'primary.main',
textDecoration: 'none',
cursor: 'pointer',
}}
>
{name}
</Typography>
</Link>
);
};

+ 1
- 1
components/layout/navbar/Navbar.jsx 查看文件

@@ -196,7 +196,7 @@ const Navbar = () => {
px: 0.5,
ml: 2.2,
mt: -1,
fontSize: 16,
fontSize: 17,
position: 'absolute',
backgroundColor: 'primary.main',
}}

+ 33
- 0
components/layout/navbar/navItems.js 查看文件

@@ -0,0 +1,33 @@
import {
BASE_PAGE,
CONTACT_PAGE,
PRODUCTS_PAGE,
} from '../../../constants/pages';

export const items = [
{
id: 1,
name: 'Home',
url: BASE_PAGE,
},
{
id: 2,
name: 'Menu',
url: BASE_PAGE,
},
{
id: 3,
name: 'About',
url: BASE_PAGE,
},
{
id: 4,
name: 'Store',
url: PRODUCTS_PAGE,
},
{
id: 5,
name: 'Contact',
url: CONTACT_PAGE,
},
];

+ 17
- 70
components/products-grid/ProductsGrid.jsx 查看文件

@@ -1,54 +1,27 @@
import { Button, Container, Grid } from '@mui/material';
import { Box } from '@mui/system';
import Image from 'next/image';
import { useMemo, useState } from 'react';
import { useFetchProductsByCategory } from '../../hooks/useFetchProductData';
import { compare } from '../../utils/helpers/sortHelpers';
import ProductCard from '../product-card/ProductCard';

const ProductsGrid = ({
allProducts,
hasNextPage,
productType,
fetchNextPage,
sort,
}) => {
const productsPerPage = 9;
const [next, setNext] = useState(productsPerPage);
const ProductsGrid = ({ allProducts, hasNextPage, fetchNextPage }) => {
// const allItems = useMemo(
// () => allProducts?.pages?.flatMap((page) => page.product),
// [allProducts]
// );

const { data: filteredData } = useFetchProductsByCategory(productType);

const allItems = useMemo(
() => allProducts?.pages?.flatMap((page) => page.product),
[allProducts]
const dataToDisplay = allProducts.pages.map((page) =>
page.product.map((item) => (
<Grid key={item._id} item md={4} sm={6} xs={12} sx={{ mb: '100px' }}>
<ProductCard product={item} />
</Grid>
))
);

const dataToDisplay =
productType === 'All' || productType === ''
? allItems.sort(compare('name', sort)).map((item) => (
<Grid key={item._id} item md={4} sm={6} xs={12} sx={{ mb: '100px' }}>
<ProductCard product={item} />
</Grid>
))
: filteredData?.productsByCategory
.slice(0, next)
.sort(compare('name', sort))
.map((item) => (
<Grid
key={item._id}
item
md={4}
sm={6}
xs={12}
sx={{ mb: '100px' }}
>
<ProductCard product={item} />
</Grid>
));

const handleMoreProducts = () => {
setNext(next + productsPerPage);
};
// const dataToDisplay = allProducts.map((item) => (
// <Grid key={item._id} item md={4} sm={6} xs={12} sx={{ mb: '100px' }}>
// <ProductCard product={item} />
// </Grid>
// ));

return (
<Container
@@ -60,7 +33,7 @@ const ProductsGrid = ({
{dataToDisplay}
</Grid>
<Box textAlign="center" mt={-5} mb={5}>
{hasNextPage && (productType === 'All' || productType === '') && (
{hasNextPage && (
<Button
onClick={fetchNextPage}
startIcon={
@@ -85,32 +58,6 @@ const ProductsGrid = ({
Load More
</Button>
)}

{filteredData && next < filteredData.productsByCategory.length && (
<Button
onClick={handleMoreProducts}
startIcon={
<Image
src="/images/arrow.svg"
alt="arrow down"
width={29}
height={29}
/>
}
sx={{
backgroundColor: 'primary.main',
height: 50,
width: 150,
color: 'white',
':hover': {
bgcolor: 'primary.main', // theme.palette.primary.main
color: 'white',
},
}}
>
Load More
</Button>
)}
</Box>
</Container>
);

+ 16
- 2
components/products/featured-product/ProductInfo.jsx 查看文件

@@ -7,6 +7,16 @@ import { useState } from 'react';
const ProductInfo = ({ data, bColor, addProductToCart, inCart }) => {
const [quantity, setQuantity] = useState(1);

const handleIncrement = () => {
setQuantity((prevState) => prevState + 1);
};

const handleDecrement = () => {
if (quantity > 1) {
setQuantity((prevState) => prevState - 1);
}
};

return (
<Box
sx={{
@@ -65,18 +75,20 @@ const ProductInfo = ({ data, bColor, addProductToCart, inCart }) => {
}}
>
<Button
disableRipple
sx={{
color: 'white',
fontSize: 20,
width: 50,
}}
onClick={() => {
setQuantity((prevState) => prevState - 1);
handleDecrement();
}}
>
-
</Button>
<Button
disableRipple
sx={{
color: 'white',
fontSize: 17,
@@ -86,19 +98,21 @@ const ProductInfo = ({ data, bColor, addProductToCart, inCart }) => {
{quantity}
</Button>
<Button
disableRipple
sx={{
color: 'white',
fontSize: 20,
width: 50,
}}
onClick={() => {
setQuantity((prevState) => prevState + 1);
handleIncrement();
}}
>
+
</Button>
</ButtonGroup>
<Button
disableRipple
sx={{
mt: { xs: 2, md: 0 },
ml: { md: 2 },

+ 10
- 10
hooks/useInfiniteQuery.js 查看文件

@@ -1,21 +1,21 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { getAllProducts } from '../requests/products/productRequest';

export const useInfiniteProducts = (filter) => {
export const useInfiniteProducts = (category, filter) => {
return useInfiniteQuery(
['products'],
async ({ pageParam = 1 }) => await getAllProducts(pageParam),
['products', category, filter],
async ({ pageParam = 1 }) =>
await getAllProducts(
pageParam,
category === '' ? 'All' : category,
filter === '' ? 'asc' : filter
),
{
getNextPageParam: (lastPage, pages) => {
const maxPages = Math.ceil(lastPage?.productCount / 9);
const nextPage = pages.length + 1;
if (nextPage <= maxPages) {
return nextPage;
if (lastPage.next !== null) {
return pages.length + 1;
}
},
enabled: filter === 'All' || filter === '',
staleTime: 0,
cacheTime: 0,
}
);
};

+ 2
- 2
pages/api/product/category/[categoryName].js 查看文件

@@ -1,5 +1,5 @@
const Product = require('../../../models/product');
import dbConnect from '../../../utils/helpers/dbHelpers';
const Product = require('../../../../models/product');
import dbConnect from '../../../../utils/helpers/dbHelpers';

async function handler(req, res) {
const { method } = req;

+ 23
- 37
pages/products/[customId].js 查看文件

@@ -5,15 +5,10 @@ import Image from 'next/image';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import Loader from '../../components/loader/Loader';
import ProductCard from '../../components/product-card/ProductCard';
import TabPanel from '../../components/tab-panel/TabPanel';
import {
useFetchSimilarProducts,
useFetchSingleProduct,
} from '../../hooks/useFetchProductData';
import { useFetchSingleProduct } from '../../hooks/useFetchProductData';
import { getProductData } from '../../requests/products/producDataRequest';
import { useStore, useStoreUpdate } from '../../store/cart-context';
import { shuffle } from '../../utils/helpers/shuffle';

const SingleProduct = () => {
const { addCartValue } = useStoreUpdate();
@@ -25,10 +20,7 @@ const SingleProduct = () => {

const { data, isLoading } = useFetchSingleProduct(customId);

const productCategory = data?.product.category;

const { data: similarProducts, isLoading: similarLoading } =
useFetchSimilarProducts(productCategory);
// const productCategory = data?.product.category;

const [value, setValue] = useState(0);

@@ -54,30 +46,26 @@ const SingleProduct = () => {
return <Loader loading={isLoading} />;
}

if (similarLoading) {
return <Loader loading={similarLoading} />;
}

const productsToShow = (id) => {
const filtered = shuffle(similarProducts?.productsByCategory)
.filter((product) => product.customID !== id)
.slice(0, 3)
.map((item) => (
<Grid
key={item._id}
item
lg={4}
md={6}
sm={6}
xs={12}
sx={{ mb: '100px' }}
>
<ProductCard product={item} />
</Grid>
));

return filtered;
};
// const productsToShow = (id) => {
// const filtered = shuffle(similarProducts?.productsByCategory)
// .filter((product) => product.customID !== id)
// .slice(0, 3)
// .map((item) => (
// <Grid
// key={item._id}
// item
// lg={4}
// md={6}
// sm={6}
// xs={12}
// sx={{ mb: '100px' }}
// >
// <ProductCard product={item} />
// </Grid>
// ));

// return filtered;
// };

return (
<Box
@@ -170,9 +158,7 @@ const SingleProduct = () => {
>
Other Product You May Like
</Typography>
<Grid container spacing={2}>
{productsToShow(customId)}
</Grid>
<Grid container spacing={2}></Grid>
</Container>
</Box>
);

+ 8
- 6
pages/products/index.js 查看文件

@@ -2,16 +2,18 @@ import { Box } from '@mui/system';
import Head from 'next/head';
import { useState } from 'react';
import FilterSort from '../../components/filter-sort/FilterSort';
import Loader from '../../components/loader/Loader';
import LoadingSpinner from '../../components/loader/basic-spinner/LoadSpinner';
import ProductsGrid from '../../components/products-grid/ProductsGrid';
import ProductsHero from '../../components/products-hero/ProductsHero';
import { useInfiniteProducts } from '../../hooks/useInfiniteQuery';

const Products = () => {
const [filter, setFilter] = useState('All');
const [sort, setSort] = useState('asc');
const { data, isLoading, fetchNextPage, hasNextPage } =
useInfiniteProducts(filter);
const [filter, setFilter] = useState('');
const [sort, setSort] = useState('');
const { data, isLoading, fetchNextPage, hasNextPage } = useInfiniteProducts(
filter,
sort
);

const handleProductTypeChange = (event) => {
const filterText = event.target.value;
@@ -24,7 +26,7 @@ const Products = () => {
};

if (isLoading) {
return <Loader loading={isLoading} />;
return <LoadingSpinner />;
}

return (

+ 2
- 2
public/images/logout.svg 查看文件

@@ -4,10 +4,10 @@
viewBox="0 0 384.971 384.971" style="enable-background:new 0 0 384.971 384.971;" xml:space="preserve">
<g>
<g id="Sign_Out">
<path d="M180.455,360.91H24.061V24.061h156.394c6.641,0,12.03-5.39,12.03-12.03s-5.39-12.03-12.03-12.03H12.03
<path fill='#664C47' d="M180.455,360.91H24.061V24.061h156.394c6.641,0,12.03-5.39,12.03-12.03s-5.39-12.03-12.03-12.03H12.03
C5.39,0.001,0,5.39,0,12.031V372.94c0,6.641,5.39,12.03,12.03,12.03h168.424c6.641,0,12.03-5.39,12.03-12.03
C192.485,366.299,187.095,360.91,180.455,360.91z"/>
<path d="M381.481,184.088l-83.009-84.2c-4.704-4.752-12.319-4.74-17.011,0c-4.704,4.74-4.704,12.439,0,17.179l62.558,63.46H96.279
<path fill='#664C47' d="M381.481,184.088l-83.009-84.2c-4.704-4.752-12.319-4.74-17.011,0c-4.704,4.74-4.704,12.439,0,17.179l62.558,63.46H96.279
c-6.641,0-12.03,5.438-12.03,12.151c0,6.713,5.39,12.151,12.03,12.151h247.74l-62.558,63.46c-4.704,4.752-4.704,12.439,0,17.179
c4.704,4.752,12.319,4.752,17.011,0l82.997-84.2C386.113,196.588,386.161,188.756,381.481,184.088z"/>
</g>

+ 5
- 5
requests/products/productRequest.js 查看文件

@@ -1,10 +1,10 @@
export const getAllProducts = async ({
url,
export const getAllProducts = async (
pageIndex,
category = 'All',
filter = 'asc',
}) => {
filter = 'asc'
) => {
const response = await fetch(
`${url}&category=${category}&filterType=${filter}`
`http://localhost:3000/api/product?pageIndex=${pageIndex}&category=${category}&filterType=${filter}`
);

const data = await response.json();

+ 32
- 0
yarn.lock 查看文件

@@ -1937,6 +1937,7 @@
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz"
integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==


"@sendgrid/client@^7.7.0":
version "7.7.0"
resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.7.0.tgz#f8f67abd604205a0d0b1af091b61517ef465fdbf"
@@ -7928,6 +7929,37 @@ mkdirp@^0.5.1:
dependencies:
minimist "^1.2.6"

mkdirp@^1.0.3, mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==

dependencies:
yallist "^4.0.0"

minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"

mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz"
integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
dependencies:
for-in "^1.0.2"
is-extendable "^1.0.1"

mkdirp@^0.5.1:
version "0.5.6"
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
dependencies:
minimist "^1.2.6"

mkdirp@^1.0.3, mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"

Loading…
取消
儲存