Kim-Xu617 / shop-1

0 stars 0 forks source link

eCommerce #1

Open Kim-Xu617 opened 4 months ago

Kim-Xu617 commented 4 months ago

Client

Install react app make it React app react router dom to change router in App.js

import {
  createBrowserRouter,
  RouterProvider,
  Outlet,
} from "react-router-dom";

Navbar component and Footer component is fixed, we use Outlet in react-router-dom

const Layout = () => {
  return (
    <div className="app">
      <Navbar/>
      <Outlet />
      <Footer />
    </div>
  );
};
function App() {

  const router = createBrowserRouter([
    {
      path: "/",
      element: <Layout/>,
      children:[
        {
          path: "/",
          element: < Home/>,
        },
        {
          path: "/products/:id",
          element: < Products />,
        },
        {
          path: "/product/:id",
          element: <Product />,
        },
        {
          path: "/login",
          element: <Login/>,
        },
        {
          path: "/signup",
          element: <Signup />,
        }
      ],
    },
  ]);
  return (
    <div>
      <RouterProvider router={router} />
    </div>
  );
}

export default App;

First we only use Home page, product page and products page for createBrowserRouter. List all of them as below. { path: "/", element: < Home /> } But since Navbar and footer are fixed, we utilize and make element as and change as children to replace only middle part of the page.

Kim-Xu617 commented 4 months ago

UI design: components

Navbar Material Icons: https://mui.com/material-ui/material-icons/

import React, { useState } from 'react'
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import SearchIcon from '@mui/icons-material/Search';
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import ShoppingCartOutlinedIcon from '@mui/icons-material/ShoppingCartOutlined';
import {Link} from 'react-router-dom'
import './Navbar.scss'
import Cart from '../Cart/Cart';
import {useDispatch, useSelector} from 'react-redux'
import {onChangeLoggedIn} from "../../redux/authReducer"

const Navbar = () => {
  const [open, setOpen] = useState(false)
  const dispatch = useDispatch()
  const isLoggedIn =  useSelector(state=>state.auth.isLoggedIn)
  console.log("isLoggedIn"+isLoggedIn)
  const number = useSelector(state=>state.cart.products.length)
  return (
    <div className='Navbar'>
      <div className="wrapper">
        <div className='left'>
          <div className="item">
            <img src='/img/en.png' alt='nation'></img>
            <KeyboardArrowDownIcon/>
          </div>
          <div className="item">
            <span>USD</span>
            <KeyboardArrowDownIcon/>
          </div>
          <div className="item">
            <Link to="/products/1" className='link'>Women</Link>
          </div>
          <div className="item">
            <Link to="/products/2" className='link'>Men</Link>
          </div>
          <div className="item">
            <Link to="/products/3" className='link'>Children</Link>
          </div>
        </div>
        <div className='center'>
          <Link to="/" className='link'>LAMASHOP</Link>
        </div>
        <div className='right'>
          <div className="item">
            <Link to="/products/3" className='link'>Homepage</Link>
          </div>
          <div className="item">
            <Link to="/products/3" className='link'>About</Link>
          </div>

          {isLoggedIn?
            (<div className="item">
            <button onClick={()=>
              dispatch(
                onChangeLoggedIn()
              )}>
              Logout
              </button>
          </div>)
            :
            (<div className="item">
              <Link to="/login">Login</Link>
            </div>)

          }
          <div className="item">
            <Link to="/products/3" className='link'>Stores</Link>
          </div>
          <div className="icons">
            <SearchIcon/>
            <PersonOutlineIcon/>
            <FavoriteBorderIcon/>
            <div className="cartIcon" onClick={()=>{setOpen(!open)}}>
              <ShoppingCartOutlinedIcon/>
              <span>{number}</span>
            </div>
          </div>
        </div>
      </div>
      {open&&<Cart/>}
    </div>
  )
}

export default Navbar

Style scss: https://sass-lang.com/

.Navbar{
    height: 80px;

    .wrapper{
        padding: 10px 30px;
        display: flex;
        justify-content: space-between;
        align-items: center;

        .item{
            display: flex;
            align-items: center;
            font-size: 18px;
        }

        .left{
            display: flex;
            align-items: center;
            gap: 20px;
            letter-spacing: 2px;
        }

        .center{
            align-items: center;
            font-size: 30px;
        }

        .right{
            display: flex;
            align-items: center;
            gap: 20px;

            .icons{
                display: flex;
                gap: 15px;
                cursor: pointer;
                color: #777;

                .cartIcon{
                    position: relative;
                    span{
                        font-size: 12px;
                        height: 20px;
                        width: 20px;
                        border-radius: 50%;
                        background-color:#2879FE;
                        color: white;
                        align-items: center;
                        justify-content: center;
                        position: absolute;
                        top: -10px;
                        right: -10px;
                        display: flex;
                    }
                }
            }
        }
    }
}

Footer

import React from 'react'
import './Footer.scss'

const Footer = () => {
  return (
    <div className='footer'>
      <div className='top'>
        <div className="item">
          <h1>Categories</h1>
          <span>Women</span>
          <span>Men</span>
          <span>Shoes</span>
          <span>Accessories</span>
          <span>New Arrivals</span>
        </div>
        <div className="item">
          <h1>Links</h1>
          <span>FAQ</span>
          <span>Pages</span>
          <span>Stores</span>
          <span>Compare</span>
          <span>Cookies</span>
        </div>
        <div className="item">
          <h1>About</h1>
          <span>
            Lorem ipsum dolor sit amet conse ctetur adipisicing elit, sed do
            eiusmod tempor incididunt ut labore et dolore. Lorem ipsum dolor sit
            amet conse ctetur adipisicing elit, seddo eiusmod tempor incididunt
            ut labore etdolore.
          </span>
        </div>
        <div className="item">
          <h1>Contact</h1>
          <span>
            Lorem ipsum dolor sit amet conse ctetur adipisicing elit, sed do
            eiusmod tempor incididunt ut labore et dolore. Lorem ipsum dolor sit
            amet conse ctetur adipisicing elit, seddo eiusmod tempor incididunt
            ut labore etdolore.
          </span>
        </div>
      </div>
      <div className="bottom">
        <div className="left">
          <span className="logo">Lamastore</span>
          <span className="copyright">
            © Copyright 2023. All Rights Reserved
          </span>
        </div>
        <div className="right">
          <img src="/img/payment.png" alt="Payment method" />
        </div>
      </div>
    </div>
  )
}

