Lo que construirá, consulte la demostración en vivo y el repositorio de GitHub para obtener más información.
En la PARTE UNO de este tutorial, construimos el contrato inteligente que impulsa nuestra aplicación. Ahora construyamos la interfaz para interactuar con ella como puedes ver arriba.
No hablemos mucho, comencemos con la codificación... Comenzaremos instalando el resto de las dependencias de esta aplicación.
En su terminal, ejecute los siguientes comandos...
yarn add firebase #Database SDK yarn add @cometchat-pro/chat #Chat SDK yarn add @material-tailwind/react #UI Kit
Si ha ejecutado con éxito los comandos anteriores, pasemos a crear algunas claves privadas para Firebase y CometChat.
Para utilizar Firebase o CometChat SDK, necesitamos crear una aplicación con sus servicios. No te preocupes, esto no te costará ni un centavo. Firebase es limitado pero gratuito, es más que suficiente para ayudarlo a aprender el desarrollo completo. CometChat ofrece a sus usuarios una versión de prueba para probar su SDK y familiarizarse con el funcionamiento de su tecnología.
Creación de una aplicación con Firebase Use este ejemplo Si aún no tiene una cuenta de Firebase, cree una para usted. Después de eso, vaya a Firebase y cree un nuevo proyecto llamado freshers , luego active el servicio de autenticación de Google, como se detalla a continuación.
Firebase admite la autenticación a través de una variedad de proveedores. Por ejemplo, autenticación social, números de teléfono y el método tradicional de correo electrónico y contraseña. Debido a que usaremos la autenticación de Google en este tutorial, necesitaremos habilitarla para el proyecto que creamos en Firebase, ya que está deshabilitada de manera predeterminada. Haga clic en el método de inicio de sesión en la pestaña de autenticación de su proyecto y debería ver una lista de los proveedores actualmente admitidos por Firebase.
Súper, eso será todo para la autenticación de Firebase, generemos las claves de configuración del SDK de Firebase.
Debe ir y registrar su aplicación en su proyecto de Firebase.
En la página de descripción general del proyecto, seleccione la opción Agregar aplicación y elija Web como plataforma.
Regrese a la página de descripción general del proyecto después de completar el registro de configuración de SDK, como se muestra en la imagen a continuación.
Ahora hace clic en la configuración del proyecto para copiar las configuraciones de configuración de SDK.
Las claves de configuración que se muestran en la imagen de arriba deben copiarse en el archivo .env . Más tarde lo usaremos en este proyecto.
Cree un archivo llamado firebase.js en la carpeta src de este proyecto y pegue los siguientes códigos antes de guardar.
import { initializeApp } from 'firebase/app' import { setAlert } from './store' import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, onAuthStateChanged, } from 'firebase/auth' import { getFirestore, query, getDocs, updateDoc, collection, collectionGroup, orderBy, deleteDoc, addDoc, doc, setDoc, serverTimestamp, } from 'firebase/firestore' const firebaseConfig = { apiKey: process.env.REACT_APP_FB_AUTH_KEY, authDomain: 'fresher-a5113.firebaseapp.com', projectId: 'fresher-a5113', storageBucket: 'fresher-a5113.appspot.com', messagingSenderId: '443136794867', appId: process.env.REACT_APP_FB_APP_ID, } const app = initializeApp(firebaseConfig) const auth = getAuth(app) const db = getFirestore(app) const logInWithEmailAndPassword = async (email, password) => { try { return await signInWithEmailAndPassword(auth, email, password).then( (res) => res.user ) } catch (error) { setAlert(JSON.stringify(error), 'red') } } const registerWithEmailAndPassword = async ( email, password, fullname, phone, account, address ) => { try { const res = await createUserWithEmailAndPassword(auth, email, password) const user = res.user const userDocRef = doc(db, 'users', user.email) await setDoc(userDocRef, { uid: user.uid, fullname, email, phone, account, address, }) return user } catch (error) { setAlert(JSON.stringify(error), 'red') } } const logout = async () => { try { await signOut(auth) return true } catch (error) { setAlert(JSON.stringify(error), 'red') } } const addToOrders = async (cart) => { try { const order = { order: Math.random().toString(36).substring(2, 9).toUpperCase(), timestamp: serverTimestamp(), cart, } await addDoc( collection(db, `users/${auth.currentUser.email}`, 'orders'), order ) return order } catch (error) { setAlert(JSON.stringify(error), 'red') } } const addProduct = async (product) => { try { await addDoc( collection(db, `users/${auth.currentUser.email}`, 'products'), { name: product.name, uid: auth.currentUser.uid, email: auth.currentUser.email, price: product.price, description: product.description, account: product.account, imgURL: product.imgURL, stock: ((Math.random() * 10) | 0) + 1, timestamp: serverTimestamp(), } ) } catch (error) { setAlert(JSON.stringify(error), 'red') } } const getProducts = async () => { try { const products = query( collectionGroup(db, 'products'), orderBy('timestamp', 'desc') ) const snapshot = await getDocs(products) return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data(), price: Number(doc.data().price), })) } catch (error) { setAlert(JSON.stringify(error), 'red') } } const getProduct = async (id) => { try { const products = query( collectionGroup(db, 'products'), orderBy('timestamp', 'desc') ) const snapshot = await getDocs(products) const product = snapshot.docs.find((doc) => doc.id == id) return { id: product.id, ...product.data(), price: Number(product.data().price), } } catch (error) { setAlert(JSON.stringify(error), 'red') } } const updateProduct = async (product) => { const productRef = doc(db, `users/${product.email}/products`, product.id) try { await updateDoc(productRef, product) } catch (error) { setAlert(JSON.stringify(error), 'red') } } const deleteProduct = async (product) => { const productRef = doc(db, `users/${product.email}/products`, product.id) try { await deleteDoc(productRef) } catch (error) { setAlert(JSON.stringify(error), 'red') } } export { auth, db, logInWithEmailAndPassword, registerWithEmailAndPassword, logout, onAuthStateChanged, addProduct, addToOrders, getProducts, getProduct, updateProduct, deleteProduct, }
Eres increíble si seguiste todo correctamente. A continuación, haremos algo similar para CometChat .
Creación de una aplicación con CometChat Vaya a CometChat y regístrese si no tiene una cuenta con ellos. A continuación, inicie sesión y se le presentará la siguiente pantalla.
Use esto como ejemplo para crear una nueva aplicación con el nombre freshers haciendo clic en el botón Agregar nueva aplicación . Se le presentará un modal donde puede ingresar los detalles de la aplicación. La siguiente imagen muestra un ejemplo.
Después de la creación de su aplicación, será dirigido a su tablero, que debería verse así.
También debe copiar estas claves en el archivo .env.
Finalmente, elimine los usuarios y grupos precargados como se muestra en las imágenes a continuación.
Impresionante, eso será suficiente para las configuraciones. Utilice esta plantilla para asegurarse de que su archivo .env siga nuestra convención.
ENDPOINT_URL=<PROVIDER_URL> SECRET_KEY=<SECRET_PHRASE> DEPLOYER_KEY=<YOUR_PRIVATE_KEY> REACT_APP_COMET_CHAT_REGION=<YOUR_COMET_CHAT_REGION> REACT_APP_COMET_CHAT_APP_ID=<YOUR_COMET_CHAT_APP_ID> REACT_APP_COMET_CHAT_AUTH_KEY=<YOUR_COMET_CHAT_AUTH_KEY> REACT_APP_FB_AUTH_KEY=<YOUR_FIREBASE_AUTH_KEY> REACT_APP_FB_APP_ID=<YOUR_FIREBASE_APP_ID>
Por último, cree un nombre de archivo cometChat.js en la carpeta src de este proyecto y pegue el código a continuación en él.
import { CometChat } from '@cometchat-pro/chat' const CONSTANTS = { APP_ID: process.env.REACT_APP_COMET_CHAT_APP_ID, REGION: process.env.REACT_APP_COMET_CHAT_REGION, Auth_Key: process.env.REACT_APP_COMET_CHAT_AUTH_KEY, } const initCometChat = async () => { try { const appID = CONSTANTS.APP_ID const region = CONSTANTS.REGION const appSetting = new CometChat.AppSettingsBuilder() .subscribePresenceForAllUsers() .setRegion(region) .build() await CometChat.init(appID, appSetting).then(() => console.log('Initialization completed successfully') ) } catch (error) { console.log(error) } } const loginWithCometChat = async (UID) => { try { const authKey = CONSTANTS.Auth_Key await CometChat.login(UID, authKey).then((user) => console.log('Login Successful:', { user }) ) } catch (error) { console.log(error) } } const signInWithCometChat = async (UID, name) => { try { let authKey = CONSTANTS.Auth_Key const user = new CometChat.User(UID) user.setName(name) return await CometChat.createUser(user, authKey).then((user) => user) } catch (error) { console.log(error) } } const logOutWithCometChat = async () => { try { await CometChat.logout().then(() => console.log('Logged Out Successfully')) } catch (error) { console.log(error) } } const getMessages = async (UID) => { try { const limit = 30 const messagesRequest = await new CometChat.MessagesRequestBuilder() .setUID(UID) .setLimit(limit) .build() return await messagesRequest.fetchPrevious().then((messages) => messages) } catch (error) { console.log(error) } } const sendMessage = async (receiverID, messageText) => { try { const receiverType = CometChat.RECEIVER_TYPE.USER const textMessage = await new CometChat.TextMessage( receiverID, messageText, receiverType ) return await CometChat.sendMessage(textMessage).then((message) => message) } catch (error) { console.log(error) } } const getConversations = async () => { try { const limit = 30 const conversationsRequest = new CometChat.ConversationsRequestBuilder() .setLimit(limit) .build() return await conversationsRequest .fetchNext() .then((conversationList) => conversationList) } catch (error) { console.log(error) } } export { initCometChat, loginWithCometChat, signInWithCometChat, logOutWithCometChat, getMessages, sendMessage, getConversations, }
Genial, comencemos a integrarlos todos en nuestra aplicación, comenzaremos con los componentes.
Comencemos a elaborar todos los componentes uno tras otro, consulte siempre el repositorio de git si tiene algún desafío.
El componente de registro
Este componente es responsable de guardar nuevos usuarios en Firebase. Navegue a los componentes src >> y cree un archivo llamado Register.jsx .
import { useState } from 'react' import Button from '@material-tailwind/react/Button' import { Link, useNavigate } from 'react-router-dom' import { registerWithEmailAndPassword, logout } from '../firebase' import { signInWithCometChat } from '../cometChat' import { setAlert } from '../store' const Register = () => { const [fullname, setFullname] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [phone, setPhone] = useState('') const [address, setAddress] = useState('') const [account, setAccount] = useState('') const navigate = useNavigate() const handleRegister = async (e) => { e.preventDefault() if ( email == '' || password == '' || fullname == '' || phone == '' || account == '' || address == '' ) return registerWithEmailAndPassword( email, password, fullname, phone, account, address ).then((user) => { if (user) { logout().then(() => { signInWithCometChat(user.uid, fullname).then(() => { resetForm() setAlert('Registeration in successfully') navigate('/signin') }) }) } }) } const resetForm = () => { setFullname('') setEmail('') setPassword('') setPhone('') setAccount('') setAddress('') } return ( <div className="relative flex flex-col justify-center items-center"> <div className="mt-10 "> <form onSubmit={handleRegister} className="relative flex w-full flex-wrap items-stretch w-96 px-8" > <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="text" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Fullname" value={fullname} onChange={(e) => setFullname(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="email" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="password" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="************" value={password} onChange={(e) => setPassword(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="number" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="081 056 8262" value={phone} onChange={(e) => setPhone(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="text" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Wallet Address" value={account} onChange={(e) => setAccount(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="text" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Address" value={address} onChange={(e) => setAddress(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch justify-between items-center"> <Link className="text-green-500" to="/signin"> Already a member? sign in </Link> <Button color="green" ripple="light" type="submit"> Sign Up </Button> </div> </form> </div> </div> ) } export default Register
¡¡¡Impresionante!!!
El componente de inicio de sesión
También creemos otro componente llamado Login.jsx en la carpeta de componentes src >> y peguemos el código a continuación en él.
import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { logInWithEmailAndPassword } from '../firebase' import { loginWithCometChat } from '../cometChat' import { setAlert } from '../store' import Button from '@material-tailwind/react/Button' const Login = () => { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const navigate = useNavigate() const handleLogin = async (e) => { e.preventDefault() if (email == '' || password == '') return logInWithEmailAndPassword(email, password).then((user) => { if (user) { loginWithCometChat(user.uid).then(() => { resetForm() setAlert('Logged in successfully') navigate('/') }) } }) } const resetForm = () => { setEmail('') setPassword('') } return ( <div className="relative flex flex-col justify-center items-center"> <div className="mt-10 "> <form onSubmit={handleLogin} className="relative flex w-full flex-wrap items-stretch w-96 px-8" > <h4 className="font-semibold text-xl my-4">Login</h4> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="email" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="password" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="************" value={password} onChange={(e) => setPassword(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch justify-between items-center"> <Link className="text-green-500" to="/signup"> New user sign up </Link> <Button color="green" ripple="light" type="submit"> Sign In </Button> </div> </form> </div> </div> ) } export default Login
Genial, estos dos componentes conforman el aspecto de autenticación de esta aplicación. Posteriormente los fusionaremos en sus respectivas vistas.
El componente de encabezado
Este componente encapsula las páginas de nuestra aplicación. Fue diseñado con el Creative TIm Tailwind-Material UI Kit gratuito. Cree un archivo llamado Header.jsx dentro del directorio de componentes src >> y pegue los códigos a continuación en él.
import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { setAlert, useGlobalState } from '../store' import { logout } from '../firebase' import { logOutWithCometChat } from '../cometChat' import { connectWallet } from '../shared/Freshers' import Navbar from '@material-tailwind/react/Navbar' import NavbarContainer from '@material-tailwind/react/NavbarContainer' import NavbarWrapper from '@material-tailwind/react/NavbarWrapper' import NavbarBrand from '@material-tailwind/react/NavbarBrand' import NavbarToggler from '@material-tailwind/react/NavbarToggler' import NavbarCollapse from '@material-tailwind/react/NavbarCollapse' import Nav from '@material-tailwind/react/Nav' import NavItem from '@material-tailwind/react/NavItem' const Header = () => { const [openNavbar, setOpenNavbar] = useState(false) const [cart] = useGlobalState('cart') const [isLoggedIn] = useGlobalState('isLoggedIn') const [connectedAccount] = useGlobalState('connectedAccount') const navigate = useNavigate() const handleSignOut = () => { logout().then((res) => { if (res) { logOutWithCometChat().then(() => { setAlert('Logged out successfully') navigate('/signin') }) } }) } return ( <Navbar color="green" navbar> <NavbarContainer> <NavbarWrapper> <Link to="/"> <NavbarBrand>Freshers</NavbarBrand> </Link> <NavbarToggler color="white" onClick={() => setOpenNavbar(!openNavbar)} ripple="white" /> </NavbarWrapper> <NavbarCollapse open={openNavbar}> {isLoggedIn ? ( <Nav leftSide> <NavItem ripple="light"> <Link to="/customers">customers</Link> </NavItem> <NavItem ripple="light"> <Link to="/product/add">Add Product</Link> </NavItem> </Nav> ) : ( <></> )} <Nav rightSide> {isLoggedIn ? ( <> {connectedAccount ? null : ( <NavItem onClick={connectWallet} active="light" ripple="light" > <span className="cursor-pointer">Connect Wallet</span> </NavItem> )} <NavItem onClick={handleSignOut} ripple="light"> <span className="cursor-pointer">Logout</span> </NavItem> </> ) : ( <NavItem ripple="light"> <Link to="/signin" className="cursor-pointer"> Login </Link> </NavItem> )} <NavItem ripple="light"> <Link to="/cart">{cart.length} Cart</Link> </NavItem> </Nav> </NavbarCollapse> </NavbarContainer> </Navbar> ) } export default Header
El componente de alimentos Este componente muestra las propiedades particulares de los alimentos en una tarjeta bellamente diseñada a partir de Tailwind CSS y Material design. Cree un archivo llamado Food.jsx aún en la carpeta de componentes y pegue los siguientes códigos en él.
Cada tarjeta muestra el nombre, la imagen, la descripción, el precio y las existencias restantes de un producto alimenticio. Aquí está el código para ello.
import React from 'react' import Card from '@material-tailwind/react/Card' import CardImage from '@material-tailwind/react/CardImage' import CardBody from '@material-tailwind/react/CardBody' import CardFooter from '@material-tailwind/react/CardFooter' import H6 from '@material-tailwind/react/Heading6' import Paragraph from '@material-tailwind/react/Paragraph' import Button from '@material-tailwind/react/Button' import { setAlert, setGlobalState, useGlobalState } from '../store' import { Link } from 'react-router-dom' const Food = ({ item }) => { const [cart] = useGlobalState('cart') const addToCart = (item) => { item.added = true let cartItems = [...cart] const newItem = { ...item, qty: (item.qty += 1), stock: (item.stock -= 1) } if (cart.find((_item) => _item.id == item.id)) { cartItems[item] = newItem setGlobalState('cart', [...cartItems]) } else { setGlobalState('cart', [...cartItems, newItem]) } setAlert(`${item.name} added to cart!`) } const toCurrency = (num) => num.toLocaleString('en-US', { style: 'currency', currency: 'USD', }) return ( <div className="mx-4 my-6 w-64"> <Card> <Link to={`/product/` + item.id}> <CardImage src={item.imgURL} alt={item.name} /> </Link> <CardBody> <Link to={`/product/` + item.id}> <H6 color="gray">{item.name}</H6> </Link> <Paragraph color="gray"> Don't be scared of the truth because we need to... </Paragraph> <div color="black" className="flex flex-row justify-between items-center" > <span className="font-semibold text-green-500"> {toCurrency(item.price)} </span> <span className="text-xs text-black">{item.stock} in stock</span> </div> </CardBody> <CardFooter> {item.stock > 0 ? ( <Button onClick={() => addToCart(item)} color="green" size="md" ripple="light" disabled={item.stock == 0} > Add To Cart </Button> ) : ( <Button color="green" size="md" buttonType="outline" ripple="light" disabled > Out of Stock </Button> )} </CardFooter> </Card> </div> ) } export default Food
A continuación, veamos el componente de alimentos.
Los componentes de alimentos Este componente es responsable de representar la colección completa de datos de alimentos en nuestra base de datos. Veamos su fragmento de código.
Aún así, en el directorio de componentes , cree otro archivo llamado Foods.jsx y pegue los códigos a continuación en él.
import Food from './Food' const Foods = ({ products }) => { return ( <div className="flex flex-wrap justify-center items-center space-x-3 space-y-3 mt-12 overflow-x-hidden"> {products.map((item, i) => ( <Food item={item} key={i} /> ))} </div> ) } export default Foods
Por último, veamos el componente CartItem .
El componente CartItem
Este componente es responsable de mostrar un solo artículo en nuestra colección de carritos. Aquí está el código responsable de ello.
import { useState } from 'react' import Card from '@material-tailwind/react/Card' import CardStatusFooter from '@material-tailwind/react/CardStatusFooter' import { Link } from 'react-router-dom' import { Image, Button } from '@material-tailwind/react' import { setGlobalState, useGlobalState } from '../store' const CartItem = ({ item }) => { const [qty, setQty] = useState(item.qty) const [cart] = useGlobalState('cart') const toCurrency = (num) => num.toLocaleString('en-US', { style: 'currency', currency: 'USD', }) const increaseQty = () => { let cartItems = [...cart] const newItem = { ...item, qty: (item.qty += 1), stock: (item.stock -= 1) } cartItems[item] = newItem setGlobalState('cart', cartItems) setQty(newItem.qty) } const decreaseQty = () => { let cartItems = [...cart] if (qty == 1) { const index = cartItems.indexOf(item) cartItems.splice(index, 1) } else { const newItem = { ...item, qty: (item.qty -= 1), stock: (item.stock += 1), } cartItems[item] = newItem setQty(newItem.qty) } setGlobalState('cart', cartItems) } return ( <Card className="flex flex-row justify-between items-end my-4"> <Link to={'/product/' + item.id} className="h-12 w-12 object-contain mr-4" > <Image src={item.imgURL} alt={item.name} rounded={false} raised={true} /> </Link> <CardStatusFooter color="green" amount={toCurrency(item.price)} date={item.name} > <div className="flex flex-row justify-center items-center mx-4"> <Button color="green" buttonType="filled" size="sm" rounded={false} block={false} iconOnly={false} ripple="dark" onClick={decreaseQty} > - </Button> <span className="mx-4">{qty}</span> <Button color="green" buttonType="filled" size="sm" rounded={false} block={false} iconOnly={false} ripple="dark" onClick={increaseQty} disabled={item.stock == 0} > + </Button> </div> </CardStatusFooter> <span className="text-sm text-gray-500"> Sub Total: {toCurrency(item.price * qty)} </span> </Card> ) } export default CartItem
Felicitaciones, acaba de terminar de codificar los componentes, pasemos a crear las vistas...
Ahora que hemos creado los componentes que admiten las distintas vistas, procedamos a continuación creando las páginas individuales.
La vista de inicio
Esta vista representa la estructura del componente Alimentos. Es decir, la vista de inicio recupera toda la recolección de alimentos de firebase y los muestra en la pantalla. Echemos un vistazo a los códigos responsables de ello.
Navegue hasta el directorio de vistas y cree un archivo llamado Home.jsx , luego pegue el código a continuación dentro de él. De hecho, creará todos estos archivos en la carpeta de vistas.
import { useEffect, useState } from 'react' import Header from '../components/Header' import Foods from '../components/Foods' import { getProducts } from '../firebase' const Home = () => { const [products, setProducts] = useState([]) useEffect(() => { getProducts().then((products) => { products.filter((item) => { item.price = Number(item.price) item.qty = 0 }) setProducts(products) }) }, []) return ( <div className="home"> <Header /> <Foods products={products} /> </div> ) } export default Home
La vista del producto
Esta vista se encarga de mostrar en detalle la información sobre un producto. Desde esta página, los usuarios pueden ver, editar y eliminar productos, así como chatear con el vendedor o comprar rápidamente el alimento con Ethereum.
Aquí está el código para ello…
import Header from '../components/Header' import { useEffect, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Button, CardImage } from '@material-tailwind/react' import { getProduct, deleteProduct, auth } from '../firebase' import { setGlobalState, useGlobalState, setAlert } from '../store' import { payWithEthers } from '../shared/Freshers' const Product = () => { const { id } = useParams() const navigate = useNavigate() const [product, setProduct] = useState(null) const [cart] = useGlobalState('cart') const [isLoggedIn] = useGlobalState('isLoggedIn') const [buyer] = useGlobalState('connectedAccount') const [ethToUsd] = useGlobalState('ethToUsd') const addToCart = () => { const item = product item.added = true let cartItems = [...cart] const newItem = { ...item, qty: (item.qty += 1), stock: (item.stock -= 1) } if (cart.find((_item) => _item.id == item.id)) { cartItems[item] = newItem setGlobalState('cart', [...cartItems]) } else { setGlobalState('cart', [...cartItems, newItem]) } setAlert('Product added to cart') } const handlePayWithEthers = () => { const item = { ...product, buyer, price: (product.price / ethToUsd).toFixed(4) } payWithEthers(item).then((res) => { if (res) setAlert('Product purchased!') }) } const handleDeleteProduct = () => { deleteProduct(product).then(() => { setAlert('Product deleted!') navigate('/') }) } const toCurrency = (num) => num.toLocaleString('en-US', { style: 'currency', currency: 'USD', }) useEffect(() => { getProduct(id).then((data) => setProduct({ ...data, qty: 1 })) }, [id]) return ( <div className="product"> <Header /> {!!product ? ( <div className="flex flex-wrap justify-start items-center p-10"> <div className="mt-4 w-64"> <CardImage src={product.imgURL} alt={product.name} /> </div> <div className="mt-4 lg:mt-0 lg:row-span-6 mx-4"> <div> <h1 className="text-2xl font-extrabold tracking-tight text-gray-900 sm:text-3xl"> {product.name} </h1> <h2 className="sr-only">Product information</h2> <div className="flex flex-row justify-start items-center"> <span className="text-xl font-bold text-green-500"> {toCurrency(product.price)} </span> <span className="text-xs mx-4"> {product.stock} left in stock </span> </div> <div className="mt-2 space-y-6"> <p className="text-base text-gray-900">{product.description}</p> </div> </div> <div className="mt-4 flex flex-row justify-start items-center space-x-2"> <Button onClick={addToCart} color="green" size="md" ripple="light" > Add To Cart </Button> {isLoggedIn ? ( <> {auth.currentUser.uid != product.uid && product.account != buyer ? ( <Button onClick={handlePayWithEthers} color="amber" size="md" ripple="light" > Buy with ETH </Button> ) : null} {auth.currentUser.uid == product.uid ? null : ( <Button onClick={() => navigate('/chat/' + product.uid)} buttonType="link" color="green" size="md" ripple="light" > Chat WIth Seller </Button> )} </> ) : null} {isLoggedIn && auth.currentUser.uid == product.uid ? ( <> <Button onClick={() => navigate('/product/edit/' + id)} buttonType="link" color="green" size="md" ripple="light" > Edit Product </Button> <Button onClick={handleDeleteProduct} buttonType="link" color="red" size="md" ripple="light" > Delete </Button> </> ) : null} </div> </div> </div> ) : null} </div> ) } export default Product
La vista Agregar producto
Como su nombre lo indica, esta vista es responsable de almacenar nuevos alimentos en nuestra colección Firestore. Observe el fragmento de código a continuación...
import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { addProduct } from '../firebase' import { setAlert } from '../store' import { useGlobalState } from '../store' import Button from '@material-tailwind/react/Button' import Header from '../components/Header' const AddProduct = () => { const [name, setName] = useState('') const [price, setPrice] = useState('') const [imgURL, setImgURL] = useState('') const [description, setDescription] = useState('') const [account] = useGlobalState('connectedAccount') const navigate = useNavigate() const handleAddProduct = (e) => { e.preventDefault() if (!account) { setAlert('Please connect your metamask account!', 'red') return } if (name == '' || price == '' || imgURL == '' || description == '') return addProduct({ name, price, imgURL, description, account }).then(() => { setAlert('Product created successfully') navigate('/') }) } return ( <div className="addProduct"> <Header /> <div className="relative flex flex-col justify-center items-center"> <div className="mt-10 "> <form onSubmit={handleAddProduct} className="relative flex w-full flex-wrap items-stretch w-96 px-8" > <h4 className="font-semibold text-xl my-4">Add Product</h4> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="text" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Product Name" value={name} onChange={(e) => setName(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="number" min={1} step={0.01} className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Product Price" value={price} onChange={(e) => setPrice(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="url" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Product Image URL" value={imgURL} onChange={(e) => setImgURL(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="text" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Product Description" value={description} onChange={(e) => setDescription(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch justify-between items-center"> <Link className="text-green-500" to="/"> Back to Home </Link> <Button color="green" ripple="light" type="submit"> Save Product </Button> </div> </form> </div> </div> </div> ) } export default AddProduct
Impresionante, estamos avanzando, veamos la vista de edición del producto...
La vista Editar producto
Esta vista nos permite editar nuestros alimentos existentes. Por supuesto, debe ser el que inicialmente agregó el producto alimenticio a la tienda antes de poder editar. Solo los propietarios de productos pueden editar, veamos los códigos que realizan esta acción.
import Header from '../components/Header' import Button from '@material-tailwind/react/Button' import { useEffect, useState } from 'react' import { Link, useParams, useNavigate } from 'react-router-dom' import { updateProduct, getProduct, auth } from '../firebase' import { setAlert } from '../store' import { useGlobalState } from '../store' const EditProduct = () => { const { id } = useParams() const navigate = useNavigate() const [product, setProduct] = useState(null) const [name, setName] = useState('') const [price, setPrice] = useState('') const [imgURL, setImgURL] = useState('') const [description, setDescription] = useState('') const [account] = useGlobalState('connectedAccount') useEffect(() => { getProduct(id).then((data) => { if (auth.currentUser.uid != data.uid) navigate('/') setProduct(data) setName(data.name) setPrice(Number(data.price)) setImgURL(data.imgURL) setDescription(data.description) }) }, [id]) const handleProductUpdate = (e) => { e.preventDefault() if (!account) { setAlert('Please connect your metamask account!', 'red') return } if (name == '' || price == '' || imgURL == '' || description == '') return updateProduct({ ...product, name, price, imgURL, description, account, }).then(() => { setAlert('Product updated successfully') navigate('/product/' + product.id) }) } return ( <div className="editProduct"> <Header /> <div className="relative flex flex-col justify-center items-center"> <div className="mt-10 "> <form onSubmit={handleProductUpdate} className="relative flex w-full flex-wrap items-stretch w-96 px-8" > <h4 className="font-semibold text-xl my-4">Update Product</h4> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="text" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Product Name" value={name} onChange={(e) => setName(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="number" min={1} step={0.01} className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Product Price" value={price} onChange={(e) => setPrice(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="url" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Product Image URL" value={imgURL} onChange={(e) => setImgURL(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch mb-3"> <input type="text" className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10" placeholder="Product Description" value={description} onChange={(e) => setDescription(e.target.value)} required /> </div> <div className="relative flex w-full flex-wrap items-stretch justify-between items-center"> <Link className="text-green-500" to={`/product/` + id}> Back to product </Link> <Button color="green" ripple="light" type="submit"> Update </Button> </div> </form> </div> </div> </div> ) } export default EditProduct
Por último, para los casos relacionados con los productos, veamos la vista del carrito…
La vista del carrito
En esta vista, puede modificar y realizar sus pedidos. Una vez que realiza su pedido, se guarda inmediatamente en Firestore. A continuación se muestra cómo se escribe el código.
import CartItem from '../components/CartItem' import Header from '../components/Header' import { Link } from 'react-router-dom' import { Button } from '@material-tailwind/react' import { useEffect, useState } from 'react' import { addToOrders } from '../firebase' import { setAlert, setGlobalState, useGlobalState } from '../store' const Cart = () => { const [cart] = useGlobalState('cart') const [isLoggedIn] = useGlobalState('isLoggedIn') const [total, setTotal] = useState(0) const getTotal = () => { let total = 0 cart.forEach((item) => (total += item.qty * item.price)) setTotal(total) } const placeOrder = () => { if (!isLoggedIn) return addToOrders(cart).then((data) => { setGlobalState('cart', []) setAlert(`Order Placed with Id: ${data.order}`) }) } const clearCart = () => { setGlobalState('cart', []) } const toCurrency = (num) => num.toLocaleString('en-US', { style: 'currency', currency: 'USD', }) useEffect(() => getTotal(), [cart]) return ( <div className="addProduct"> <Header /> <div className="relative flex flex-col justify-center items-center"> {cart.length > 0 ? ( <div className="mt-10 "> <div className="relative flex w-full flex-wrap items-stretch px-8"> <div className="flex flex-wrap justify-center items-center h-64 overflow-y-scroll"> {cart.map((item, i) => ( <CartItem key={i} item={item} /> ))} </div> </div> <div className="flex flex-row justify-between items-center my-4 px-8"> <h4>Grand Total:</h4> <span className="text-sm text-green-500"> {toCurrency(total)} </span> </div> <div className="flex flex-row justify-between items-center my-4 px-8"> <Button onClick={clearCart} color="red" ripple="light" type="submit" > Clear Cart </Button> {isLoggedIn ? ( <Button onClick={placeOrder} color="green" ripple="light" type="submit" > Place Order </Button> ) : null} </div> </div> ) : ( <div className="mt-10 text-center"> <h4 className="mb-4">Cart empty, add some items to your cart</h4> <Link to="/" className="text-green-500"> Choose Product </Link> </div> )} </div> </div> ) } export default Cart
A continuación, ocupémonos de las últimas cuatro vistas en nuestra bandeja...
La vista de lista de chat
Esta vista simplemente enumera las conversaciones recientes que ha tenido con sus clientes hasta el momento. Esto es posible con la ayuda de CometChat SDK, los códigos a continuación le muestran cómo se implementó.
import Header from '../components/Header' import { getConversations } from '../cometChat' import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { auth } from '../firebase' const ChatList = () => { const [customers, setCustomers] = useState([]) const [loaded, setLoaded] = useState(false) useEffect(() => { getConversations().then((conversation) => { console.log(conversation) setCustomers(conversation) setLoaded(true) }) }, []) return ( <div className="chatList"> <Header /> <div className="flex justify-center items-center p-10"> <div className="relative mx-auto w-full"> <div className="border-0 rounded-lg relative flex flex-col w-full"> <div className="flex items-start justify-between my-4"> <h3 className="text-md font-semibold">Recent Chats</h3> </div> {loaded ? customers.map((customer, i) => ( <Conversation key={i} currentUser={auth.currentUser.uid.toLowerCase()} owner={customer.lastMessage.receiverId.toLowerCase()} conversation={customer.lastMessage} /> )) : null} </div> </div> </div> </div> ) } const Conversation = ({ conversation, currentUser, owner }) => { const possessor = (key) => { return currentUser == owner ? conversation.sender[key] : conversation.receiver[key] } const timeAgo = (date) => { let seconds = Math.floor((new Date() - date) / 1000) let interval = seconds / 31536000 if (interval > 1) { return Math.floor(interval) + 'yr' } interval = seconds / 2592000 if (interval > 1) { return Math.floor(interval) + 'mo' } interval = seconds / 86400 if (interval > 1) { return Math.floor(interval) + 'd' } interval = seconds / 3600 if (interval > 1) { return Math.floor(interval) + 'h' } interval = seconds / 60 if (interval > 1) { return Math.floor(interval) + 'm' } return Math.floor(seconds) + 's' } return ( <Link to={'/chat/' + possessor('uid')} className="flex flex-row justify-between items-center mb-2 py-2 px-4 bg-gray-100 rounded-lg cursor-pointer" > <div className=""> <h4 className="text-sm font-semibold">{possessor('name')}</h4> <p className="text-sm text-gray-500">{conversation.text}</p> </div> <span className="text-sm"> {timeAgo(new Date(Number(conversation.sentAt) * 1000).getTime())} </span> </Link> ) } export default ChatList
La vista de chat
Esta es una vista de chat uno a uno para que un vendedor y un comprador se comuniquen. El SDK de CometChat nos lo pone más fácil. El siguiente código demuestra cómo funciona bastante bien.
import { CometChat } from '@cometchat-pro/chat' import { sendMessage, getMessages } from '../cometChat' import { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' import Header from '../components/Header' const Chat = () => { const { receiverID } = useParams() const [message, setMessage] = useState('') const [messages, setMessages] = useState([]) const handleSendMsg = (e) => { e.preventDefault() sendMessage(receiverID, message).then((msg) => { setMessages((prevState) => [...prevState, msg]) setMessage('') scrollToEnd() }) } const handleGetMessages = () => { getMessages(receiverID).then((msgs) => { setMessages(msgs) scrollToEnd() }) } const listenForMessage = (listenerID) => { CometChat.addMessageListener( listenerID, new CometChat.MessageListener({ onTextMessageReceived: (message) => { setMessages((prevState) => [...prevState, message]) scrollToEnd() }, }) ) } const scrollToEnd = () => { const elmnt = document.getElementById('messages-container') elmnt.scrollTop = elmnt.scrollHeight } useEffect(() => { handleGetMessages() listenForMessage(receiverID) }, [receiverID]) return ( <div className="chat"> <Header /> <div className="flex justify-center items-center p-10"> <div className="relative mx-auto w-full"> <div className="border-0 rounded-lg relative flex flex-col w-full"> <div className="flex items-start justify-between p-5"> <h3 className="text-md font-semibold">Chat</h3> </div> <div id="messages-container" className="relative p-6 flex-auto h-64 overflow-y-scroll" style={{ height: '20rem' }} > <div className="flex flex-col justify-center items-center"> {messages.map((msg, i) => msg?.receiverId?.toLowerCase() != receiverID.toLowerCase() ? ( <div key={i} className="flex flex-col justify-center items-start w-full mb-4" > <div className="rounded-lg p-2 bg-green-100"> <p>{msg.text}</p> </div> </div> ) : ( <div key={i} className="flex flex-col justify-center items-end w-full mb-4" > <div className="rounded-lg p-2 bg-gray-100"> <p>{msg.text}</p> </div> </div> ) )} </div> </div> <form onSubmit={handleSendMsg} className="flex flex-row justify-center items-center mt-4 py-4" > <input type="text" placeholder="Type Message..." className="px-3 py-8 placeholder-blueGray-300 text-blueGray-600 relative bg-green-100 rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full flex-1 border-0" value={message} onChange={(e) => setMessage(e.target.value)} /> </form> </div> </div> </div> </div> ) } export default Chat
La vista de registro Cree un nuevo archivo llamado SignUp.jsx y pegue los códigos a continuación dentro de él.
import Header from '../components/Header' import Register from '../components/Register' const SignUp = () => { return ( <div className="signup"> <Header /> <Register /> </div> ) } export default SignUp
La vista de inicio de sesión Hagamos lo mismo para la vista de inicio de sesión, cree un nuevo archivo llamado SignIn.jsx y pegue los códigos a continuación dentro de él.
import Header from '../components/Header' import Login from '../components/Login' const SignIn = () => { return ( <div className="signIn"> <Header /> <Login /> </div> ) } export default SignIn
Sorprendente, acabamos de agregar todas las vistas esenciales en nuestra aplicación, ordenemos el resto del código...
Este es el primer archivo que se ejecuta antes que cualquier otra vista y componente de nuestra aplicación. En su archivo App.jsx, pegue los siguientes códigos dentro de él.
import { useEffect, useState } from 'react' import { Routes, Route } from 'react-router-dom' import { useGlobalState, setGlobalState, latestPrice } from './store' import { auth, onAuthStateChanged } from './firebase' import Product from './views/Product' import Home from './views/Home' import SignUp from './views/SignUp' import SignIn from './views/SignIn' import AuthGuard from './AuthGuard' import EditProduct from './views/EditProduct' import AddProduct from './views/AddProduct' import Cart from './views/Cart' import Chat from './views/Chat' import ChatList from './views/ChatList' import { loadWeb3 } from './shared/Freshers' function App() { const [user, setUser] = useState(null) const [isLoaded, setIsLoaded] = useState(false) const [alert] = useGlobalState('alert') useEffect(() => { loadWeb3() onAuthStateChanged(auth, (user) => { if (user) { setUser(user) setGlobalState('isLoggedIn', true) } else { setUser(null) setGlobalState('isLoggedIn', false) } setIsLoaded(true) }) latestPrice() }, []) return ( <div className="App"> {isLoaded ? ( <> {alert.show ? ( <div className={`text-white px-6 py-2 border-0 rounded relative bg-${alert.color}-500`} > <span className="text-xl inline-block mr-5 align-middle"> <i className="fas fa-bell" /> </span> <span className="inline-block align-middle mx-4"> <b className="capitalize">Alert!</b> {alert.msg}! </span> <button onClick={() => setGlobalState('alert', { show: false, msg: '' }) } className="absolute bg-transparent text-2xl font-semibold leading-none right-0 top-0 mt-2 mr-6 outline-none focus:outline-none" > <span>×</span> </button> </div> ) : null} <Routes> <Route path="/" element={<Home />} /> <Route path="product/:id" element={<Product />} /> <Route path="product/edit/:id" element={ <AuthGuard user={user}> <EditProduct /> </AuthGuard> } /> <Route path="product/add" element={ <AuthGuard user={user}> <AddProduct /> </AuthGuard> } /> <Route path="chat/:receiverID" element={ <AuthGuard user={user}> <Chat /> </AuthGuard> } /> <Route path="customers" element={ <AuthGuard user={user}> <ChatList /> </AuthGuard> } /> <Route path="cart" element={<Cart />} /> <Route path="signin" element={<SignIn />} /> <Route path="signup" element={<SignUp />} /> </Routes> </> ) : null} </div> ) } export default App
Este archivo contiene la lógica para evitar que los usuarios no autenticados accedan a rutas seguras en nuestra aplicación. Cree un nuevo archivo en la carpeta src y asígnele el nombre AuthGuard.jsx , luego pegue los siguientes códigos dentro de él.
import { Navigate } from 'react-router-dom' const AuthGuard = ({ user, children, redirectPath = '/signin' }) => { if (!user) { return <Navigate to={redirectPath} replace /> } return children } export default AuthGuard
Pegue los siguientes códigos dentro del archivo index.jsx y guarde...
import React from 'react' import ReactDOM from 'react-dom' import { BrowserRouter } from 'react-router-dom' import '@material-tailwind/react/tailwind.css' import { initCometChat } from './cometChat' import App from './App' initCometChat().then(() => { ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') ) })
Usando el poder de la biblioteca react-hooks-global-state, creemos una tienda para administrar algunas de nuestras variables de estado global. En el directorio src , >> store crea un archivo llamado index.jsx y pega los códigos de abajo dentro de él.
import { createGlobalState } from 'react-hooks-global-state' const { setGlobalState, useGlobalState } = createGlobalState({ isLoggedIn: false, alert: { show: false, msg: '', color: '' }, cart: [], contract: null, connectedAccount: '', ethToUsd: 0, }) const setAlert = (msg, color = 'amber') => { setGlobalState('alert', { show: true, msg, color }) setTimeout(() => { setGlobalState('alert', { show: false, msg: '', color }) }, 5000) } const latestPrice = async () => { await fetch( 'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD' ) .then((data) => data.json()) .then((res) => setGlobalState('ethToUsd', res.USD)) } export { useGlobalState, setGlobalState, setAlert, latestPrice }
Por último, tenemos el archivo fresher.jsx que sirve como interfaz entre el Abi de nuestro contrato inteligente y la interfaz. Todos los códigos necesarios para interactuar con nuestro contrato inteligente se almacenan en este archivo, aquí está el código.
import Web3 from 'web3' import { setAlert, setGlobalState } from '../store' import Store from './abis/Store.json' const { ethereum } = window const getContract = async () => { const web3 = window.web3 const networkId = await web3.eth.net.getId() const networkData = Store.networks[networkId] if (networkData) { const contract = new web3.eth.Contract(Store.abi, networkData.address) return contract } else { window.alert('Store contract not deployed to detected network.') } } const payWithEthers = async (product) => { try { const web3 = window.web3 const seller = product.account const buyer = product.buyer const amount = web3.utils.toWei(product.price.toString(), 'ether') const purpose = `Sales of ${product.name}` const contract = await getContract() await contract.methods .payNow(seller, purpose) .send({ from: buyer, value: amount }) return true } catch (error) { setAlert(error.message, 'red') } } const connectWallet = async () => { try { if (!ethereum) return alert('Please install Metamask') const accounts = await ethereum.request({ method: 'eth_requestAccounts' }) setGlobalState('connectedAccount', accounts[0]) } catch (error) { setAlert(JSON.stringify(error), 'red') } } const loadWeb3 = async () => { try { if (!ethereum) return alert('Please install Metamask') window.web3 = new Web3(ethereum) await ethereum.enable() window.web3 = new Web3(window.web3.currentProvider) const web3 = window.web3 const accounts = await web3.eth.getAccounts() setGlobalState('connectedAccount', accounts[0]) } catch (error) { alert('Please connect your metamask wallet!') } } export { loadWeb3, connectWallet, payWithEthers }
Dentro de esta carpeta compartida, tenemos otra carpeta llamada abis que contenía el código ABI generado para nuestra tienda implementada. Truffle generó estos códigos para nosotros cuando implementamos el contrato inteligente en la PARTE UNO de este artículo.
Asegúrese de haber incluido el archivo .env en el archivo .gitignore , esto es muy importante para que no exponga sus claves privadas en línea.
Si todo eso está solucionado, entonces debe saber que ha completado este proyecto.
¡¡¡Felicidades!!!
La tecnología Blockchain ha venido para quedarse, en este nuevo mundo de contratos inteligentes, DAO, NFT y aplicaciones DeFi, es muy importante armarse con habilidades de desarrollo de blockchain.
No puedo esperar a verlo en el próximo artículo, consulte la demostración en vivo y el repositorio de GitHub para obtener más información.
¡Hasta la próxima, todo lo mejor!
Gospel Darlington inició su viaje como ingeniero de software en 2016. A lo largo de los años, ha desarrollado habilidades completas en pilas de JavaScript como React, ReactNative, VueJs y más.
Actualmente trabaja de forma independiente, crea aplicaciones para clientes, escribe tutoriales técnicos y enseña a otros cómo hacer lo que él hace.
Gospel Darlington está abierto y disponible para escuchar de usted. Puede comunicarse con él en LinkedIn , Facebook , Github o en su sitio web .