diff --git a/package.json b/package.json index 4d93cc101b5ff41b7ae9b869ef3e4588718543cf..7f5bdb051d7900c3e40064721e178e02d2b86064 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "firebase": "^9.21.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.11.1" diff --git a/src/App.js b/src/App.js index 1960caf24be8c32b3bed44b21ea0acf057413e42..dd378b6f8ae5c3a134e9e411fe1438c4afc957ac 100755 --- a/src/App.js +++ b/src/App.js @@ -1,31 +1,109 @@ -import React, { useState, useEffect } from 'react'; -import './assets/css/app.css'; -import CheckoutPage from './checkout'; -import Products from './products'; -import Header from './header'; +import React, { useState, useEffect } from "react"; +import "./assets/css/app.css"; +import CheckoutPage from "./checkout"; +import Products from "./products"; +import Header from "./header"; +import RegisterPage from "./RegisterProductPage"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { collection, getDocs, addDoc } from "firebase/firestore"; +import { db } from "./firebase"; const App = (props) => { let [cartItems, setCartItems] = useState([]); - let [showProduct, setShowProduct] = useState(true); + let [products, setProducts] = useState([]); + let [user, setUser] = useState(null); + + const fetchData = async () => { + /* firestore security rule change : + allow read : if request.auth != null; + allow write : if request != null; + */ + const querySnapshot = await getDocs(collection(db, "petshop")); + let products = []; + querySnapshot.forEach((doc) => { + products.push(doc.data()); + }); + setProducts([...products]); + console.log(products); + }; + + useEffect(() => { + fetchData(); + }, []); + + const handleDeleteItem = (index) => { + const updatedCartItems = cartItems.filter((item, i) => i !== index); + setCartItems(updatedCartItems); + }; + + const DeleteAllItem = () => { + setCartItems([]); + }; + + const updateInventory = () => { + cartItems.forEach((item) => { + console.log("item" + item); + products.forEach((product) => { + console.log("product" + product); + if (item.id === product.id) product.availableInventory -= 1; + }); + }); + setCartItems([]); + }; + + const addProduct = async (product) => { + const docRef = await addDoc(collection(db, "petshop"), product); + console.log(`document id : ${docRef.id}`); + setProducts([...products, product]); + }; return ( - <> + <BrowserRouter> <header> - <Header name={props.sitename} - nItems={cartItems.length} - onToggleShowProduct={() => setShowProduct(!showProduct)} + <Header + name={props.sitename} + nItems={cartItems.length} + user={user} + onSignIn={setUser} + onSignOut={() => setUser(null)} /> </header> <main> - {showProduct && - <Products cart={cartItems} onSetCart={setCartItems}/> - } - {!showProduct && /* Checkout 페이지 들어갈 자리 */ - <CheckoutPage /> - } + <Routes> + <Route + path="/" + element={ + <Products + products={products} + cart={cartItems} + onSetCart={setCartItems} + user={user} + /> + } + ></Route> + <Route + path="/checkout" + element={ + <CheckoutPage + cartItems={cartItems} + onDeleteItem={handleDeleteItem} + deleteAll={DeleteAllItem} + updateInventory={updateInventory} + /> + } + ></Route> + <Route + path="/register-product" + element={ + <RegisterPage + addProduct={addProduct} + /> + } + ></Route> + </Routes> </main> - </> + </BrowserRouter> ); -} +}; export default App; diff --git a/src/RegisterProductPage.js b/src/RegisterProductPage.js new file mode 100644 index 0000000000000000000000000000000000000000..f742b6cde212dbcc733c7e98a8f4945d4297eed0 --- /dev/null +++ b/src/RegisterProductPage.js @@ -0,0 +1,110 @@ +import React, { useState} from 'react'; +import { Link, useNavigate } from 'react-router-dom'; + +const ProductRegistrationPage = (props) => { + const [product, setProduct] = useState({ + title: '', + description: '', + image: '', + price: 0, + availableInventory:0, + }); + + let navigate = useNavigate(); + + const handleInputChange = (event) => { + const { name, value } = event.target; + setProduct({ ...product, [name]: value }); + }; + + const handleSubmit = (event) => { + event.preventDefault(); + console.log(product); + if (product.title.length < 1 || product.description.length < 3 || product.image.length < 3) { + alert('상품정보를 바르게 입력하세요'); + return; + } + props.addProduct(product) + navigate("/"); + }; + + return ( + <div> + <h3>제품 등록</h3> + <form > + <div className="form-group"> + <label htmlFor="title">상품 제목:</label> + <input + type="text" + id="title" + name="title" + className="form-control" + value={product.title} + onChange={handleInputChange} + required + /> + </div> + <div className="form-group"> + <label htmlFor="description">상품 설명:</label> + <textarea + id="description" + name="description" + className="form-control" + value={product.description} + onChange={handleInputChange} + required + ></textarea> + </div> + + <div className="form-group"> + <label htmlFor="image">상품 이미지:</label> + <input + type="text" + id="image" + name="image" + className="form-control" + value={product.image} + onChange={handleInputChange} + required + /> + </div> + <div className="form-group"> + <label htmlFor="price">가격:</label> + <input + type="number" + id="price" + name="price" + className="form-control" + min="0" + value={product.price} + onChange={handleInputChange} + required + /> + </div> + <div className="form-group"> + <label htmlFor="availableInventory">재고량:</label> + <input + type="number" + id="availableInventory" + name="availableInventory" + className="form-control" + min="0" + value={product.availableInventory} + onChange={handleInputChange} + required + /> + </div> + <Link to="/"> + <button type="button" className="btn btn-primary">취소 + </button> + </Link> + <Link to="/"> + <button type="submit" className="btn btn-primary" onClick={handleSubmit}>제출 + </button> + </Link> + </form> + </div> + ); +}; + +export default ProductRegistrationPage; diff --git a/src/assets/css/app.css b/src/assets/css/app.css index c881ca9484ce22987f147aa17014780ca3661d8a..204cd32b22d0265201890e400da57543a38c6858 100644 --- a/src/assets/css/app.css +++ b/src/assets/css/app.css @@ -73,4 +73,9 @@ label { .container { max-width: 970px; } +} + +.btn-add-product { + font-size: 50px; + padding: 5px 120px; } \ No newline at end of file diff --git a/src/checkout.js b/src/checkout.js index 9d650d8209a3eb82b54058162b458de0c75193d6..22d81e39d7223881b73c487f171e78ac4982896a 100644 --- a/src/checkout.js +++ b/src/checkout.js @@ -1,21 +1,73 @@ -import React, {useState} from 'react'; +import React, { useEffect, useState } from 'react'; +import { Link,useNavigate } from 'react-router-dom'; -const CheckoutPage = (props) => { +const CheckoutPage = ({ products, cartItems, onSetCartItems, onSetProducts }) => { + const [order, setOrder] = useState({ firstName: '', lastName: '', state: 'CA', address: '', gift: false }); + const states = ['AL', 'AR', 'CA', 'NV', 'NY', 'FL']; - let [order, setOrder] = useState({ firstName: '', lastName: '', state: 'CA', address: '' }); - let states = ['AL', 'AR', 'CA', 'NV', 'NY', 'FL']; + const updateOrder = (event) => { + const { name, value, type, checked } = event.target; + const newValue = type === 'checkbox' ? checked : value; + setOrder({ ...order, [name]: newValue }); + }; - const updateOrder = (event) => { - let control = event.target; - if (control.name == "gift") { - setOrder({ ...order, gift: control.checked }); - return; - } - setOrder({ ...order, [control.name]: control.value }); - } + const navigate = useNavigate(); - return ( - <div> + const [totalAmount, setTotalAmount] = useState(0); + + useEffect(() => { + let total = 0; + cartItems.forEach((item) => { + total += item.price; + }); + setTotalAmount(total); + }, [cartItems]); + + const handleDelete = (index) => { + const updatedItems = [...cartItems]; + updatedItems.splice(index, 1); + onSetCartItems(updatedItems); + }; + + const handleCancel = () => { + onSetCartItems([]); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + cartItems.forEach((item) => { + let product = products.find(p => p.id == item.id); + product.availableInventory -= 1; + + onSetProducts([...products]); + }); + onSetCartItems([]); + navigate('/'); + }; + + + const nItems = cartItems.length; + + return ( + <div> + <table> + <tbody> + {cartItems.map((item, i) => ( + <tr key={i}> + <td>{item.title}</td> + <td>{item.price}</td> + <td>1</td> + <td> + <button onClick={() => handleDelete(i)}>삭제</button> + </td> + </tr> + ))} + </tbody> + </table> + <div>합계:{totalAmount}</div> + + + <form> <div className="col-md-6"> <strong>이름</strong> <input className="form-control" name="firstName" value={order.firstName} onChange={updateOrder} /> @@ -24,29 +76,32 @@ const CheckoutPage = (props) => { <strong>성</strong> <input className="form-control" name="lastName" value={order.lastName} onChange={updateOrder} /> </div> - <div className="form-group"> - <div className="col-md-12"> <strong>주소:</strong></div> + <div className="col-md-12"> + <strong>주소:</strong> + </div> <div className="col-md-12"> <input className="form-control" name="address" value={order.address} onChange={updateOrder} /> </div> </div> - <div className="form-group"> - <div className="col-md-12"> <strong>주:</strong> + <div className="col-md-12"> + <strong>주:</strong> <select className="form-control" name="state" value={order.state} onChange={updateOrder}> - {states.map((st) => <option value={st}> {st}</option>)} + {states.map((st) => ( + <option key={st} value={st}> + {st} + </option> + ))} </select> </div> </div> - <div className="form-group"> <div className="col-md-6 boxes"> - <input type="checkbox" name="gift" id="gift" value={true} onChange={updateOrder} /> + <input type="checkbox" name="gift" id="gift" checked={order.gift} onChange={updateOrder} /> <label htmlFor="gift">선물로 보내기?</label> </div> </div> - <div className="form-group"> <div className="col-md-9 boxes"> <div className="col-md-3"> @@ -59,31 +114,26 @@ const CheckoutPage = (props) => { </div> </div> </div> + </form> + <div className="form-group"> + <div className="col-md-12"> - <div className="form-group"> - <div className="col-md-12"> - <input type="submit" name="submit" className="btn btn-lg btn-primary submit" - onClick={() => { alert("제출완료") }} /> - </div> - </div> + <button type="button" className="btn btn-default btn-lg" onClick={handleSubmit}> + <span className="glyphicon glyphicon-shopping-cart"> + {nItems || ''} + </span> + 제출 + </button> - <div className="col-md-12 verify"> - <pre> - 이름 : {order.firstName} - <br /> - 성 : {order.lastName} - <br /> - 주소 : {order.address} - <br /> - 주 : {order.state} - <br /> - 배송지: {order.method} - <br /> - 선물 : {order.gift ? "선물" : "선물아님"} - </pre> + <Link to="/"> + <button type="button" className="btn btn-default btn-lg" onClick={handleCancel}> + 구매취소 + </button> + </Link> </div> </div> - ) -} + </div> + ); +}; -export default CheckoutPage; \ No newline at end of file +export default CheckoutPage; diff --git a/src/firebase b/src/firebase new file mode 100644 index 0000000000000000000000000000000000000000..978a80dec070808306ef40a58f13eb779ff51dad --- /dev/null +++ b/src/firebase @@ -0,0 +1,32 @@ +// Import the functions you need from the SDKs you need +import { initializeApp } from "firebase/app"; +import { getAnalytics } from "firebase/analytics"; + +// TODO: Add SDKs for Firebase products that you want to use +// https://firebase.google.com/docs/web/setup#available-libraries + +// Your web app's Firebase configuration +// For Firebase JS SDK v7.20.0 and later, measurementId is optional + +import { getFirestore } from 'firebase/firestore'; +import { getAuth ,GoogleAuthProvider } from 'firebase/auth'; + + + + +const firebaseConfig = { + apiKey: "AIzaSyAMGZojwvf5o32lokZ1SkF3JRFYcD2NQcQ", + authDomain: "petshop-f62e0.firebaseapp.com", + projectId: "petshop-f62e0", + storageBucket: "petshop-f62e0.appspot.com", + messagingSenderId: "305060990130", + appId: "1:305060990130:web:98797b3fbb420f080a8dab", + measurementId: "G-CCV33S1HTF" +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); +const analytics = getAnalytics(app); +export const db = getFirestore(app); +export const auth = getAuth(); +export const auth_provider = new GoogleAuthProvider(); diff --git a/src/header.js b/src/header.js index 9d7da6290df0a7c71f89b0f6343073ca66c4f7c8..83d1fd7585498407bcd57b90ff009f9c9ed81be5 100644 --- a/src/header.js +++ b/src/header.js @@ -1,24 +1,98 @@ import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; + +import { + GoogleAuthProvider, signInWithPopup, + getAdditionalUserInfo, signOut +} from 'firebase/auth'; +import { auth, auth_provider } from './firebase'; + + + + + + +const Header = ({ name, nItems, user, onSignIn, onSignOut }) => { + const location = useLocation(); + const hasItemsInCart = nItems > 0; + const isCheckoutPage = location.pathname.startsWith('/checkout'); + const checkoutButtonText = isCheckoutPage && hasItemsInCart ? '더 사기' : '체크아웃'; + + const handleCheckoutClick = () => { + if (isCheckoutPage && hasItemsInCart) { + + } + }; + const handleSignIn = () => { + signInWithPopup(auth, auth_provider) + .then(res => { + const credential = GoogleAuthProvider.credentialFromResult(res); + const token = credential.accessToken; + const user = res.user; + const userInfo = getAdditionalUserInfo(res); + console.log(userInfo.profile); + onSignIn(userInfo.profile); + }).catch(error => { + console.log(`[${error.code}] ${error.message}`); + }) + } + const handleSignOut = () => { + signOut(auth).then(() => { + console.log(`Bye ${user.name}`); + onSignOut(); + }).catch(error => { + console.log(`[${error.code}] ${error.message}`); + }) + } + -const Header = ({name, nItems, onToggleShowProduct}) => { return ( - <div className="navbar navbar-default"> - <div className="navbar-header"> - <h1> - {name} - </h1> - </div> - <div className="nav navbar-nav navbar-right cart"> - <button type="button" className="btn btn-default btn-lg" - onClick={onToggleShowProduct} > - <span className="glyphicon glyphicon-shopping-cart"> - {nItems || ''} - </span> - 체크아웃 - </button> - </div> + <div className="navbar navbar-default"> + <div className="navbar-header"> + <h1>{name}</h1> + </div> + <div className="nav navbar-nav navbar-right cart"> + + <Link to={isCheckoutPage ? '/' : '/checkout'}> + <button + type="button" + className="btn btn-default btn-lg" + onClick={handleCheckoutClick} + > + <span className="glyphicon glyphicon-shopping-cart"> + {nItems || ''} + </span> + {checkoutButtonText} + </button> + </Link> + {(user) ? + <button type="button" className="btn btn-default btn-lg" + style={{ margin: '5px' }} + onClick={handleSignOut} > + <span style={{ margin: '5px' }}> + <img className="rounded-circle" src={user.picture} + style={{ width: '20px', objectFit: 'cover' }} /> + </span> + {user.name} 로그아웃 + </button> + : + <button type="button" className="btn btn-default btn-lg" + onClick={handleSignIn} > + 로그인 + </button> + } + + + </div> - ) -} + </div> + ); +}; + +export default Header; + + + + + -export default Header; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 0d944304d2b0bce6d117c625ffd199400fde889c..e8036430c42f8d859c8691dc91a2fc7038bbd90e 100755 --- a/src/index.js +++ b/src/index.js @@ -7,4 +7,4 @@ var root = ReactDOM.createRoot(container); root.render(<App sitename="React PetShop"/>); -module.hot.accept() +module.hot.accept() \ No newline at end of file diff --git a/src/products.js b/src/products.js index 394acc6d1bfd2ae39719c37c1982e2330afc6786..0a08586a5f4efcb95d1aacd50cbd746128e6647f 100644 --- a/src/products.js +++ b/src/products.js @@ -1,104 +1,103 @@ -import React, {useState, useEffect} from 'react'; - -const Products = ({cart, onSetCart}) => { - - let [products, setProducts] = useState([]); - - useEffect(() => { - /* wget https://raw.githubusercontent.com/gilbutITbook/007024/master/chapter-05/products.json */ - fetch('./products.json') - .then((r) => r.json()) - .then(data => { - setProducts([...data.products]); - console.log(products); - }) - }, []) +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +const Products = ({ products, cart, onSetCart,user }) => { + const formatPrice = (price) => { - if (!parseInt(price)) { return ""; } + if (!parseInt(price)) { + return ''; + } if (price > 99999) { var priceString = (price / 100).toFixed(2); - var priceArray = priceString.split("").reverse(); + var priceArray = priceString.split('').reverse(); var index = 3; while (priceArray.length > index + 3) { - priceArray.splice(index + 3, 0, ","); + priceArray.splice(index + 3, 0, ','); index += 4; } - return "$" + priceArray.reverse().join(""); + return '$' + priceArray.reverse().join(''); } else { - return "$" + (price / 100).toFixed(2); + return '$' + (price / 100).toFixed(2); } - } + }; const addToShoppingCart = (product) => { + console.log(cart); onSetCart([...cart, product]); - } + }; const canAddToCart = (product) => { if (product.availableInventory) { - return product.availableInventory > cart.filter((p) => p.id == product.id).length; + return product.availableInventory > cart.filter((p) => p.id === product.id).length; } return false; - } + }; const checkRating = (n, product) => { return product.rating - n >= 0; - } + }; const nameOrder = (p, q) => { - let [pLow, qLow] = [p.title.toLowerCase(), q.title.toLowerCase()] + let [pLow, qLow] = [p.title.toLowerCase(), q.title.toLowerCase()]; if (pLow > qLow) { return 1; - } - else if(pLow == qLow) { - return 0; - } - else { + } else if (pLow < qLow) { return -1; + } else { + return 0; } - } + }; - return ( - <> - {products && products.sort(nameOrder).map(product => { - let nProductInCart = cart.filter((p) => p.id == product.id).length; - console.log(product.title, nProductInCart); - return ( - <div className="row product"> - <div className="col-md-5 col-md-offset-0"> - <figure> - <img className="product" src={product.image} /> - </figure> - </div> - <div className="col-md-6 col-md-offset-0 description"> - <h1> {product.title} </h1> - <p> {product.description} </p> - <p className="price"> {formatPrice(product.price)} </p> - {canAddToCart(product) && - <button className="btn btn-primary btn-lg" - onClick={() => addToShoppingCart(product)}> 장바구니 담기 - </button> - } + return ( + <> + <div className="row product"> + <div className="col-md-12 text-center"> + {user!= null&&<Link to="/register-product" className="btn btn-primary btn-lg btn-add-product"> + 상품추가 + </Link>} + + </div> + </div> + {products && + products.sort(nameOrder).map((product) => { + let nProductInCart = cart.filter((p) => p.id === product.id).length; + console.log(product.title, nProductInCart); + return ( + <div className="row product" key={product.id}> + <div className="col-md-5 col-md-offset-0"> + <figure> + <img className="product" src={product.image} alt={product.title} /> + </figure> + </div> + <div className="col-md-6 col-md-offset-0 description"> + <h1>{product.title}</h1> + <p>{product.description}</p> + <p className="price">{formatPrice(product.price)}</p> + {canAddToCart(product) && ( + <button className="btn btn-primary btn-lg" onClick={() => addToShoppingCart(product)}> + 장바구니 담기 + </button> + )} - <span className="inventory-message"> - {product.availableInventory == nProductInCart && `품절!(Sold out)`} - {nProductInCart == 0 && `지금 구매하세요.`} - {product.availableInventory > nProductInCart && nProductInCart > 0 && - `${product.availableInventory - nProductInCart} 남았습니다.` } - </span> - <div className="rating"> - {[1, 2, 3, 4, 5].map((i) => { - return <span className={checkRating(i, product) ? "rating-active" : ""}>☆</span> - }) - } + <span className="inventory-message"> + {product.availableInventory === nProductInCart && `품절!(Sold out)`} + {/*nProductInCart === 0 && `지금 구매하세요.`*/} + {product.availableInventory > nProductInCart && + nProductInCart >= 0 && + `${product.availableInventory - nProductInCart} 남았습니다.`} + </span> + <div className="rating"> + {[1, 2, 3, 4, 5].map((i) => ( + <span className={checkRating(i, product) ? "rating-active" : ""} key={i}> + ☆ + </span> + ))} + </div> </div> </div> - </div> - ) - }) - } + ); + })} </> - ) -} - -export default Products; \ No newline at end of file + ); +}; +export default Products;