export default Footer
.footer{
    margin: 100px 200px 20px 200px;
    .top{
        display: flex;
        gap: 50px;

        .item{
            flex:1;
            display: flex;
            flex-direction: column;
            text-align: justify;
            gap: 10px;
            font-size: 14px;

            h1{
                font-size: 18px;
                font-weight: 500;
                color: #555;
            }

            span{
                color: gray;
            }
        }
    }
    .bottom{
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-top: 50px;

        .left{
            display: flex;
            align-items: center;

            .logo{
                color: #2879fe;
                font-weight: bold;
                font-size: 24px;
            }

            .copyright{
                margin-left: 20px;
                font-size: 12px;
                color: gray;
              }
        }

        .right{
            display: flex;
            align-items: center;

            img{
                height: 50px;
            }
        }
    }

}

Slider

import React, { useState } from 'react'
import "./Slider.scss"
import EastOutlinedIcon from "@mui/icons-material/EastOutlined";
import WestOutlinedIcon from "@mui/icons-material/WestOutlined";

const Slider = () => {
    const data = [
        "https://images.pexels.com/photos/1549200/pexels-photo-1549200.jpeg?auto=compress&cs=tinysrgb&w=1600",
        "https://images.pexels.com/photos/949670/pexels-photo-949670.jpeg?auto=compress&cs=tinysrgb&w=1600",
        "https://images.pexels.com/photos/837140/pexels-photo-837140.jpeg?auto=compress&cs=tinysrgb&w=1600",
        ];
    const [currentSlide, setCurrentSlide] = useState(0)
    const prevSlide = () =>{
        setCurrentSlide(currentSlide===0? 2: currentSlide-1)
    }
    const nextSlide = () =>{
        setCurrentSlide(currentSlide===2? 0: currentSlide+1)
    }
  return (
    <div>
        <div className="slider">
            <div className="container" style={{transform:`translateX(-${100*currentSlide}vw)`}}>
                <img src={data[0]} alt="slider 1st image" />
                <img src={data[1]} alt="slider 2nd image" />
                <img src={data[2]} alt="slider 3th image" />
            </div>
            <div className="icons">
                <div className="icon" onClick={prevSlide}>
                    <WestOutlinedIcon/>
                </div>
                <div className="icon" onClick={nextSlide}>
                    <EastOutlinedIcon/>
                </div>
            </div>
        </div>
    </div>
  )
}

export default Slider
.slider{
    height: calc(100vh - 80px);
    width: 100vw;
    position: relative;
    overflow: hidden;

    .container{
        width: 300vw;
        height: 100%;
        transition: all 1s ease;

        img{
            width: 100vw;
            height: 100%;
            object-fit: cover;
        }
    }
    .icons{
        position: absolute;
        left: 0px;
        right: 0px;
        bottom: 100px;
        margin: auto;
        width: fit-content;
        display: flex;
        gap: 10px;

        .icon{
            border: 1px solid #999;
            width: 50px;
            height: 50px;
            justify-content: center;
            align-items: center;
            display: flex;
            cursor: pointer;
        }
    }
}

FeaturedProducts

import React from 'react'
import './FeaturedProducts.scss'
import Card from '../Card/Card.jsx'
import useFetch from '../../hooks/useFetch.js'

export const FeaturedProducts = ({type}) => {
    const {data, loading, error} = useFetch(`/products?populate=*&filters[type][$eq]=${type}`)
  return (
    <div className='featureProducts'>
        <div className="top">
            <h1>{type} Products</h1>
            <p>
                Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
                eiusmod tempor incididunt ut labore et dolore magna aliqua. Quis ipsum
                suspendisse ultrices gravida. Risus commodo viverra maecenas accumsan
                lacus vel facilisis labore et dolore magna aliqua. Quis ipsum
                suspendisse ultrices gravida. Risus commodo viverra maecenas.
            </p>
        </div>
        <div className="bottom">
            {   error 
                    ? "Something went error" 
                    : loading 
                    ? "loading" 
                    : data?.map(item=><Card item={item} key={item.id}/>)
            }
        </div>
    </div>
  )
}
.featureProducts{
    margin: 100px 200px;

    .top{
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-bottom: 50px;

        h1{
            flex:2;
            text-transform: capitalize;
        }

        p{
            flex:3;
            color: grey;
        }
    }

    .bottom{
        display: flex;
        justify-content: center;
        gap: 50px;
    }
}

Card

import "./Card.scss";
import {Link} from 'react-router-dom'

import React from 'react'

const Card = ({item}) => {
  return (
    <Link className="link" to={`/product/${item.id}`}>
      <div className="card">
        <div className="image">
          {item.attributes.isNew&&<span>New Season</span>}
          <img 
            src={
              process.env.REACT_APP_UPLOAD_URL+item.attributes?.img?.data?.attributes?.url
            } 
            className="mainImg" 
            alt="item 1" 
          />
          {item.attributes.img2&&<img 
            src={
              process.env.REACT_APP_UPLOAD_URL+item.attributes?.img2?.data?.attributes?.url
            } 
            className="secondImg" 
            alt="item 2" />
          }
        </div>
        <h2>{item.attributes.title}</h2>
        <div className="prices">
          <h3>${item.attributes.price + 20}</h3>
          <h3>${item.attributes.price}</h3>
        </div>
      </div>
    </Link>

  )
}

