Projeto Web Ecommerce-SC
Acredito que todos os desenvolvedores passam pelo momento Ecommerce (aprendizagem ou mercado), justamente pela gama de serviços hospedados neste segmento atualmente, o que oferece praticidade aos adeptos (deliverys) e consequentemente gera empregos aos entregadores, neste projeto, busquei fazer algo funcional que ampliasse meu conhecimento com components do React
, sendo assim, foquei na utilização do styled components
e alguns Hooks
, além do contato com o Redux
e Redux Persist
, razões: tokens persistentes para o carrinho de compras… O resultado realmente me surpreendeu, não costumo implementar full stack e concluir praticamente todas as etapas de construção, até em razão da demanda x tempo, entretanto este projeto me entusiasmou a este ponto. Inclusive, também foi implementado uma página administradora ao manager do site, que também inclui análises (analytics) de vendas em relação aos meses anteriores.
As etapas identidades foram selecionadas na seguinte lista:
- Método checkout com o serviço
stripe
; - o carrinho de compras utilizando
redux toolkit
eredux persist
; - Página de gestão com querys específicas;
Em todo ecommerce é necessário um método de pagamento, é o que caracteriza esse serviço. Pensando assim, adotei o Stripe como serviço, acreditando ser um dos mais seguros e com custos interessantes. Para que essa ferramenta funcione corretamente, criei uma conta na página, o que garantiu as minhas chaves (pública e secreta), da mesma forma o stripe fornece um código para integração do método checkout. Logo, meu código ficou da seguinte forma:
import StripeCheckout from 'react-stripe-checkout'
const KEY = process.env.REACT_APP_STRIPE //public key
const [stripeToken, setStripeToken] = useState(null)
const onToken = (token) => { //pegando o token gerado e atribuindo a variavel
setStripeToken(token);
}
useEffect(()=> { // assim que o token foi gerando
const makeRequest = async () => { //função de envio ao banco de dados
const localPrice = cart.total * 100;
await publicRequest.post("/checkout/payment", {
tokenId: stripeToken.id,
amount: localPrice,
}).then((res) => {
history("/success", {state:{data:res.data}}) //se tudo deu certo com a compra, redireciona o cliente
//console.log(res.data)
}).catch((err) => {
history("/failed", {state:{data:err.response.data}}) //se algo deu errado com cartão, imprime o mal sucedido
})
}
stripeToken && makeRequest(); //se o token existe chama a função de envio
}, [stripeToken, cart.total, history])
return ( // importando o método no meu front
<StripeCheckout
name="Santa Colina" //nome site
image="https://images.vexels.com/media/users/3/200093/isolated/preview/596f0d8cb733b17268752d044976f102-icone-de-sacola-de-compras.png" //avtar
billingAddress
shippingAddress
description={`TOTAL IS $ ${cart.total}`}
amount={cart.total*100}
token={onToken} //chamando a função onToken
stripeKey={KEY} //utilizando a minha public
>
<Button>BUY NOW</Button>
</StripeCheckout>
)
De outra forma, no backend, a implementação da rota payment ficou assim:
const dotenv = require('dotenv').config()
const router = require('express').Router()
const stripe = require('stripe')(process.env.STRIPE_KEY) //secret
router.post("/payment", (req, res) => {
stripe.charges.create({ //utilizando o método charges para conectar com o serviço na página
source: req.body.tokenId,
amount: req.body.amount,
currency: "brl", //identificando a moéda corrente usd/ eur/ neste caso brl
}).then((stripeRes) => {
res.status(200).json(stripeRes) //se a resposta for ok, os dados estão aceito
}).catch((stripeError) => {
console.log(stripeError) //caso houver erro no pagamento
res.status(500).json(stripeError)
})
})
module.exports = router
Para construir o carrinho de compras, é fundamental que a responsividade ocorra, principalmente quando existe alteração na quantidade de itens ou na persistência dos itens, ou seja, quando o cliente sair da página os itens devem permanecer no carrinho. Para isso utilizei o redux
, que atualizava as incrementações no frontend quando cada função era acionada. Sendo assim, a implementação consistiu em mais de um arquivo no fonte e está melhor detalhada no código abaixo:
//page product
import { addProduct } from '../redux/cartRedux'
const [quantity, setQuantity] = useState(1);
const handleQuantity = (type) => { //função que verifica, incrementa or decrementa
if(type === 'dec'){
setQuantity(quantity-1) //se remove decrementa
if(quantity <= 1) setQuantity(1);
}else{
setQuantity(quantity+1) //se add incrementa
}
}
const handleClick = async () => { //se foi clicado para enviar ao carrinho
await userRequest.post("/cart", { //rota para criar um carrinho gerado ao banco de dados, independente de o cliente finalizar ou nao a compra, aqui temos dados para depois lançar promoçoes ou avisos ao cliente
userId: currentUser._id,
products: {productId:id, quantity:quantity},
}).then((res) => {
const _idCart = res.data._id
dispatch(addProduct({_idCart, ...product, quantity, color, size}), history("/cart")); //o dispatch para o redux acontece aqui, logo apos a linha ter sido adicionada ao banco de dados
}).catch((err) => {
console.log("this error "+ err)
})
}
<AmountContainer>
<Remove cursor="pointer" onClick={()=>handleQuantity("dec")}/>
<Amount>{quantity}</Amount>
<Add cursor="pointer" onClick={()=>handleQuantity("inc")}/>
</AmountContainer>
A página cartRedux:
import {createSlice} from "@reduxjs/toolkit" //utilizando a lib toolkit para criar o slice
import storage from 'redux-persist/lib/storage'
const cartSlice = createSlice({ //minhas variaveis de estado no meu slice
name:"cart",
initialState: {
products:[],
quantity:0,
total:0,
},
reducers:{ //eis os meus reducers, quando adicionar, ou remover
addProduct:(state, action)=> {
state.quantity += 1
state.products.push(action.payload)
state.total += action.payload.price * action.payload.quantity
},
resetSkill:(state, action) => { //esta função eu criei para limpar o carrinho
storage.removeItem('root') //removendo o persist para limpar de verdade o carrinho
state.quantity = 0
state.products = []
state.total = 0
},
removeProduct: (state, action) => { //remove o produto clicado, confesso que essa função me tomou tempo
const {product} = action.payload
let lexval = 0
for (let i = 0; i < state.quantity; i++) {
if(product._idCart === state.products[i]._idCart){
lexval = i
}
}
let decrement = product.price * product.quantity
state.products.splice(lexval, 1)
state.products = [...state.products]
state.quantity -= 1
if(state.quantity <= 0) state.quantity = 0
state.total -= decrement
if(state.total <= 0) state.total = 0
}
},
});
export const { addProduct, resetSkill, removeProduct } = cartSlice.actions; //exportando para adicionar na Product.jsx
export default cartSlice.reducer;
Entretanto, para que essas variáveis e funções sejam úteis, é fundamental a criação de um store
, onde o persistor vai ser iniciado, como mostra o fonte abaixo:
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import cartReducer from "./cartRedux";
import userReducer from "./userRedux";
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'; //funções nativas do persist
import storage from 'redux-persist/lib/storage'; //o storage deve ser iniciado para acessar a raiz em casos especificos, por exemplo: limpar carrinho.
const persistConfig = {
key: 'root', //variavel raiz que acessei anteriormente no cartRedux
version: 1,
storage,
}
const rootReducer = combineReducers({ user: userReducer, cart: cartReducer }); //como foi iniciado o cart e o user é fundamental que o combineReducers aconteça, outra função nativa que achei legal
const persistedReducer = persistReducer(persistConfig, rootReducer); //subindo o persist com as configs
export const store = configureStore({ // o store que sera importado na main, neste caso index.js
reducer: persistedReducer, //definindo os reducers
middleware:(getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoreActions: [
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER
], //ignorando as funções nativas
},
}),
});
export let persistor = persistStore(store); //exportando o persistor
E finalmente, utilizando esse store
na index page, já torna possível acessar essas funções/variáveis em toda e qualquer parte do código, o que eu considerei vantajosamente diferencial.
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { Provider } from "react-redux" //provider que passa a variavel store dentro
import {store, persistor} from './redux/store' //incluindo a store e o persistor
import { PersistGate } from 'redux-persist/integration/react' //PersistorGate que passa o persistor dentro
const root = ReactDOM.createRoot(document.getElementById('root'));
const AppView = () => (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
);
setTimeout(() => {
root.render(<AppView />);
}, 2500);
No projeto para gestão do site, implementei para além das funcionalidades convencionais (população de dados no banco, exclusão de dados e atualizações). Nesta etapa, busquei dados como, função de crescimento de novos clientes no último ano (um exemplo: jan: 4 clientes; fev: 10 clientes… dez: 20 clientes); porcentagens de ganhos ou perdas dos últimos 2 meses, a média das vendas dos últimos 3 meses e o lucro acumulado do item por mês (esta última na página do determinado produto). Em razão de já ter incluído bastante código nesta publicação, abaixo algumas imagens dos resultados para cada uma dessas etapas elicitados acima.
Sales | representa o crescimento das vendas no último mês; |
Last month | representa o crescimento das vendas no mês anterior em relação ao anterior dele; |
Mean | representa a média das vendas nos últimos três meses; |
Users Analytics | gráfico que representa o crescimento dos usuários durante uma linha do tempo de até 12 meses. |
Neste exemplo, o gráfico apresenta o valor acumulado nos 3 meses qual foi comprado o item Barrow Teddy Green, sendo 300 em julho, 900 em agosto e 600 em setembro, ao aproximar o cursor até o ponto no gráfico, uma shadowbox com todas estas informações é lançada.
Enfim, para complementar a publicação, uma animação das páginas mais expressivas do site, optei por algo mais minimalista e baseando-me em um projeto do Youtube, o qual faço referências no Github.
O fonte deste projeto pode estar sendo acessado neste repositório.