├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── components
├── adSlider
│ └── adSlider.js
├── banner
│ └── banner.js
├── footer
│ ├── footer.js
│ └── style.less
├── itemsSlider
│ ├── itemsSlider.js
│ └── style.less
├── layout.js
├── loading
│ └── loading.js
├── meta
│ └── meta.js
├── navbar
│ ├── lowerNav.js
│ ├── navbar.js
│ ├── style.less
│ └── upperNav.js
├── partners
│ ├── partners.js
│ └── style.less
├── product
│ ├── controls.js
│ ├── gridItem.js
│ ├── gridItem.less
│ ├── listItem.js
│ ├── listItem.less
│ ├── product.js
│ ├── productPreview.js
│ ├── productPreview.less
│ └── rating.js
├── searchPage
│ ├── searchControls.js
│ ├── searchFilter.js
│ └── style.less
├── tabbedItems
│ └── tabbedItems.js
└── widget
│ ├── style.less
│ └── widget.js
├── documentation
└── images
│ ├── captured.gif
│ └── captured2.gif
├── next.config.js
├── now.json
├── package-lock.json
├── package.json
├── pages
├── _app.js
├── _document.js
├── _error.js
├── about.js
├── app.less
├── index.js
├── items
│ └── [slug].js
└── search.js
├── public
├── favicon.ico
├── images
│ ├── banner.gif
│ ├── banner.png
│ ├── item1.jpg
│ ├── item2.jpg
│ ├── logo.png
│ ├── logo2.png
│ ├── partner.jpg
│ ├── sample.jpg
│ ├── sample2.jpg
│ └── slider.jpg
└── manifest.json
├── server.js
├── static
└── manifest.json
├── styles
├── antd.less
├── index.less
├── searchPage.less
└── variables.less
└── utils
├── URLS.js
└── axios-instance.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | [
4 | "inline-import",
5 | {
6 | "extensions": [
7 | ".css"
8 | ]
9 | }
10 | ],
11 | [
12 | "import",
13 | {
14 | "libraryName": "antd"
15 | }
16 | ]
17 | ],
18 | "presets": [
19 | [
20 | "next/babel",
21 | {
22 | // "styled-jsx": {
23 | // "plugins": [
24 | // "styled-jsx-plugin-sass"
25 | // ]
26 | // }
27 | }
28 | ]
29 | ]
30 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .package-lock.json
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | .env*
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 mohammadou1
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nextstore
2 | An eCommerce platform built with NextJS
3 | Still in development phase, will implement backend as soon as my frontend is finished.
4 | Using Ant Design for templating.
5 |
6 | Please make sure to leave your opinion, it does matter no matter how small it is.
7 |
8 |
9 | This project is still in development phase, after finishing the frontend I'll start developing it's backend as NodeJS
10 | to try it,
11 |
12 |
13 | online demo: [NextStore](https://nextstore.itmohou.now.sh/)
14 | just clone and
15 |
16 | ```npm run dev```
17 | or
18 | ```yarn dev```
19 |
20 |
21 |
22 | 
23 |
24 | 
25 |
26 |
--------------------------------------------------------------------------------
/components/adSlider/adSlider.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Slider from "react-slick";
3 | import Link from 'next/link';
4 | const settings = {
5 | arrows: true,
6 | infinite: true,
7 | autoplay: true,
8 | autoplaySpeed: 3000,
9 | slidesToShow: 1,
10 | lazyLoad:true,
11 | slidesToScroll: 1,
12 |
13 | };
14 | const PrevArrow = props => {
15 | const { className, onClick } = props;
16 | return (
17 |
18 |
);
19 | }
20 | const NextArrow = props => {
21 | const { className, onClick } = props;
22 | return (
23 |
24 |
);
25 | }
26 |
27 |
28 |
29 |
30 | const adSlider = () => {
31 |
32 |
33 | const [isSliding, setIsSliding] = useState(false);
34 |
35 | // * to check if slick slider is being dragged to prevent conflict between click and swipe.
36 | const isSlidingHandler = e => {
37 | if (isSliding) {
38 | e.preventDefault();
39 | }
40 | }
41 |
42 | const ads = [
43 | { id: 1, src: "/images/slider.jpg", href: "items/some-item-slug" },
44 | { id: 2, src: "/images/slider.jpg", href: "items/some-item-slug" },
45 | ].map((ad, idx) => );
55 |
56 | return (
57 | setIsSliding(true)}
58 | afterChange={() => setIsSliding(false)}
59 | prevArrow={ }
60 | nextArrow={ } {...settings}>
61 | {ads}
62 |
63 | );
64 | }
65 |
66 | export default adSlider;
67 |
--------------------------------------------------------------------------------
/components/banner/banner.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | const banner = ({ imageSrc, children, href, hrefAs, alt }) => {
3 | return (
4 |
5 |
15 |
16 | );
17 | }
18 | export default banner;
19 |
20 |
--------------------------------------------------------------------------------
/components/footer/footer.js:
--------------------------------------------------------------------------------
1 | import './style.less';
2 | import {
3 | Row,
4 | Col,
5 | Input,
6 | Form,
7 | } from 'antd';
8 | import Link from 'next/link';
9 |
10 | const aboutLinks = [
11 | { title: 'About Us', href: '/about', },
12 | { title: 'Privacy & Policy', href: '/privacy-policy', },
13 | { title: 'FAQ', href: '/faq', },
14 | { title: 'Contact Us', href: '/contact', as: 'contact-us' },
15 | ].map((link, idx) =>
16 |
17 |
18 | {link.title}
19 |
20 |
21 | );
22 |
23 | const pagesLinks = [
24 | { title: 'Home', href: '/', },
25 | { title: 'Brands', href: '/brands', },
26 | { title: 'Collections', href: '/faq', },
27 | { title: 'Offers', href: '/offers', as: '/special-offers' },
28 | ].map((link, idx) =>
29 |
30 |
31 | {link.title}
32 |
33 |
34 | );
35 |
36 | const accountLinks = [
37 | { title: 'My Account', href: '/account', },
38 | { title: 'Order History', href: '/history', as: '/order-history' },
39 | { title: 'Terms & Conditions', href: '/terms', as: '/terms-conditions' },
40 | { title: 'Delivery Information', href: '/delivery', as: '/delivery-information' },
41 | ].map((link, idx) =>
42 |
43 |
44 | {link.title}
45 |
46 |
47 | );
48 |
49 | const footer = () => {
50 | return (
51 |
111 | );
112 | }
113 |
114 | export default footer;
115 |
--------------------------------------------------------------------------------
/components/footer/style.less:
--------------------------------------------------------------------------------
1 | @import "../../styles/variables.less";
2 |
3 | .footer {
4 | background-color: @primary--color;
5 | padding: 35px 25px 0 35px;
6 | background: linear-gradient(
7 | to right,
8 | @primary--color 60%,
9 | darken(@primary--color, 10%)
10 | );
11 |
12 | @media (max-width: 768px) {
13 | text-align: center;
14 | }
15 |
16 | .ant-input-group-wrapper {
17 | max-width: 400px;
18 | margin-top: 1rem;
19 |
20 | input {
21 | height: 40px;
22 | }
23 | }
24 |
25 | hr {
26 | border-width: 0;
27 | border-top-width: 1px;
28 | margin-top: 25px;
29 | }
30 |
31 | h5 {
32 | color: @secondary--color;
33 | font-weight: bold;
34 | font-size: 17px;
35 | margin-bottom: 1.5rem;
36 |
37 | @media (max-width: 768px) {
38 | margin-top: 2rem;
39 | margin-bottom: 1rem;
40 | }
41 | }
42 |
43 | p {
44 | color: #ffffff;
45 | font-size: 14px;
46 | margin: 0;
47 | }
48 |
49 | ul {
50 | margin: 0;
51 | padding: 0;
52 | list-style: none;
53 |
54 | li {
55 | line-height: 30px;
56 | }
57 |
58 | a {
59 | font-size: 14px;
60 | color: #ffffff;
61 |
62 | &:hover {
63 | color: @secondary--color;
64 | }
65 | }
66 | }
67 |
68 | .footer-end {
69 | padding: 10px 0;
70 |
71 | p {
72 | font-size: 13px;
73 | }
74 |
75 | a {
76 | color: @secondary--color;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/components/itemsSlider/itemsSlider.js:
--------------------------------------------------------------------------------
1 | import Slider from "react-slick";
2 | import Product from '../product/product';
3 | import './style.less';
4 |
5 |
6 | const itemsSlider = props => {
7 | const { items = [], slidesToShow = 4, slidesToShowMobile, slidesToShowTablet } = props;
8 |
9 | const settings = {
10 | arrows: false,
11 | infinite: false,
12 | slidesToShow,
13 | slidesToScroll: 1,
14 | responsive: [
15 | {
16 | breakpoint: 768,
17 | settings: {
18 | slidesToShow: slidesToShowMobile || slidesToShow
19 | },
20 | },
21 | {
22 | breakpoint: 992,
23 | settings: {
24 | slidesToShow: slidesToShowTablet || slidesToShow
25 | },
26 | },
27 | {
28 | breakpoint: 1100,
29 | settings: {
30 | slidesToShow: slidesToShow
31 | },
32 | },
33 | ]
34 |
35 | };
36 |
37 | return (
38 |
39 | {items.map(item => )}
40 |
41 | );
42 | }
43 |
44 | export default itemsSlider;
45 |
--------------------------------------------------------------------------------
/components/itemsSlider/style.less:
--------------------------------------------------------------------------------
1 | @import "../../styles/variables.less";
2 |
3 | .products-slider {
4 | .slick-slide {
5 | &:not(:nth-child(1)) {
6 | .product-item {
7 | border-left: 1px solid @line-color;
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/components/layout.js:
--------------------------------------------------------------------------------
1 | // import stylesheet from 'antd/dist/antd.min.css';
2 | import '../styles/index.less';
3 | import Navbar from './navbar/navbar';
4 | import Footer from './footer/footer';
5 | import Router from 'next/router';
6 | import NProgress from 'nprogress';
7 |
8 | Router.events.on('routeChangeStart', _ => NProgress.start());
9 | Router.events.on('routeChangeComplete', _ => NProgress.done());
10 | Router.events.on('routeChangeError', _ => NProgress.done());
11 | const Layout = ({ children }) => {
12 |
13 | return (
14 | {/* */}
15 |
16 |
17 |
18 | {children}
19 |
20 |
21 |
);
22 | }
23 |
24 | export default Layout;
--------------------------------------------------------------------------------
/components/loading/loading.js:
--------------------------------------------------------------------------------
1 |
2 | const loading = () => {
3 | return (
4 |
5 | Loading...
6 |
7 | );
8 | }
9 |
10 | export default loading;
11 |
--------------------------------------------------------------------------------
/components/meta/meta.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import PropTypes from "prop-types";
3 |
4 | /**
5 | * Pass any extra meta title as a child
6 | */
7 | const Meta = props => {
8 | const {
9 | title,
10 | keywords,
11 | description,
12 | robots,
13 | ogTitle,
14 | ogType,
15 | ogImage,
16 | ogDescription,
17 | ogUrl,
18 | // * extra metas provided as children
19 | children
20 | } = props;
21 | return (
22 |
23 | {title && {title} }
24 | {keywords && }
25 | {description && }
26 | {robots && }
27 | {ogTitle && }
28 | {ogType && }
29 | {ogImage && }
30 | {ogDescription && }
31 | {ogUrl && }
32 | {children}
33 |
34 | );
35 | };
36 |
37 | Meta.propTypes = {
38 | /** Page title */
39 | title: PropTypes.string,
40 | /** Meta keywords, each keyword is seprated by a comma (,) */
41 | keywords: PropTypes.string,
42 | /** Meta description tag, page description */
43 | description: PropTypes.string,
44 | /**Meta robots, describes if google will follow / index this page */
45 | robots: PropTypes.string,
46 | /** Open graph meta title */
47 | ogTitle: PropTypes.string,
48 | /** Open graph meta tag, descripts the type of the item, such as video, audio, product etc.... */
49 | ogType: PropTypes.string,
50 | /** Open graph image url */
51 | ogImage: PropTypes.string,
52 | /** Open graph description */
53 | ogDescription: PropTypes.string,
54 | /**Open graph url */
55 | ogUrl: PropTypes.string
56 | };
57 |
58 | export default Meta;
59 |
--------------------------------------------------------------------------------
/components/navbar/lowerNav.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Menu, Row, Col, Icon, Drawer } from "antd";
3 | const { Item, SubMenu } = Menu;
4 | import NoSSR from "react-no-ssr";
5 | import { useMediaQuery } from "react-responsive";
6 | import Link from "next/link";
7 | import { withRouter } from "next/router";
8 |
9 | const links = [
10 | { title: "Home", href: "/" },
11 | { title: "Brands", href: "/brands" },
12 | {
13 | title: "New Collection",
14 | href: "/collections/new",
15 | className: "/new-collection"
16 | },
17 | {
18 | title: "Offers / Specials",
19 | href: "/offers",
20 | as: "/special-offers",
21 | className: "offer"
22 | },
23 | { title: "Contact Us", href: "/contact" }
24 | ].map(link => (
25 | -
26 |
27 | {link.title}
28 |
29 |
30 | ));
31 |
32 | const LowerNav = props => {
33 | const [toggled, setToggled] = useState(false);
34 | const isMobile = useMediaQuery({ query: "(max-width: 992px)" });
35 | const { router } = props;
36 | const toggleMenu = () => setToggled(!toggled);
37 |
38 | const selectedPath =
39 | router.pathname !== "/_error" ? router.pathname : router.asPath;
40 | return (
41 |
42 |
43 | {!isMobile ? (
44 |
45 |
46 |
47 |
48 | {links}
49 |
50 |
51 |
52 |
53 |
54 |
57 | Account
58 |
59 | }
60 | >
61 | -
62 |
63 | Login
64 |
65 |
66 | -
67 |
68 | Register
69 |
70 |
71 |
72 | -
73 |
74 |
75 |
76 |
77 | My Cart
78 | $0.00
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | ) : (
87 |
118 | )}
119 |
120 |
121 | );
122 | };
123 |
124 | export default withRouter(LowerNav);
125 |
--------------------------------------------------------------------------------
/components/navbar/navbar.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import { Fragment } from 'react';
4 | import UpperNav from './upperNav';
5 | import LowerNav from './lowerNav';
6 | import './style.less';
7 |
8 | const Navbar = () => {
9 |
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
19 | export default Navbar;
20 |
--------------------------------------------------------------------------------
/components/navbar/style.less:
--------------------------------------------------------------------------------
1 | @import "../../styles/variables.less";
2 |
3 | .upper-menu {
4 | background-color: @upper-menu-bg;
5 | padding: 23px 0;
6 |
7 | .brand-logo {
8 | // width : 64px;
9 | max-width: 100%;
10 | }
11 |
12 | .input-search {
13 | input {
14 | height: 40px;
15 | }
16 | }
17 |
18 | @media (max-width: 768px) {
19 | padding: 23px 12px;
20 | .brand-logo {
21 | margin-bottom: 16px;
22 | }
23 | .ant-form {
24 | margin-bottom: 16px;
25 | }
26 | }
27 | }
28 |
29 | .lower-menu {
30 | background-color: @primary--color;
31 |
32 | @media (max-width: 992px) {
33 | padding: 10px;
34 | }
35 |
36 | .ant-menu {
37 | .ant-menu-item {
38 | a {
39 | .anticon {
40 | font-size: 32px;
41 | }
42 |
43 | span {
44 | color: @secondary--color;
45 | font-size: 18px;
46 | font-weight: bold;
47 | }
48 | }
49 | }
50 | }
51 |
52 | .menu-toggler {
53 | cursor: pointer;
54 | font-size: 30px;
55 | color: #ffffff;
56 | background-color: lighten(@primary--color, 10%);
57 | padding: 4px 8px;
58 | border-radius: @border-radius-base;
59 |
60 | &:hover {
61 | background-color: lighten(@primary--color, 5%);
62 | }
63 | }
64 | }
65 |
66 | .ant-menu.ant-menu-sub {
67 | .ant-menu-item,
68 | .ant-menu-item-selected {
69 | a {
70 | color: @primary--color;
71 |
72 | &:hover,
73 | &:active,
74 | &:focus {
75 | color: darken(@primary--color, 20%);
76 | }
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/components/navbar/upperNav.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Router, { withRouter } from "next/router";
3 | import { Row, Col, Form, Input, Select, Menu, Dropdown, Icon } from "antd";
4 | const { Item } = Menu;
5 | const { Option } = Select;
6 |
7 | const options = [];
8 |
9 | for (let i = 0; i < 5; i++) {
10 | options.push({i.toString(36) + i} );
11 | }
12 | const categoriesDropdown = (
13 |
14 | {options}
15 |
16 | );
17 |
18 | const currencies = [
19 | { title: "SAR", href: "/" },
20 | { title: "USD", href: "/" }
21 | ].map((currency, idx) => - {currency.title}
);
22 | const currenciesDropdown = {currencies} ;
23 |
24 | const UpperNav = props => {
25 | // * to extract search value and assign it to default search input value.
26 | const { router } = props;
27 | const searchHandler = value => {
28 | // ! used _limit just to match jsonplaceholder format..
29 | Router.push(`/search?_title=${value}&_limit=10`);
30 | };
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
48 |
57 |
58 |
59 |
69 |
70 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default withRouter(UpperNav);
78 |
--------------------------------------------------------------------------------
/components/partners/partners.js:
--------------------------------------------------------------------------------
1 | import Slider from "react-slick";
2 | import './style.less';
3 |
4 | const settings = {
5 | arrows: false,
6 | infinite: true,
7 | autoplay: true,
8 | autoplaySpeed: 1500,
9 | slidesToShow: 7,
10 | lazyLoad: true,
11 | slidesToScroll: 1,
12 | responsive: [
13 | {
14 | breakpoint: 568,
15 | settings: {
16 | slidesToShow: 2
17 | },
18 | },
19 | {
20 | breakpoint: 768,
21 | settings: {
22 | slidesToShow: 3
23 | },
24 | },
25 | {
26 | breakpoint: 992,
27 | settings: {
28 | slidesToShow: 4
29 | },
30 | },
31 | {
32 | breakpoint: 1100,
33 | settings: {
34 | slidesToShow: 6
35 | },
36 | },
37 | {
38 | breakpoint: 1200,
39 | settings: {
40 | slidesToShow: 7
41 | },
42 | },
43 | ]
44 |
45 | };
46 | const partners = props => {
47 | return (
48 |
49 |
50 | {props.partners.map(partner =>
51 |
52 |
)
53 | }
54 |
55 |
56 | );
57 | }
58 |
59 | export default partners;
60 |
--------------------------------------------------------------------------------
/components/partners/style.less:
--------------------------------------------------------------------------------
1 | .partners-slider {
2 | .partner {
3 | padding: 10px;
4 | }
5 | img {
6 | max-width: 100%;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/components/product/controls.js:
--------------------------------------------------------------------------------
1 | import { Button } from 'antd';
2 |
3 | const controls = ({addToCardHandler,previewHandler}) => {
4 | return (
5 |
6 | addToCardHandler()}>
7 |
8 |
9 | previewHandler()}>
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | export default controls;
17 |
--------------------------------------------------------------------------------
/components/product/gridItem.js:
--------------------------------------------------------------------------------
1 | import NoSSR from "react-no-ssr";
2 | import { Fragment } from "react";
3 | import Link from "next/link";
4 | import Truncate from "react-truncate";
5 | import Controls from "./controls";
6 | import Rating from "./rating";
7 | import "./gridItem.less";
8 |
9 | const gridItem = props => {
10 | const { item, togglePreview } = props;
11 | const { id, image, title, slug, rating, price, discount } = item;
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ...}>
32 | {title}
33 |
34 |
35 |
36 |
37 |
38 | {discount ? (
39 |
40 | ${discount}
41 | ${price}
42 |
43 | ) : (
44 | ${price}
45 | )}
46 |
47 |
48 | console.log("add to cart")}
50 | previewHandler={togglePreview}
51 | />
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default gridItem;
59 |
--------------------------------------------------------------------------------
/components/product/gridItem.less:
--------------------------------------------------------------------------------
1 | @import "../../styles/variables.less";
2 |
3 | .grid-item {
4 | padding: 0 15px;
5 | height: 100%;
6 | margin-bottom: 3rem;
7 | i {
8 | font-size: 13px;
9 | }
10 |
11 | .details {
12 | overflow: hidden;
13 | position: relative;
14 | }
15 |
16 | .product-image {
17 | overflow: hidden;
18 | }
19 | img {
20 | transition: all 300ms ease-in-out;
21 | max-width: 100%;
22 | }
23 |
24 | .product-controls {
25 | background-color: #ffffff;
26 | position: absolute;
27 | transition: all 250ms ease-in-out;
28 | transform: translateY(100%);
29 | left: 0;
30 | top: 0;
31 | right: 0;
32 | bottom: 0;
33 | z-index: 2;
34 | display: flex;
35 | justify-content: center;
36 | align-items: center;
37 |
38 | button:nth-child(1) {
39 | margin: 0 10px;
40 | }
41 |
42 | @media (max-width: 992px) {
43 | position: relative;
44 | transform: none;
45 | }
46 | }
47 |
48 | &:hover {
49 | .product-controls {
50 | transform: translateY(0);
51 | }
52 |
53 | img {
54 | transform: scale(1.1);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/components/product/listItem.js:
--------------------------------------------------------------------------------
1 | import NoSSR from "react-no-ssr";
2 | import { Fragment } from "react";
3 | import Link from "next/link";
4 | import { Row, Col } from "antd";
5 | import Truncate from "react-truncate";
6 | import Controls from "./controls";
7 | import Rating from "./rating";
8 | import "./listItem.less";
9 |
10 | const listItem = props => {
11 | const { item, togglePreview } = props;
12 | const { id, image, title, slug, rating, price, discount, body: brief } = item;
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ...}>
31 | {title}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | ...}>
43 | {brief}
44 |
45 |
46 |
47 | {discount ? (
48 |
49 | ${discount}
50 | ${price}
51 |
52 | ) : (
53 | ${price}
54 | )}
55 |
56 |
57 | console.log("add to cart")}
59 | previewHandler={togglePreview}
60 | />
61 |
62 |
63 |
64 |
65 |
66 | );
67 | };
68 |
69 | export default listItem;
70 |
--------------------------------------------------------------------------------
/components/product/listItem.less:
--------------------------------------------------------------------------------
1 | .product-item.list-item {
2 | img {
3 | max-width: 100%;
4 | }
5 | h3 {
6 | &:hover {
7 | text-decoration: underline;
8 | }
9 | }
10 | .details {
11 | height: 100%;
12 | width: 100%;
13 | position: relative;
14 | }
15 | .product-controls {
16 | button {
17 | margin-right: 5px;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/components/product/product.js:
--------------------------------------------------------------------------------
1 |
2 | import { useState } from 'react';
3 | import GridItem from './gridItem';
4 | import ListItem from './listItem';
5 |
6 | import dynamic from 'next/dynamic';
7 | const ProductPreview = dynamic(import('./productPreview'), {
8 | ssr: false,
9 | });
10 |
11 |
12 |
13 | const product = props => {
14 | const { item, type = 'grid' } = props;
15 | const [preview, setPreview] = useState(false);
16 |
17 | const togglePreview = () => setPreview(!preview);
18 |
19 | return (
20 |
21 | {preview &&
}
24 |
25 | {type === 'grid' ?
:
26 |
}
27 |
);
28 | }
29 |
30 | export default product;
31 |
--------------------------------------------------------------------------------
/components/product/productPreview.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { Modal, Row, Col, Input, Button } from "antd";
3 | import Link from "next/link";
4 | import axios from "../../utils/axios-instance";
5 | import Skeleton from "react-loading-skeleton";
6 | import Magnifier from "react-magnifier";
7 | import Rating from "./rating";
8 | import "./productPreview.less";
9 |
10 | const ProductSkelton = () => (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 |
24 | const Image = ({ src, alt }) => ;
25 |
26 | const productPreview = ({ preview, toggle, productID }) => {
27 | const [product, setProduct] = useState(null);
28 | const [quantity, setQuantity] = useState(null);
29 |
30 | useEffect(() => {
31 | const getProduct = async () => {
32 | const response = await axios.get(`/posts/${productID}`);
33 | setTimeout(() => setProduct(response.data), 1000);
34 | };
35 | getProduct();
36 | }, []);
37 |
38 | const quantityChangedHandler = e => setQuantity(e.target.value || 0);
39 |
40 | return (
41 |
48 |
49 | {!product ? (
50 |
51 | ) : (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
62 |
63 | {product.title}
64 |
65 |
66 |
67 |
68 |
69 | Availability:
70 | Out of Stock
71 |
72 |
73 | Product Code:
74 | 131
75 |
76 |
77 | Ex Tax:
78 | $10
79 |
80 |
81 |
$250
82 |
$200
83 |
84 |
85 |
91 | addToCardHandler()}
96 | >
97 |
98 |
99 |
100 |
101 |
102 |
103 | )}
104 |
105 |
106 | );
107 | };
108 |
109 | export default productPreview;
110 |
--------------------------------------------------------------------------------
/components/product/productPreview.less:
--------------------------------------------------------------------------------
1 | .preview {
2 | .preview-details-skeleton,
3 | .preview-details {
4 | margin-top: 20px;
5 | padding-left: 15px;
6 |
7 | .price {
8 | font-size: 22px;
9 | }
10 | .discounted {
11 | font-size: 18px;
12 | }
13 | h3 {
14 | margin-bottom: 14px;
15 | }
16 |
17 | i {
18 | font-size: 13px;
19 | }
20 |
21 | a {
22 | &:hover {
23 | text-decoration: underline;
24 | color: black;
25 | }
26 | }
27 |
28 | span {
29 | margin-bottom: 10px;
30 | }
31 |
32 | ul {
33 | margin-top: 0 9px;
34 | padding: 0;
35 | list-style: none;
36 | font-size: 16px;
37 |
38 | li {
39 | line-height: 2;
40 |
41 | span {
42 | margin: 0 6px;
43 | font-weight: bold;
44 | }
45 | }
46 | }
47 | .controls {
48 | input {
49 | width: 76px;
50 | height: 40px;
51 | margin: 0 10px;
52 | }
53 | }
54 | }
55 |
56 | img {
57 | max-width: 100%;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/components/product/rating.js:
--------------------------------------------------------------------------------
1 |
2 | const rating = ({ rating = 0 }) => {
3 | return (
4 |
5 | {
6 | [...Array(5)].map((_, idx) => {
7 | if ((idx + 1) <= rating) {
8 | return
9 | }
10 | return
11 | })
12 | }
13 |
14 | );
15 | }
16 |
17 | export default rating;
18 |
--------------------------------------------------------------------------------
/components/searchPage/searchControls.js:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | Button,
4 | Select
5 | } from 'antd';
6 | const { Option } = Select;
7 |
8 | const options = [
9 | { value: 10 },
10 | { value: 25 },
11 | { value: 50 },
12 | ].map((option, idx) => {option.value} );
13 |
14 | const searchControls = props => {
15 | const {query ,searchHandler,displayChangedHandler} = props;
16 | const limit = query._limit;
17 |
18 |
19 | const limitChangedHandler = value => {
20 | query._limit = value;
21 | searchHandler(query);
22 | }
23 |
24 | return (
25 |
26 |
27 | displayChangedHandler('list')}>
28 |
29 |
30 | displayChangedHandler('grid')}>
31 |
32 |
33 |
34 |
35 |
36 | Show: {' '}
37 |
39 | {options}
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
47 | export default searchControls;
48 |
--------------------------------------------------------------------------------
/components/searchPage/searchFilter.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import {
3 | Card,
4 | Select,
5 | Slider,
6 | Checkbox,
7 | Button
8 | } from 'antd';
9 | import './style.less';
10 | import Router from 'next/router';
11 | const { Option } = Select;
12 |
13 |
14 | const options = [
15 | { value: 'A', text: 'Name (A-Z)' },
16 | { value: 'Z', text: 'Name (Z-A)' },
17 | { value: 'low', text: 'Price low > high' },
18 | { value: 'high', text: 'Price high > low' },
19 | ].map(option => {option.text} );
20 |
21 | const searchFilters = props => {
22 | const { searchHandler, query } = props;
23 | const { _title = '', _limit } = query;
24 |
25 | const [prices, setPrices] = useState([100, 1000]);
26 | const [sort, setSort] = useState('A');
27 | const [offer, setOffer] = useState('A');
28 | const [fiveStars, setFiveStars] = useState(false);
29 | const [fourStars, setFourStars] = useState(false);
30 | const [threeStars, setThreeStars] = useState(false);
31 | const [twoStars, setTwoStars] = useState(false);
32 | const [oneStar, setOneStar] = useState(false);
33 |
34 |
35 | const changePriceHandler = value => setPrices(value);
36 | const changeSortHandler = value => setSort(value);
37 | const changeOfferHandler = value => setOffer(value.target.checked);
38 | const changeRatingFiveStars = value => setFiveStars(value.target.checked);
39 | const changeRatingFourStars = value => setFourStars(value.target.checked);
40 | const changeRatingThreeStars = value => setThreeStars(value.target.checked);
41 | const changeRatingTwoStars = value => setTwoStars(value.target.checked);
42 | const changeRatingOneStar = value => setOneStar(value.target.checked);
43 |
44 | const filterSearchHandler = () => {
45 | const params = {
46 | _title,
47 | sort,
48 | prices,
49 | offer,
50 | fiveStars,
51 | fourStars,
52 | threeStars,
53 | twoStars,
54 | oneStar,
55 | _limit,
56 | }
57 | searchHandler(params);
58 | }
59 | return (
60 | }>
62 |
63 |
169 |
170 |
171 |
172 | Filter
173 |
174 |
175 |
176 | )
177 | }
178 |
179 | export default searchFilters;
--------------------------------------------------------------------------------
/components/searchPage/style.less:
--------------------------------------------------------------------------------
1 | @import "../../styles/variables.less";
2 |
3 | .filters {
4 | border: 3px solid #f0f0f0;
5 | ul {
6 | margin: 0;
7 | padding: 0;
8 | list-style: none;
9 | #sort_filter {
10 | width: 100%;
11 | }
12 | li {
13 | margin-bottom: 17px;
14 | p {
15 | margin-bottom: 4px;
16 | }
17 | .ant-checkbox-wrapper {
18 | margin-right: 5px;
19 | margin-left: 5px;
20 | }
21 | span {
22 | font-size: 15px;
23 | }
24 | }
25 | .aligned {
26 | display: flex;
27 | justify-content: space-between;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/components/tabbedItems/tabbedItems.js:
--------------------------------------------------------------------------------
1 | import {
2 | Tabs,
3 | } from 'antd';
4 |
5 | const { TabPane } = Tabs;
6 |
7 | const tabbedItems = props => {
8 | const { tabs = [], extraContent } = props;
9 | return (
10 |
11 |
12 | {
13 | tabs.map((tab, idx) =>
14 | {tab.component}
15 | )
16 | }
17 |
18 |
19 | );
20 | }
21 |
22 | export default tabbedItems;
23 |
--------------------------------------------------------------------------------
/components/widget/style.less:
--------------------------------------------------------------------------------
1 | .widget{
2 | background-size: cover;
3 | background-position: center;
4 | background-repeat: no-repeat;
5 | position: relative;
6 | width: 100%;
7 | height: 100%;
8 | cursor: pointer;
9 | @media (max-width:992px) {
10 | display: none;
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/components/widget/widget.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import './style.less';
3 | const widget = ({ imageSrc, href, hrefAs, children }) => {
4 | return (
5 |
6 |
7 | {children}
8 |
9 |
10 | );
11 | }
12 |
13 | export default widget;
14 |
--------------------------------------------------------------------------------
/documentation/images/captured.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/documentation/images/captured.gif
--------------------------------------------------------------------------------
/documentation/images/captured2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/documentation/images/captured2.gif
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const { PHASE_PRODUCTION_SERVER } =
2 | process.env.NODE_ENV === "development"
3 | ? {} // We're never in "production server" phase when in development mode
4 | : !process.env.NOW_REGION
5 | ? require("next/constants") // Get values from `next` package when building locally
6 | : require("next-server/constants"); // Get values from `next-server` package when building on now v2
7 |
8 | module.exports = (phase, { defaultConfig }) => {
9 | if (phase === PHASE_PRODUCTION_SERVER) {
10 | // Config used to run in production.
11 | return {};
12 | }
13 |
14 | /* eslint-disable */
15 | const withCSS = require("@zeit/next-css");
16 | const withLess = require("@zeit/next-less");
17 | const lessToJS = require("less-vars-to-js");
18 | // const withOffline = require('next-offline')
19 | const withManifest = require("next-manifest");
20 |
21 | const fs = require("fs");
22 | const path = require("path");
23 |
24 | // Where your antd-custom.less file lives
25 | const themeVariables = lessToJS(
26 | fs.readFileSync(path.resolve(__dirname, "./styles/antd.less"), "utf8")
27 | );
28 |
29 | // fix: prevents error when .less files are required by node
30 | if (typeof require !== "undefined") {
31 | require.extensions[".less"] = file => { };
32 | }
33 |
34 | // ! PWA configurations
35 | const manifest = {
36 | output: "./static",
37 | name: "NextStore",
38 | icons: [
39 | {
40 | src: "/images/logo.png",
41 | sizes: "64x64",
42 | type: "image/png"
43 | },
44 | {
45 | src: "/images/logo2.png",
46 | sizes: "144x144",
47 | type: "image/png"
48 | }
49 | ]
50 | };
51 |
52 | // const nextConfig = {
53 | // target: "serverless",
54 | // // generateInDevMode: true,
55 | // // transformManifest: manifest => ['/'].concat(manifest),
56 | // workboxOpts: {
57 | // swDest: "static/service-worker.js",
58 | // runtimeCaching: [
59 | // {
60 | // urlPattern: /^https?.*/,
61 | // handler: "networkFirst",
62 | // options: {
63 | // cacheName: "https-calls",
64 | // networkTimeoutSeconds: 15,
65 | // expiration: {
66 | // maxEntries: 150,
67 | // maxAgeSeconds: 7 * 24 * 60 * 60 // 1 week
68 | // },
69 | // cacheableResponse: {
70 | // statuses: [0, 200]
71 | // }
72 | // }
73 | // }
74 | // ]
75 | // }
76 | // };
77 |
78 | return withManifest(
79 | // withOffline(
80 | withCSS(
81 | withLess(
82 | {
83 | lessLoaderOptions: {
84 | javascriptEnabled: true,
85 | modifyVars: themeVariables
86 | },
87 | manifest: {
88 | ...manifest
89 | }
90 | // ...nextConfig
91 | }
92 | // )
93 | )
94 | )
95 | );
96 | };
97 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "NextStore",
4 | "builds": [
5 | {
6 | "src": "next.config.js",
7 | "use": "@now/next"
8 | }
9 | ],
10 | "routes": [
11 | {
12 | "src": "^/service-worker.js$",
13 | "dest": "/_next/static/service-worker.js",
14 | "headers": {
15 | "cache-control": "public, max-age=43200, immutable",
16 | "Service-Worker-Allowed": "/"
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ecommerce",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next",
7 | "build": "next build",
8 | "start": "next start",
9 | "export": "next export",
10 | "demo": "next build && next start"
11 | },
12 | "dependencies": {
13 | "@zeit/next-css": "^1.0.1",
14 | "@zeit/next-less": "^1.0.1",
15 | "antd": "^3.24.3",
16 | "axios": "^0.19.0",
17 | "babel-plugin-import": "^1.12.2",
18 | "babel-plugin-inline-import": "^3.0.0",
19 | "express": "^4.17.1",
20 | "less": "^3.10.3",
21 | "less-vars-to-js": "^1.3.0",
22 | "next": "^9.1.5",
23 | "next-manifest": "^3.0.1",
24 | "nprogress": "^0.2.0",
25 | "prop-types": "^15.7.2",
26 | "react": "^16.12.0",
27 | "react-device-detect": "^1.9.10",
28 | "react-dom": "^16.12.0",
29 | "react-loading-skeleton": "^1.2.0",
30 | "react-magnifier": "^3.0.4",
31 | "react-no-ssr": "^1.1.0",
32 | "react-responsive": "^8.0.1",
33 | "react-slick": "^0.25.2",
34 | "react-truncate": "^2.4.0"
35 | },
36 | "devDependencies": {
37 | "url-loader": "^3.0.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import App from 'next/app';
2 |
3 | // ! Don't remove this import, it will cause a problem with importing less files,
4 | // ! it's a bug with next-less package, wait for an offical update for the package
5 | import './app.less';
6 |
7 | class MyApp extends App {
8 |
9 | render() {
10 | const { Component, pageProps } = this.props
11 | return
12 | }
13 | }
14 |
15 | export default MyApp
16 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Head, Main, NextScript } from "next/document";
2 | // import Manifest from 'next-manifest/manifest';
3 | export default class EvexDocument extends Document {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
13 |
14 |
15 |
19 |
25 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/pages/_error.js:
--------------------------------------------------------------------------------
1 | import Layout from "../components/layout";
2 | import { withRouter } from "next/router";
3 |
4 | const error = props => {
5 | const { code } = props;
6 | const statusCode = code ? code : 404;
7 |
8 | return (
9 |
10 |
11 | {code
12 | ? `Something went wrong :( server responeded with ${code}`
13 | : "Page not found :("}
14 |
15 |
16 | );
17 | };
18 |
19 | export default withRouter(error);
20 |
--------------------------------------------------------------------------------
/pages/about.js:
--------------------------------------------------------------------------------
1 | import Layout from '../components/layout';
2 |
3 |
4 | export default props => (
5 |
6 | About
7 |
8 | );
--------------------------------------------------------------------------------
/pages/app.less:
--------------------------------------------------------------------------------
1 | // ! Don't remove me!
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import Layout from "../components/layout";
2 | import NoSSR from "react-no-ssr";
3 | import { Row, Col } from "antd";
4 | import Loading from "../components/loading/loading";
5 | import AdSlider from "../components/adSlider/adSlider";
6 | import TabbedItems from "../components/tabbedItems/tabbedItems";
7 | import ItemsSlider from "../components/itemsSlider/itemsSlider";
8 | import Banner from "../components/banner/banner";
9 | import Widget from "../components/widget/widget";
10 | import Partners from "../components/partners/partners";
11 | import { Fragment } from "react";
12 | import { useMediaQuery } from "react-responsive";
13 | import Meta from "../components/meta/meta";
14 |
15 | const tempItems = [
16 | {
17 | id: "1",
18 | image: "/images/item1.jpg",
19 | title: "Item name or title",
20 | slug: "item-name-slug",
21 | rating: 4,
22 | price: 250,
23 | discount: 150
24 | },
25 | {
26 | id: "7",
27 | image: "/images/item1.jpg",
28 | title: "Item name or title",
29 | slug: "item-name-slug7",
30 | rating: 4,
31 | price: 250,
32 | discount: 150
33 | },
34 | {
35 | id: "2",
36 | image: "/images/item1.jpg",
37 | title: "Item name or title",
38 | slug: "item-name-slug2",
39 | rating: 5,
40 | price: 700
41 | },
42 | {
43 | id: "3",
44 | image: "/images/item1.jpg",
45 | title: "Item name or title",
46 | slug: "item-name-slug3",
47 | rating: 4,
48 | price: 250,
49 | discount: 150
50 | },
51 | {
52 | id: "4",
53 | image: "/images/item1.jpg",
54 | title: "Item name or title",
55 | slug: "item-name-slug4",
56 | rating: 4,
57 | price: 250
58 | },
59 | {
60 | id: "5",
61 | image: "/images/item1.jpg",
62 | title: "Item name or title",
63 | slug: "item-name-slug5",
64 | rating: 4,
65 | price: 250
66 | },
67 | {
68 | id: "6",
69 | image: "/images/item1.jpg",
70 | title: "Item name or title",
71 | slug: "item-name-slug6",
72 | rating: 4,
73 | price: 250
74 | }
75 | ];
76 | const tabs = [
77 | {
78 | title: "Sales",
79 | component: (
80 |
86 | )
87 | },
88 | {
89 | title: "Best Seller",
90 | component: (
91 |
97 | )
98 | }
99 | ];
100 |
101 | const services = [
102 | {
103 | icon: "fas fa-truck",
104 | title: "Free Delivery",
105 | content: "On orders with $100 and more."
106 | },
107 | {
108 | icon: "fas fa-hand-holding-usd",
109 | title: "Money Back",
110 | content: "Money back guarantee within 30 days."
111 | },
112 | {
113 | icon: "fas fa-headset",
114 | title: "24 Hours Support",
115 | content: "Call us anytime you want."
116 | },
117 | {
118 | icon: "fas fa-gifts",
119 | title: "Gifts On Payment",
120 | content: "Earn a gift for each online payment."
121 | }
122 | ];
123 |
124 | const partners = [
125 | { id: 1, imageSrc: "/images/partner.jpg", name: "Partner" },
126 | { id: 2, imageSrc: "/images/partner.jpg", name: "Partner" },
127 | { id: 3, imageSrc: "/images/partner.jpg", name: "Partner" },
128 | { id: 4, imageSrc: "/images/partner.jpg", name: "Partner" },
129 | { id: 5, imageSrc: "/images/partner.jpg", name: "Partner" },
130 | { id: 6, imageSrc: "/images/partner.jpg", name: "Partner" },
131 | { id: 7, imageSrc: "/images/partner.jpg", name: "Partner" },
132 | { id: 8, imageSrc: "/images/partner.jpg", name: "Partner" }
133 | ];
134 |
135 | const seo = {
136 | title: "NextStore",
137 | meta_keywords:
138 | "Store,NextStore,NextJS, NextJS Ecommerce,Open Source Ecommerce,ReactJS Ecommerce",
139 | meta_description: "The best NextJS ECommerce open source project",
140 | robots: "index,follow"
141 | };
142 |
143 | const Index = () => {
144 | const featuredTitle = Featured Items ;
145 | return (
146 |
147 |
148 |
154 |
155 |
156 |
157 | }>
158 |
159 |
160 |
161 |
162 |
163 |
164 |
}>
165 |
166 | {/*
167 |
168 | banner title
169 |
170 |
171 | Some banner text here.
172 |
173 |
*/}
174 |
175 |
176 |
177 |
178 |
179 |
180 |
185 |
186 |
Some Title Here
187 |
188 | some text here to describe an advertisement or something.
189 |
190 |
191 |
192 |
193 |
194 |
200 |
201 |
202 |
203 |
204 |
205 | }>
206 | {services.map((service, idx) => (
207 |
208 |
209 |
210 |
211 |
212 |
213 |
{service.title}
214 |
{service.content}
215 |
216 |
217 |
218 | ))}
219 |
220 |
221 |
222 |
223 |
224 |
}>
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 | );
233 | };
234 |
235 | export default Index;
236 |
--------------------------------------------------------------------------------
/pages/items/[slug].js:
--------------------------------------------------------------------------------
1 | import { Fragment } from "react";
2 | import Layout from "../../components/layout";
3 | import { Row, Col } from "antd";
4 | import Meta from "../../components/meta/meta";
5 | import { useRouter } from "next/router";
6 | import { DOMAIN_URL } from "../../utils/URLS";
7 |
8 | // * NextJS can handle routing in multiple ways
9 | // * in fact you can modify it via express server as well
10 | // * but it recommends 2 ways
11 | // * first use query params, which is easy but confusing when you want a slashed url
12 | // * the second one called "clean url"
13 | // * which what am using here, its to treat this "dynamic" page that depends on a param received from the route
14 | // * adding [] to the file name will make next relize that this page will recieve a prop inside props.query (like query param) named as the file name inside the []
15 | // * this page is called [slug].js, so the prop will be named slug
16 | // ! remember that the folder pathing is important, since now inside items folder, it will be as followed /items/[slug]
17 |
18 | const fake = {
19 | seo: {
20 | title: "Mobile Phone",
21 | meta_keywords: "Mobile,Phone,Mobile Phone",
22 | meta_description: "Fast, cheap and strong mobile phone.",
23 | robots: "index,follow",
24 | og_type: "product",
25 | og_image: "/images/partner.jpg",
26 | og_description: "The best mobile phone ever."
27 | },
28 | name: ""
29 | };
30 | const Item = ({ slug }) => {
31 | const { seo } = fake;
32 | return (
33 |
34 |
35 |
46 |
47 |
48 |
49 | LEFT SIDE
50 |
51 |
52 | RIGHT SIDE
53 |
54 |
55 |
56 |
57 |
58 | );
59 | };
60 | Item.getInitialProps = async props => {
61 | const { slug } = props.query;
62 | // * We can fetch the item using the slug recieved.
63 | return {
64 | slug
65 | };
66 | };
67 |
68 | export default Item;
69 |
--------------------------------------------------------------------------------
/pages/search.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import Layout from '../components/layout';
3 | import axios from '../utils/axios-instance';
4 | import {
5 | Row,
6 | Col,
7 | Pagination,
8 | } from 'antd';
9 | import Filters from '../components/searchPage/searchFilter';
10 | import Controls from '../components/searchPage/searchControls';
11 | import Product from '../components/product/product';
12 | import '../styles/searchPage.less';
13 | import Router from 'next/router';
14 |
15 |
16 |
17 | const search = props => {
18 | const { query, items = [], error } = props;
19 | const [display, setDisplay] = useState('list');
20 | const paginateHandler = value => {
21 | console.log(value);
22 | const obj = { ...query };
23 | obj.skip = (value - 1) * obj._limit;
24 |
25 | searchHandler(obj);
26 | }
27 |
28 | const searchHandler = obj => {
29 | let search = '';
30 | Object.keys(obj).forEach(key => search += `${key}=${obj[key]}&`);
31 | search = search.slice(0, -1);
32 | Router.push(`/search?${search}`);
33 | }
34 |
35 | const displayChangedHandler = type => {
36 | setDisplay(type);
37 | }
38 | return (
39 |
40 |
41 | {/*
{query._title} */}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
52 |
53 |
54 | {items.map(item => {
55 | // ! just faking some data that jsonplaceholder don't return
56 | item.price = 200;
57 | item.image = '/images/item1.jpg';
58 | item.discount = item.id % 3 === 0 ? 150 : null;
59 | if (display === 'list') {
60 | return
61 |
62 |
63 | }
64 |
65 | return
66 |
67 | ;
68 | })}
69 |
70 |
71 |
76 |
77 |
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | search.getInitialProps = async ({ query }) => {
85 | try {
86 | let search = '';
87 | // ? why using this?
88 | // * to map through the query and assign a query string/
89 | Object.keys(query).forEach(key => search += `${key}=${query[key]}&`);
90 |
91 | search = search.slice(0, -1);
92 | // * get request from jsonplaceholder to get posts matching this fake query.
93 | const response = await axios.get(`posts?${search}`);
94 | return { query, items: response.data }
95 | } catch (error) {
96 |
97 | // ! this can be changed to your prefering, what am doing here is
98 | // ! checking the status code to handle errors (I think it's better for multilanguage)
99 | return { query, error: error.response ? error.response.status : '500' };
100 | }
101 |
102 |
103 | }
104 |
105 | export default search;
106 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/banner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/images/banner.gif
--------------------------------------------------------------------------------
/public/images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/images/banner.png
--------------------------------------------------------------------------------
/public/images/item1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/images/item1.jpg
--------------------------------------------------------------------------------
/public/images/item2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/images/item2.jpg
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/images/logo.png
--------------------------------------------------------------------------------
/public/images/logo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/images/logo2.png
--------------------------------------------------------------------------------
/public/images/partner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/images/partner.jpg
--------------------------------------------------------------------------------
/public/images/sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/images/sample.jpg
--------------------------------------------------------------------------------
/public/images/sample2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/images/sample2.jpg
--------------------------------------------------------------------------------
/public/images/slider.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohammadou1/nextstore/413efff1abe6e7c7640bd9f262f3e89c984cd54d/public/images/slider.jpg
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "NextStore",
3 | "short_name": "NextStore",
4 | "start_url": "/",
5 | "display": "fullscreen",
6 | "background_color": "#1f4c94",
7 | "theme_color": "#1f4c94",
8 | "description": "NextJS eCommerce.",
9 | "icons": [
10 | {
11 | "src": "/images/logo.png",
12 | "sizes": "64x64",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "/images/logo2.png",
17 | "sizes": "144x144",
18 | "type": "image/png"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const next = require('next');
2 | const express = require('express');
3 | const dev = process.env.NODE_ENV !== 'production';
4 | const port = process.env.PORT || 3000;
5 | const app = next({ dev });
6 | const handle = app.getRequestHandler();
7 |
8 |
9 | app.prepare().then(() => {
10 | const server = express();
11 |
12 |
13 | server.get('*', (req, res) => handle(req, res));
14 |
15 | server.listen(port, err => {
16 | if (err) throw err;
17 | console.log(port);
18 | });
19 | });
--------------------------------------------------------------------------------
/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "NextStore",
3 | "short_name": "Short name for Icon and Task",
4 | "icons": [
5 | {
6 | "src": "/images/logo.png",
7 | "sizes": "64x64",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/images/logo2.png",
12 | "sizes": "144x144",
13 | "type": "image/png"
14 | }
15 | ],
16 | "start_url": "/?utm_source=web_app_manifest",
17 | "display": "standalone",
18 | "orientation": "natural",
19 | "background_color": "#FFFFFF",
20 | "theme_color": "#FFFFFF"
21 | }
22 |
--------------------------------------------------------------------------------
/styles/antd.less:
--------------------------------------------------------------------------------
1 | @import "~antd/dist/antd.less";
2 | @import "./variables.less";
3 |
4 | .ant-menu-horizontal {
5 | line-height: 75px;
6 | }
7 |
8 | .ant-select-selection__placeholder {
9 | font-size: 14px;
10 | }
11 | .ant-drawer .ant-drawer-content {
12 | background-color: @primary--color;
13 | }
14 | .ant-tabs-bar {
15 | border-bottom: 1px solid @line-color;
16 | }
17 | .ant-tabs-extra-content {
18 | float: left !important;
19 | }
20 | .ant-card-head {
21 | border-bottom: none;
22 | }
23 |
--------------------------------------------------------------------------------
/styles/index.less:
--------------------------------------------------------------------------------
1 | @import "./antd.less";
2 |
3 | body,
4 | html {
5 | font-family: "Raleway", sans-serif;
6 | }
7 |
8 | a:hover {
9 | color: @secondary--color;
10 | }
11 |
12 | .primary-color {
13 | color: @primary--color;
14 | }
15 |
16 | .secondary-color {
17 | color: @secondary--color;
18 | }
19 |
20 | .bold {
21 | font-weight: bold;
22 | }
23 |
24 | .flex {
25 | display: flex !important;
26 | }
27 |
28 | .flex-column {
29 | display: flex;
30 | flex-direction: column;
31 | line-height: 1.5;
32 | }
33 |
34 | .px {
35 | padding-left: @small-value;
36 | padding-right: @small-value;
37 | }
38 |
39 | .py {
40 | padding-bottom: @small-value;
41 | padding-top: @small-value;
42 | }
43 |
44 | .px-large {
45 | padding-left: @large-value;
46 | padding-right: @large-value;
47 | }
48 |
49 | .py-large {
50 | padding-bottom: @large-value;
51 | padding-top: @large-value;
52 | }
53 |
54 | .pb {
55 | padding-bottom: @small-value;
56 | }
57 |
58 | .pt {
59 | padding-top: @small-value;
60 | }
61 |
62 | .pl {
63 | padding-left: @small-value;
64 | }
65 |
66 | .pr {
67 | padding-right: @small-value;
68 | }
69 |
70 | .mt {
71 | margin-top: @small-value;
72 | }
73 |
74 | .mb {
75 | margin-bottom: @small-value;
76 | }
77 |
78 | .mt-large {
79 | margin-top: @large-value;
80 | }
81 |
82 | .mb-large {
83 | margin-bottom: @large-value;
84 | }
85 |
86 | .ml-large {
87 | margin-left: @large-value;
88 | }
89 |
90 | .mr-large {
91 | margin-right: @large-value;
92 | }
93 |
94 | .img-fluid {
95 | max-width: 100%;
96 | }
97 |
98 | .justify-content-center {
99 | justify-content: center;
100 | }
101 |
102 | .align-items-center {
103 | align-items: center;
104 | }
105 |
106 | .mobile-flex-center {
107 | @media only screen and (max-width: 992px) {
108 | text-align: center;
109 | }
110 | }
111 |
112 | .slick-arrow {
113 | position: absolute;
114 | z-index: 5;
115 | background-color: #ffffff;
116 | padding: 40px 0;
117 | width: 35px;
118 | font-size: 30px;
119 | color: black;
120 | display: flex;
121 | align-items: center;
122 |
123 | &:hover {
124 | background-color: transparent;
125 | color: lighten(black, 25%);
126 | }
127 |
128 | &::before {
129 | display: none;
130 | }
131 |
132 | @media (max-width: 992px) {
133 | display: none;
134 | }
135 | }
136 |
137 | .slick-prev {
138 | left: 0;
139 | }
140 |
141 | .slick-next {
142 | right: 0;
143 | justify-content: flex-end;
144 | }
145 |
146 | h1,
147 | h2 {
148 | font-size: 22px;
149 | color: @text-color;
150 | }
151 |
152 | .text {
153 | color: @text-color;
154 | }
155 |
156 | .text-right {
157 | text-align: right;
158 | }
159 |
160 | .text-left {
161 | text-align: left;
162 | }
163 |
164 | .text-center {
165 | text-align: center;
166 | }
167 |
168 | .slick-slide img {
169 | max-width: 100%;
170 | }
171 |
172 | .star-filled {
173 | color: @star-filled;
174 | }
175 |
176 | .star-not-filled {
177 | color: @star-not-filled;
178 | }
179 |
180 | .inline-block {
181 | display: inline-block;
182 | }
183 |
184 | .float-left {
185 | float: left;
186 | }
187 |
188 | .float-right {
189 | float: right;
190 | }
191 |
192 | .relative {
193 | position: relative;
194 | }
195 |
196 | .sticky {
197 | position: sticky;
198 | top: 0%;
199 | z-index: 999;
200 | }
201 |
202 | .overlay {
203 | position: absolute;
204 | left: 0;
205 | right: 0;
206 | top: 0;
207 | bottom: 0;
208 | }
209 |
210 | .banner {
211 | overflow: hidden;
212 | min-width: 100%;
213 |
214 | img {
215 | min-width: 100%;
216 | }
217 | }
218 |
219 | .discounted {
220 | text-decoration: line-through;
221 | color: lighten(@text-color, 10%);
222 | margin: 0 10px;
223 | font-size: 14px;
224 | }
225 | .services {
226 | border: 1px solid @line-color;
227 | border-radius: @border-radius-base;
228 | padding: 24px 25px;
229 |
230 | @media (min-width: 992px) {
231 | .ant-col:not(:nth-child(1)) {
232 | &::before {
233 | content: " ";
234 | position: absolute;
235 | left: -15px;
236 | height: 100%;
237 | width: 1px;
238 | background-color: @line-color;
239 | }
240 | }
241 | }
242 |
243 | .service-container {
244 | position: relative;
245 | margin-top: 12px;
246 | margin-bottom: 12px;
247 | display: flex;
248 | align-items: center;
249 |
250 | .content {
251 | margin: 0 15px;
252 |
253 | h4 {
254 | color: black;
255 | font-size: 18px;
256 | margin: 0;
257 | }
258 |
259 | p {
260 | margin: 0;
261 | font-size: 15px;
262 | }
263 | }
264 |
265 | i {
266 | font-size: 28px;
267 | color: @primary--color;
268 | }
269 | }
270 | }
271 |
272 | .widget-content {
273 | background-color: rgba(0, 0, 0, 0.4);
274 | padding: 15% 12px;
275 |
276 | h4 {
277 | color: #ffffff;
278 | position: relative;
279 | margin-bottom: 10px;
280 | font-size: 18px;
281 |
282 | &::after {
283 | content: "";
284 | width: 50%;
285 | height: 2px;
286 | background-color: @secondary--color;
287 | position: absolute;
288 | left: 0;
289 | bottom: -5px;
290 | display: block;
291 | }
292 | }
293 |
294 | p {
295 | color: #ffffff;
296 | align-self: center;
297 | margin-top: 25%;
298 | font-size: 15px;
299 | }
300 | }
301 |
302 | .container {
303 | margin-left: auto;
304 | margin-right: auto;
305 | max-width: 100%;
306 |
307 | @media only screen and (min-width: 1500px) {
308 | width: 1500px;
309 | }
310 |
311 | @media only screen and (min-width: 1400px) and (max-width: 1500px) {
312 | width: 1400px;
313 | }
314 |
315 | @media only screen and (min-width: 1300px) and (max-width: 1400px) {
316 | width: 1250px;
317 | }
318 |
319 | @media only screen and (min-width: 1200px) and (max-width: 1300px) {
320 | width: 1150px;
321 | }
322 |
323 | @media only screen and (min-width: 992px) and (max-width: 1200px) {
324 | width: 970px;
325 | }
326 |
327 | @media only screen and (min-width: 568px) and (max-width: 992px) {
328 | width: 90%;
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/styles/searchPage.less:
--------------------------------------------------------------------------------
1 | @import "./variables.less";
2 |
3 | .search-results {
4 | padding: 10px;
5 | .search-controls {
6 | overflow: hidden;
7 | padding: 10px;
8 |
9 | button {
10 | padding: 0;
11 | margin-left: 10px;
12 | font-size: 24px;
13 | }
14 |
15 | .ant-select {
16 | min-width: 100px;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/styles/variables.less:
--------------------------------------------------------------------------------
1 | @primary--color: #1f4c94;
2 | @secondary--color: #ffd200;
3 | @primary-color: #1f4c94;
4 | @layout-header-height: 51px;
5 | @layout-header-padding: 0px 0px;
6 | @layout-header-background: #f8f8f8;
7 | @layout-body-background: #ffffff;
8 | @border-radius-base: 3px;
9 | @collapse-content-bg: #ffffff;
10 | @menu-bg: transparent;
11 | @menu-item-color: #ffffff;
12 | @menu-highlight-color: @secondary--color;
13 | @upper-menu-bg: lighten(@primary--color, 10%);
14 | @border-color-split: @primary--color;
15 | @font-size-base: 17px;
16 | @dropdown-selected-color: @secondary--color;
17 | @menu-item-height: auto !important;
18 | @tabs-highlight-color: @secondary--color;
19 | @tabs-hover-color: @secondary--color;
20 | @tabs-active-color: @secondary--color;
21 | @line-color: #dbdbdb;
22 | @star-filled: #ffab00;
23 | @small-value: 5px;
24 | @large-value: 3rem;
25 | @star-not-filled: darken(@line-color, 10%);
26 | @text-color: rgba(0, 0, 0, 0.65);
27 | @card-head-background: #f0f0f0;
28 | @card-head-color: black;
29 |
--------------------------------------------------------------------------------
/utils/URLS.js:
--------------------------------------------------------------------------------
1 | export const DOMAIN_URL = "localhost:3000";
2 |
--------------------------------------------------------------------------------
/utils/axios-instance.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 |
4 | // * Creating an instance of axios to shorten requests.
5 | export default axios.create({
6 | baseURL: 'https://jsonplaceholder.typicode.com/'
7 | });
8 |
--------------------------------------------------------------------------------