export default Card
.card{
  display: flex;
  flex-direction: column;
  width: 280px;
  gap: 10px;
  margin-bottom: 50px;

  .image{
    height: 400px;
    width: 100%;
    overflow: hidden;
    position: relative;

    span{
      z-index: 3;
      position: absolute;
      top:5px;
      left: 5px;
      background-color: white;
      color: teal;
      font-size: 12px;
      font-weight: 500;
      padding: 3px 5px;
    }

    &:hover{
      .secondImg{
        z-index: 2;
      }
    }
    .mainImg{
      z-index: 1;
    }
    img{
      width: 100%;
      height: 100%;
      object-fit: cover;
      position: absolute;
    }
  }

  h2{
    font-size: 16px;
    font-weight: 400;
    margin: 0px;
  }
  .prices{
    display: flex;
    gap: 20px;

    h3{
      font-size: 18px;
      font-weight: 500;
      margin: 0px;

      &:first-child{
        color: grey;
        text-decoration: line-through;
      }
    }
  }
}
import React from 'react'
import "../Catergories/Catergories.scss"
import {Link} from 'react-router-dom'

const Catergories = () => {
  return (
    <div className='catergories'>
        <div className="col">
            <div className="row">
                <img
                    src="https://images.pexels.com/photos/818992/pexels-photo-818992.jpeg?auto=compress&cs=tinysrgb&w=1600"
                    alt=""
                />     
                <Link to="products/1" className='link'>
                    <button>
                        Sale
                    </button>
                </Link>
            </div>
            <div className="row">
                <img
                    src="https://images.pexels.com/photos/2036646/pexels-photo-2036646.jpeg?auto=compress&cs=tinysrgb&w=1600"
                    alt=""
                />
                <Link to="products/1" className='link'>
                    <button>
                        Women
                    </button>
                </Link>
            </div>
        </div>
        <div className="col">
            <div className="row">
                <img
                    src="https://images.pexels.com/photos/1813947/pexels-photo-1813947.jpeg?auto=compress&cs=tinysrgb&w=1600"
                    alt=""
                />       
                <Link to="products/3" className='link'>
                    <button>
                        New Season
                    </button>
                </Link>

          </div>
        </div>
        <div className="col col-l">
            <div className="row">
                <div className="col">
                    <div className="row">
                        <img
                            src="https://images.pexels.com/photos/1192609/pexels-photo-1192609.jpeg?auto=compress&cs=tinysrgb&w=1600"
                            alt=""
                        />
                        <Link to="products/2" className='link'>
                            <button>
                                Men
                            </button>
                        </Link>
                    </div>
                </div>
                <div className="col">
                    <div className="row">
                        <img
                            src="https://images.pexels.com/photos/2703202/pexels-photo-2703202.jpeg?auto=compress&cs=tinysrgb&w=1600"
                            alt=""
                        />
                        <Link to="products/5" className='link'>
                            <button>
                                Accessories
                            </button>
                        </Link>
                    </div>
                </div>
            </div>
            <div className="row">
                <img
                    src="https://images.pexels.com/photos/1159670/pexels-photo-1159670.jpeg?auto=compress&cs=tinysrgb&w=1600"
                    alt=""
                />
                <Link to="products/6" className='link'>
                    <button>
                        Shoes
                    </button>
                </Link>     
            </div>
        </div>
    </div>
  )
}

export default Catergories
.catergories{
    display: flex;
    height: 80vh;
    gap:10px;
    margin: 10px;

    .col{
        flex:1;
        display: flex;
        flex-direction: column;
        gap:10px;

    }

    .col-l{
        flex:2;
    }

    .row{
        flex: 1;
        display: flex;
        gap: 10px;
        position: relative;
        overflow: hidden;

        img{
            height: 100%;
            width: 100%;
            object-fit: cover;
        }

        button{
            position: absolute;
            top:0px;
            bottom:0px;
            right:0px;
            left:0px;
            min-width: 100px;
            height: 50px;
            width: fit-content;
            margin: auto;
            cursor: pointer;
            padding: 10px;
            border: none;
            background-color: #fff;
            text-transform:uppercase;
        }
    }
}
import React from 'react'
import "./Contact.scss"
import FacebookIcon from "@mui/icons-material/Facebook";
import InstagramIcon from "@mui/icons-material/Instagram";
import TwitterIcon from "@mui/icons-material/Twitter";
import GoogleIcon from "@mui/icons-material/Google";
import PinterestIcon from "@mui/icons-material/Pinterest";

const Contact = () => {
  return (
    <div className='contact'>
        <div className="wrapper">
            <span>BE IN TOUCH WITH US</span>
            <div className="mail">
                <input type='text' placeholder='Enter your email...'></input>
                <button>JOIN US</button>
            </div>
            <div className="icons">
                <FacebookIcon/>
                <InstagramIcon/>
                <TwitterIcon/>
                <GoogleIcon/>
                <PinterestIcon/>
            </div>
        </div>
    </div>
  )
}

export default Contact
.contact{
    background-color: #2879fe;
    color: #fff;
    padding: 15px;
    display: flex;
    justify-content: center;

    .wrapper{
        width: 50%;
        display: flex;
        justify-content: space-between;
        align-items: center;

    }

    input{
        padding: 10px;
        border:none;
        border-radius: 5px 0 0 5px;
    }

    button{
        background-color: black;
        color: #fff;
        border-radius: 0 5px 5px 0;
        padding: 10px;
        border: none;
    }

    .icons{
        display: flex;
        gap:10px
    }
}
Kim-Xu617 commented 4 months ago

UI desigin: Home page

const Home = () => {
  return (
    <div className='home'>
      <Slider/>
      <FeaturedProducts type="featured"/>
      <Catergories/>
      <FeaturedProducts type="trending"/>
      <Contact/>
    </div>
  )
}

export default Home
Kim-Xu617 commented 4 months ago

UI design: Products page

import './Products.scss'
import List from '../../components/List/List'
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import  useFetch  from '../../hooks/useFetch'

const Products = () => {
  const catId = parseInt(useParams().id)
  const [maxPrice, setMaxPrice] = useState(1000)
  const [sort, setSort]=useState("asc")

  const {data, loading, error} = useFetch(`/sub-categories?[filters][categories][id][$eq]=${catId}`) 

  const [selectedSubCats, setSelectedSubCats] = useState([])

  const handleChange = (e) =>{
    const value=e.target.value;
    const isChecked = e.target.checked;

    setSelectedSubCats(
      isChecked
      ?[...selectedSubCats,value]
      :selectedSubCats.filter(item=>(item!==value))
    )
  }

  return (
    <div className='products'>
      <div className="left">
        <div className="filterItem">
          <h2>Product Catergories</h2>
          {
            data?.map(item=>(
              <div className="inputItem" key={item.id}>
                <input type="checkbox" id='1' value={item.id} onChange={handleChange}></input>
                <label htmlFor={item.id}>{item.attributes.title}</label>
              </div>
            ))
          }

        </div>
        <div className="filterItem">
          <h2>Filter by Price</h2>
          <div className="inputItem">
            <span>0</span>
            <input type='range' min="0" max="1000" onChange={(e)=>setMaxPrice(e.target.value)}/>
            <span>{maxPrice}</span>
          </div>
        </div>
        <div className="filterItem">
          <h2>Sort by</h2>
          <div className="inputItem">
            <input type='radio' id='asc' value='asc' name='price' onChange={()=>setSort("asc")}/>
            <label htmlFor='asc'>Price (Lowest first)</label>
          </div>

          <div className="inputItem">
            <input type='radio' id='desc' value='desc' name='price' onChange={()=>setSort("desc")}/>
            <label htmlFor='desc'>Price (Highest first)</label>
          </div>
        </div>
      </div>
      <div className="right">
        <img
          className="catImg"
          src="https://images.pexels.com/photos/1074535/pexels-photo-1074535.jpeg?auto=compress&cs=tinysrgb&w=1600"
          alt=""
        />
        <List catId={catId} sort={sort} maxPrice={maxPrice} subCats={selectedSubCats}/>
      </div>
    </div>
  )
}

export default Products
Kim-Xu617 commented 4 months ago

UI design components:

List component in products page

import React from 'react'
import "./List.scss"
import Card from '../Card/Card'
import useFetch from '../../hooks/useFetch'

const List = ({catId,sort,maxPrice, subCats})=>{

    const {data, loading, error} = useFetch(
        `/products?populate=*&[filters][categories][id][$eq]=${catId}
        ${subCats.map(item=>`&[filters][sub_categories][id][$eq]=${item}`
        )}&[filters][price][$lte]=${maxPrice}&sort=price:${sort}`
    );
    console.log(data)
  return (
    <div className='list'>
        {   
            error
                ? console.log(error)
                :
            loading 
                ? "loading"
                :data?.map(item => 
                <Card item={item} key={item.attributes.id}/>
        )}          
    </div>
  )
}

export default List
.list{
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
}
Kim-Xu617 commented 4 months ago

UI design: product page

import React, { useState } from 'react'
import './Product.scss'
import AddShoppingCartIcon from "@mui/icons-material/AddShoppingCart";
import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder";
import BalanceIcon from "@mui/icons-material/Balance";
import useFetch from '../../hooks/useFetch';
import { useParams } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { addToCart } from '../../redux/cartReducer';

const Product = () => {
  const [selectImg, setSelectImg] = useState("img")
  const [quantity, setQuantity] = useState(0)
  const productId = useParams().id
  const {data, loading, error} = useFetch(`/products/${productId}?populate=*`) 

  const dispatch = useDispatch()

  return (
    <div className='product'>
      {error?
      "Something went wrong"
      :loading? "loading" 
      :(<>
        <div className="left">
          <div className="images">
            <img src={process.env.REACT_APP_UPLOAD_URL + data?.attributes?.img?.data?.attributes?.url} alt="" onClick={()=>{setSelectImg("img")}}/>
            <img src={process.env.REACT_APP_UPLOAD_URL + data?.attributes?.img2?.data?.attributes?.url} alt="" onClick={()=>{setSelectImg("img2")}} />
          </div>
          <div className="mainImg">
            <img src={process.env.REACT_APP_UPLOAD_URL + data?.attributes[selectImg].data?.attributes?.url} alt="" />
          </div>
        </div>
        <div className="right">
          <h1>{data?.attributes?.title}</h1>
          <span className='price'>${data?.attributes?.price}</span>
          <p>{data?.attributes?.desc}</p>
          <div className="quantity">
            <button onClick={()=>{setQuantity(quantity===1?1:quantity-1)}}>-</button>
            {quantity}
            <button onClick={()=>{setQuantity(quantity+1)}}>+</button>
          </div>
          <button 
            className="add" 
            onClick={()=>
              dispatch(
                addToCart({
                  id: data.id,
                  title: data.attributes.title,
                  desc: data.attributes.desc,
                  price: data.attributes.price,
                  img: data.attributes.img.data.attributes.url,
                  quantity: quantity,
                })
              )
            }>
            <AddShoppingCartIcon/>
            ADD TO CART
          </button>
          <div className="links">
            <div className="item">
              <FavoriteBorderIcon/>
              ADD TO WISHLIST
            </div>
            <div className="item">
              <BalanceIcon/>
              ADD TO COMPARE
            </div>
          </div>
          <div className="info">
            <span>Vendor: Polo</span>
            <span>Product Type: T-Shirt</span>
            <span>Tag: T-Shirt, Women, Top</span>
          </div>
          <hr />
          <div className="info">
            <span>DESCRIPTION</span>
            <hr />
            <span>ADDITIONAL INFORMATION</span>
            <hr />
            <span>FAQ</span>
          </div>
        </div>
      </>)}
    </div>
  )
}

export default Product
.product{
    padding: 20px 50px;
    display: flex;
    gap: 50px;

    .left{
        flex:1;
        display: flex;
        gap: 20px;

        .images{
            flex:1;
            img{
                width: 100%;
                height: 150px;
                object-fit: cover;
                cursor: pointer;
                margin-bottom: 10px;
            }
        }
        .mainImg{
            flex: 5;

            img{
                width: 100%;
                max-height: 800px;
                object-fit: cover;
            }
        }
    }

    .right{
        flex:1;
        display: flex;
        flex-direction: column;
        gap: 30px;

        .price{
            font-size: 30px;
            color: #2879fe;
            font-weight: 500;
        }

        p{
            font-size: 18px;
            font-weight: 300;
            text-align: justify;
        }

        .quantity{
            display: flex;
            align-items: center;
            gap: 10px;

            button{
                width: 50px;
                height: 50px;
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                border: none;
            }
        }

        .add{
            width: 250px;
            padding: 10px;
            background-color: #2879fe;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            gap: 20px;
            border: none;
            font-weight: 500;
        }

        .links{
            display: flex;
            gap: 20px;

            .item{
                display: flex;
                align-items: center;
                gap: 10px;
                color: #2879fe;
                font-size: 14px;
            }
        }

        .info{
            display: flex;
            flex-direction: column;
            gap: 10px;
            color: gray;
            margin-top: 30px;
            font-size: 14px;

            hr{
                width: 200px;
                border: 1px solid rgb(238,237,237);
                justify-content: left;
                margin-left: 0px;
            }
        }
        hr{
            width:100%;
            border: 1px solid rgb(238,237,237);
        }
    }

}
Kim-Xu617 commented 4 months ago

UI Design: cart

import React from 'react'
import './Cart.scss'
import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined";
import {useDispatch, useSelector} from 'react-redux'
import { removeItem, resetCart } from '../../redux/cartReducer';
import { makeRequest } from "../../makeRequest";

import {Elements} from '@stripe/react-stripe-js';
import {loadStripe} from '@stripe/stripe-js';

const Cart = () => {

    const products = useSelector(state=>state.cart.products)
    const total = () =>{
        let total = 0;
        products.map(item=>total+=item.price*item.quantity)
        return total.toFixed(2)
    }
    const dispatch = useDispatch()
    const stripePromise = loadStripe('pk_test_51P8mMdF11XhvAKLGcJEi5CvV6IQeVz6TyCVMKAfUJ3P0PyG3dRKzdKaxm6fHv0sODycvU3pPTs1uWMrbB2ncHMWu00Wa5dm0qM');

    const handlePayment = async() =>{
        try {
            const stripe = await stripePromise;

            const res = await makeRequest.post("/orders",{
                products,
            });

            await stripe.redirectToCheckout({
                sessionId: res.data.stripeSession.id,
            })
        } catch (error) {
            console.log(error)
        }
    }

  return (
    <div className='cart'>
        <h1>Products in your cart</h1>
        {products?.map(item=>(
            <div className="item" key={item.id}>
                <img src={process.env.REACT_APP_UPLOAD_URL +item.img} alt="" />
                <div className="details">
                    <h1>{item.title}</h1>
                    <p>{item.desc?.substring(0,100)}</p>
                    <div className="price">{item.quantity} x ${item.price}</div>
                </div>
                <DeleteOutlinedIcon 
                    className='delete' 
                    onClick={()=>dispatch(
                        removeItem(item)
                    )}/>
            </div>
        ))}
        <div className="total">
            <span>SUBTOTAL</span>
            <span>${total()}</span>
        </div>
        <button onClick={handlePayment}>PROCEED TO CHECKOUT</button>
        <span 
            className='reset'
            onClick={()=>dispatch(
                resetCart()
            )}
        >
                Reset Cart
            </span>
    </div>
  )
}

export default Cart
.cart{
    position: absolute;
    top: 80px;
    right: 20px;
    z-index: 999;
    background-color: #fff;
    padding: 20px;
    -webkit-box-shadow: -5px 6px 5px 0px rgba(0,0,0,0.75);
    -moz-box-shadow: -5px 6px 5px 0px rgba(0,0,0,0.75);
    box-shadow: -5px 6px 5px 0px rgba(0,0,0,0.75);

    h1{
        margin-bottom: 30px;
        color: gray;
        font-weight: 400;
        font-size: 24px;
    }

    .item{
        display: flex;
        align-items: center;
        gap: 20px;
        margin-bottom: 30px;

        img{
            width: 80px;
            height: 100px;
            object-fit: cover;
        }

        .details {
            h1 {
              font-size: 18px;
              font-weight: 500;
            }

            p {
              color: gray;
              margin-bottom: 10px;
              font-size: 14px;
            }

            .price {
              color: #2879fe;
            }
        }

        .delete {
            color: red;
            font-size: 30px;
            cursor: pointer;
        }
    }

    .total {
        display: flex;
        justify-content: space-between;
        font-weight: 500;
        font-size: 18px;
        margin-bottom: 20px;
    }

    button {
        width: 250px;
        padding: 10px;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 20px;
        cursor: pointer;
        border: none;
        background-color: #2879fe;
        color: white;
        font-weight: 500;
        margin-bottom: 20px;
      }

    .reset{
    color:red;
    font-size: 12px;
    cursor: pointer;
    }
}
Kim-Xu617 commented 4 months ago

API

npx create-strapi-app@latest .

image image image image image

image

Kim-Xu617 commented 4 months ago

Fetch data - Axios (https://axios-http.com/docs/intro)

makeRequest.js

import axios from "axios";

export const makeRequest = axios.create({
    baseURL: process.env.REACT_APP_API_URL,                    
    headers:{
        Authorization: "bearer "+process.env.REACT_APP_API_TOKEN
    }

})
import { useEffect, useState } from "react"
import { makeRequest } from "../makeRequest"

const useFetch = (url) =>{

    const [data, setData] = useState(null)
    const [loading, setLoading] = useState(false)
    const [error, setError] = useState(false)

    useEffect(()=>{
        const fetchData = async () =>{
            try {
                setLoading(true)
                const res = await makeRequest.get(url)
                setData(res.data.data)
            } catch (error) {
                setError(true)
            }
            setLoading(false)
        }
        fetchData()
    },[url]);

    return {data, loading, error}
}

export default useFetch;

In FeaturedProducts we need to fetch data

import React from 'react'
import './FeaturedProducts.scss'
import Card from '../Card/Card.jsx'
import useFetch from '../../hooks/useFetch.js'

export const FeaturedProducts = ({type}) => {
    const {data, loading, error} = useFetch(`/products?populate=*&filters[type][$eq]=${type}`)
  return (
    <div className='featureProducts'>
        <div className="top">
            <h1>{type} Products</h1>
            <p>
                Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
                eiusmod tempor incididunt ut labore et dolore magna aliqua. Quis ipsum
                suspendisse ultrices gravida. Risus commodo viverra maecenas accumsan
                lacus vel facilisis labore et dolore magna aliqua. Quis ipsum
                suspendisse ultrices gravida. Risus commodo viverra maecenas.
            </p>
        </div>
        <div className="bottom">
            {   error 
                    ? "Something went error" 
                    : loading 
                    ? "loading" 
                    : data?.map(item=><Card item={item} key={item.id}/>)
            }
        </div>
    </div>
  )
}

Left part of products page we need to fetch subcategory of category(url)

import './Products.scss'
import List from '../../components/List/List'
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import  useFetch  from '../../hooks/useFetch'

const Products = () => {
  const catId = parseInt(useParams().id)
  const [maxPrice, setMaxPrice] = useState(1000)
  const [sort, setSort]=useState("asc")

  const {data, loading, error} = useFetch(`/sub-categories?[filters][categories][id][$eq]=${catId}`) 

  const [selectedSubCats, setSelectedSubCats] = useState([])

  const handleChange = (e) =>{
    const value=e.target.value;
    const isChecked = e.target.checked;

    setSelectedSubCats(
      isChecked
      ?[...selectedSubCats,value]
      :selectedSubCats.filter(item=>(item!==value))
    )
  }

  return (
    <div className='products'>
      <div className="left">
        <div className="filterItem">
          <h2>Product Catergories</h2>
          {
            data?.map(item=>(
              <div className="inputItem" key={item.id}>
                <input type="checkbox" id='1' value={item.id} onChange={handleChange}></input>
                <label htmlFor={item.id}>{item.attributes.title}</label>
              </div>
            ))
          }

        </div>
        <div className="filterItem">
          <h2>Filter by Price</h2>
          <div className="inputItem">
            <span>0</span>
            <input type='range' min="0" max="1000" onChange={(e)=>setMaxPrice(e.target.value)}/>
            <span>{maxPrice}</span>
          </div>
        </div>
        <div className="filterItem">
          <h2>Sort by</h2>
          <div className="inputItem">
            <input type='radio' id='asc' value='asc' name='price' onChange={()=>setSort("asc")}/>
            <label htmlFor='asc'>Price (Lowest first)</label>
          </div>

          <div className="inputItem">
            <input type='radio' id='desc' value='desc' name='price' onChange={()=>setSort("desc")}/>
            <label htmlFor='desc'>Price (Highest first)</label>
          </div>
        </div>
      </div>
      <div className="right">
        <img
          className="catImg"
          src="https://images.pexels.com/photos/1074535/pexels-photo-1074535.jpeg?auto=compress&cs=tinysrgb&w=1600"
          alt=""
        />
        <List catId={catId} sort={sort} maxPrice={maxPrice} subCats={selectedSubCats}/>
      </div>
    </div>
  )
}

export default Products

In list component in products we need to fetch data according to props from products

import React from 'react'
import "./List.scss"
import Card from '../Card/Card'
import useFetch from '../../hooks/useFetch'

const List = ({catId,sort,maxPrice, subCats})=>{

    const {data, loading, error} = useFetch(
        `/products?populate=*&[filters][categories][id][$eq]=${catId}
        ${subCats.map(item=>`&[filters][sub_categories][id][$eq]=${item}`
        )}&[filters][price][$lte]=${maxPrice}&sort=price:${sort}`
    );
    console.log(data)
  return (
    <div className='list'>
        {   
            error
                ? console.log(error)
                :
            loading 
                ? "loading"
                :data?.map(item => 
                <Card item={item} key={item.attributes.id}/>
        )}          
    </div>
  )
}

export default List

product

import React, { useState } from 'react'
import './Product.scss'
import AddShoppingCartIcon from "@mui/icons-material/AddShoppingCart";
import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder";
import BalanceIcon from "@mui/icons-material/Balance";
import useFetch from '../../hooks/useFetch';
import { useParams } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { addToCart } from '../../redux/cartReducer';

const Product = () => {
  const [selectImg, setSelectImg] = useState("img")
  const [quantity, setQuantity] = useState(0)
  const productId = useParams().id
  const {data, loading, error} = useFetch(`/products/${productId}?populate=*`) 

  const dispatch = useDispatch()

  return (
    <div className='product'>
      {error?
      "Something went wrong"
      :loading? "loading" 
      :(<>
        <div className="left">
          <div className="images">
            <img src={process.env.REACT_APP_UPLOAD_URL + data?.attributes?.img?.data?.attributes?.url} alt="" onClick={()=>{setSelectImg("img")}}/>
            <img src={process.env.REACT_APP_UPLOAD_URL + data?.attributes?.img2?.data?.attributes?.url} alt="" onClick={()=>{setSelectImg("img2")}} />
          </div>
          <div className="mainImg">
            <img src={process.env.REACT_APP_UPLOAD_URL + data?.attributes[selectImg].data?.attributes?.url} alt="" />
          </div>
        </div>
        <div className="right">
          <h1>{data?.attributes?.title}</h1>
          <span className='price'>${data?.attributes?.price}</span>
          <p>{data?.attributes?.desc}</p>
          <div className="quantity">
            <button onClick={()=>{setQuantity(quantity===1?1:quantity-1)}}>-</button>
            {quantity}
            <button onClick={()=>{setQuantity(quantity+1)}}>+</button>
          </div>
          <button 
            className="add" 
            onClick={()=>
              dispatch(
                addToCart({
                  id: data.id,
                  title: data.attributes.title,
                  desc: data.attributes.desc,
                  price: data.attributes.price,
                  img: data.attributes.img.data.attributes.url,
                  quantity: quantity,
                })
              )
            }>
            <AddShoppingCartIcon/>
            ADD TO CART
          </button>
          <div className="links">
            <div className="item">
              <FavoriteBorderIcon/>
              ADD TO WISHLIST
            </div>
            <div className="item">
              <BalanceIcon/>
              ADD TO COMPARE
            </div>
          </div>
          <div className="info">
            <span>Vendor: Polo</span>
            <span>Product Type: T-Shirt</span>
            <span>Tag: T-Shirt, Women, Top</span>
          </div>
          <hr />
          <div className="info">
            <span>DESCRIPTION</span>
            <hr />
            <span>ADDITIONAL INFORMATION</span>
            <hr />
            <span>FAQ</span>
          </div>
        </div>
      </>)}
    </div>
  )
}

export default Product
Kim-Xu617 commented 4 months ago

Cart and Auth

Redux to centralize management of cart state React persist to persist its state

import { configureStore } from '@reduxjs/toolkit'
import cartReducer from './cartReducer'
import authReducer from './authReducer'

import {
  persistStore,
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'

const persistConfig = {
  key: 'root',
  version: 1,
  storage,
}

const persistedReducer = persistReducer(persistConfig, cartReducer)
const persistedAuthReducer = persistReducer(persistConfig, authReducer)

export const store = configureStore({
    reducer:{
        cart: persistedReducer,
        auth: persistedAuthReducer,
    },

    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware({
        serializableCheck: {
            ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
        },
        }),
})

export let persistor = persistStore(store)

cart reducer slice

import { createSlice } from '@reduxjs/toolkit'

export const cartSlice = createSlice({
  name: 'cart',
  initialState: {
    products:[]
  },
  reducers: {
    addToCart: (state, action) => {
        const item = state.products.find(item=>item.id===action.payload.id)
        if(item){
            item.quantity=action.payload.quantity+item.quantity
        }
        else{
            state.products.push(action.payload)
        }
    },
    removeItem: (state, action) => {
      state.products = state.products.filter(item=>item.id!==action.payload.id)
    },
    resetCart: (state) => {
      state.products=[]
    },
  },
})

// Action creators are generated for each case reducer function
export const { addToCart, removeItem, resetCart } = cartSlice.actions

export default cartSlice.reducer

auth reducer slice

import { createSlice } from '@reduxjs/toolkit'

export const authSlice = createSlice({
    name: 'auth',
    initialState: {
        isLoggedIn: false
    },
    reducers:{
        onChangeLoggedIn: (state) => {
            state.isLoggedIn = !state.isLoggedIn
        }
    }
})

export const {onChangeLoggedIn} = authSlice.actions
export default authSlice.reducer

Index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
//import './index.css';
import App from './App';
//import reportWebVitals from './reportWebVitals';
import {store} from './redux/store';
import {persistor} from './redux/store';
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>
);

dispatch cartslice

    <div className='cart'>
        <h1>Products in your cart</h1>
        {products?.map(item=>(
            <div className="item" key={item.id}>
                <img src={process.env.REACT_APP_UPLOAD_URL +item.img} alt="" />
                <div className="details">
                    <h1>{item.title}</h1>
                    <p>{item.desc?.substring(0,100)}</p>
                    <div className="price">{item.quantity} x ${item.price}</div>
                </div>
                <DeleteOutlinedIcon 
                    className='delete' 
                    onClick={()=>dispatch(
                        removeItem(item)
                    )}/>
            </div>
        ))}
        <div className="total">
            <span>SUBTOTAL</span>
            <span>${total()}</span>
        </div>
        <button onClick={handlePayment}>PROCEED TO CHECKOUT</button>
        <span 
            className='reset'
            onClick={()=>dispatch(
                resetCart()
            )}
        >
                Reset Cart
            </span>
    </div>

Login submit form with correct credentials dispatch authslice

import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import "../Login/Login.scss"
import axios from 'axios'
import { useNavigate } from 'react-router-dom';
import {useDispatch} from 'react-redux'
import { onChangeLoggedIn } from '../../redux/authReducer';

const Login = () => {
  const navigate = useNavigate();

  const [formData, setFormData] = useState({
    identifier: '',
    password: '',
  })

  const dispatch = useDispatch();
  const handleSubmit = (e) =>{
    e.preventDefault();
    axios
      .post('http://localhost:1337/api/auth/local', formData)
      .then(response => {
        // Handle success.
        console.log('Well done!');
        console.log('User profile', response.data.user);
        console.log('User token', response.data.jwt);
        dispatch(onChangeLoggedIn());
        navigate('/');
    })
    .catch(error => {
      // Handle error.
      console.log('An error occurred:', error.response);
      alert("Login failed")
  });
  }

  const handleChange = (e) =>{
    setFormData({
      ...formData,
      [e.target.name]:e.target.value
    })
  }

  return (
    <div className='login'>
      <h1>Sign in to LAMASTORE</h1>
      <form onSubmit={handleSubmit}>
        <div className="row">
          <div className="column">
            <h2>Username or email</h2>
            <input type='text' name="identifier" onChange={handleChange}></input>
          </div>
        </div>
        <div className="row">
          <div className="column">
            <h2>Password</h2>
            <input type="password" name="password" onChange={handleChange}></input>
          </div>
        </div>
        <div className="row">
          <button type='submit'>Sign In</button>
        </div>
        <div className="row">
          <p>Don't have an account?</p>
          <Link to="/signup">Sign up</Link>
        </div>
      </form>
    </div>
  )
}

export default Login
Kim-Xu617 commented 4 months ago

Stripe payment

Create collectionId

api > src > api > table names We have controllers, routes and services. We can write complex function is controllers. We need to make payment function in backend server. We will create a session, and save the sessionId and order into table.

Create .env file and keep stripe key there Order.js

// @ts-ignore
const stripe = require('stripe')(process.env.STRIPE_KEY);

'use strict';

/**
 * order controller
 */

const { createCoreController } = require('@strapi/strapi').factories;

module.exports = createCoreController('api::order.order',({strapi})=>({
    async create(ctx) {
        // @ts-ignore
        const {products} = ctx.request.body;

        try {
            const lineItems = await Promise.all(
                products.map(async(product)=>{
                    const item = await strapi
                        .service("api::product.product")
                        .findOne(product.id);

                        return{
                            price_data: {
                                currency: "usd",
                                product_data: {
                                  name: item.title,
                                },
                                unit_amount: item.price*100,
                              },
                            quantity: product.quantity,
                        };
                })
            )
            console.log(lineItems);
            const session = await stripe.checkout.sessions.create({
                mode: 'payment',
                success_url: `${process.env.CLIENT_URL}?success=true`,
                cancel_url: `${process.env.CLIENT_URL}?canceled=true`,
                line_items: lineItems,
                shipping_address_collection: {allowed_countries: ['US', 'CA']},
                payment_method_types: ["card"],
            });

            await strapi
            .service("api::order.order")
            .create({ 
                data: {  
                    products, 
                    stripeId: session.id 
                } 
            });

            await strapi
            .service("api::order.order")
            .create({ 
                data: {  
                    products, 
                    stripeId: session.id 
                } 
            });

            return { stripeSession: session };
        } catch (error) {
            ctx.response.status = 500
            return error;
        }
      }
}));

Cart.jsx

    const stripePromise = loadStripe('pk_test_51P8mMdF11XhvAKLGcJEi5CvV6IQeVz6TyCVMKAfUJ3P0PyG3dRKzdKaxm6fHv0sODycvU3pPTs1uWMrbB2ncHMWu00Wa5dm0qM');

    const handlePayment = async() =>{
        try {
            const stripe = await stripePromise;

            const res = await makeRequest.post("/orders",{
                products,
            });

            await stripe.redirectToCheckout({
                sessionId: res.data.stripeSession.id,
            })
        } catch (error) {
            console.log(error)
        }
    }
Kim-Xu617 commented 4 months ago

My custom code: Login

import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import "../Login/Login.scss"
import axios from 'axios'
import { useNavigate } from 'react-router-dom';
import {useDispatch} from 'react-redux'
import { onChangeLoggedIn } from '../../redux/authReducer';

const Login = () => {
  const navigate = useNavigate();

  const [formData, setFormData] = useState({
    identifier: '',
    password: '',
  })

  const dispatch = useDispatch();
  const handleSubmit = (e) =>{
    e.preventDefault();
    axios
      .post('http://localhost:1337/api/auth/local', formData)
      .then(response => {
        // Handle success.
        console.log('Well done!');
        console.log('User profile', response.data.user);
        console.log('User token', response.data.jwt);
        dispatch(onChangeLoggedIn());
        navigate('/');
    })
    .catch(error => {
      // Handle error.
      console.log('An error occurred:', error.response);
      alert("Login failed")
  });
  }

  const handleChange = (e) =>{
    setFormData({
      ...formData,
      [e.target.name]:e.target.value
    })
  }

  return (
    <div className='login'>
      <h1>Sign in to LAMASTORE</h1>
      <form onSubmit={handleSubmit}>
        <div className="row">
          <div className="column">
            <h2>Username or email</h2>
            <input type='text' name="identifier" onChange={handleChange}></input>
          </div>
        </div>
        <div className="row">
          <div className="column">
            <h2>Password</h2>
            <input type="password" name="password" onChange={handleChange}></input>
          </div>
        </div>
        <div className="row">
          <button type='submit'>Sign In</button>
        </div>
        <div className="row">
          <p>Don't have an account?</p>
          <Link to="/signup">Sign up</Link>
        </div>
      </form>
    </div>
  )
}

export default Login

Sign up

import '../Signup/Signup.scss'

import React from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'

const Signup = () => {

    const navigate = useNavigate()
    const [formData, setFormData] = useState({
        name: '',
        username: '',
        email: '',
        password: ''
      });

    const handleChange = (e)=>{
        setFormData({
            ...formData,
            [e.target.name]: e.target.value
        })
    }

    const handleSubmit = (e) =>{
        e.preventDefault();
        if(formData.password.length<6){

            alert('password at least 6 characters');
            return
        }

        if(formData.username.length<3){

            alert('username at least 3 characters');
            return
        }
        axios
        .post('http://localhost:1337/api/auth/local/register', formData)
        .then(response => {
            // Handle success.
            console.log('Well done!');
            console.log('User profile', response.data.user);
            console.log('User token', response.data.jwt);
            navigate('/login');
        })
        .catch(error => {
            // Handle error.
            console.log('An error occurred:', error.response);
            alert("Sign up failed")
        });
    }

  return (

        <div className='signup'>
            <h1>Sign up to LAMASHOP</h1>
            <form onSubmit={handleSubmit}>
                <div className='row'>
                    <div className='column'>
                        <h2>Name</h2>
                        <input type='text' name='name' onChange={handleChange}></input>
                    </div>
                    <div className='column'>
                        <h2>Username</h2>
                        <input type='text' name='username' onChange={handleChange}></input>
                    </div>
                </div>

                <div className='row'>
                    <div className="column">
                        <h2>Email</h2>
                        <input type='email' className='email' name='email' onChange={handleChange}></input>
                    </div>
                </div>
                <div className='row'>
                    <div className="column">
                        <h2>Password</h2>
                        <input type='password' className="psw" name='password' onChange={handleChange}></input>
                    </div>
                </div>
                <div className="row">
                    <button type='submit'>Create Account</button>
                </div>
                <div className="row">
                    <Link to="/Login">Sign In</Link>
                </div>
            </form>
        </div>

  )
}

export default Signup
Kim-Xu617 commented 4 months ago

Pending action:

clear cache when log out 发货地址 订单管理

Kim-Xu617 commented 4 months ago

Homepage image image image image image

Categories image

Product image