11 changed files with 2918 additions and 564 deletions
@ -0,0 +1,55 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express'); |
||||
const stripe = require('stripe')(process.env.REACT_APP_STRIPE_SKEY); |
||||
const app = express(); |
||||
|
||||
app.use(express.json()); |
||||
|
||||
app.post('/api/create-payment-intent', async (req, res) => { |
||||
try { |
||||
const { amount, currency = 'usd', items } = req.body; |
||||
|
||||
// Create a PaymentIntent with the order amount and currency
|
||||
const paymentIntent = await stripe.paymentIntents.create({ |
||||
amount: amount, |
||||
currency: currency, |
||||
metadata: { |
||||
integration_check: 'accept_a_payment', |
||||
items: JSON.stringify(items) |
||||
} |
||||
}); |
||||
|
||||
res.send({ |
||||
clientSecret: paymentIntent.client_secret |
||||
}); |
||||
} catch (error) { |
||||
console.error('Error creating payment intent:', error); |
||||
res.status(400).send({ error: error.message }); |
||||
} |
||||
}); |
||||
|
||||
// Additional endpoints for order management
|
||||
app.post('/api/create-order', async (req, res) => { |
||||
try { |
||||
// Process order and save to database
|
||||
const order = { |
||||
items: req.body.items, |
||||
total: req.body.total, |
||||
status: 'pending', |
||||
createdAt: new Date() |
||||
}; |
||||
|
||||
// Save to your database
|
||||
// const savedOrder = await saveOrderToDatabase(order);
|
||||
|
||||
res.json({ order }); |
||||
} catch (error) { |
||||
res.status(400).json({ error: 'Failed to create order' }); |
||||
} |
||||
}); |
||||
|
||||
app.listen(process.env.SERVER_PORT || 5000, () => { |
||||
console.log('test', process.env.SERVER_PORT); |
||||
console.log(`Server running on port ${process.env.SERVER_PORT || 5000}`); |
||||
}); |
||||
@ -1,38 +1,263 @@
|
||||
.App { |
||||
max-width: 1200px; |
||||
margin: 0 auto; |
||||
padding: 20px; |
||||
font-family: Arial, sans-serif; |
||||
} |
||||
|
||||
.header { |
||||
background-color: #333; |
||||
color: white; |
||||
padding: 1rem; |
||||
position: sticky; |
||||
top: 0; |
||||
z-index: 100; |
||||
} |
||||
|
||||
.header-content { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
|
||||
.cart-count { |
||||
background-color: #ff4d4d; |
||||
border-radius: 50%; |
||||
padding: 0.2rem 0.5rem; |
||||
font-size: 0.8rem; |
||||
margin-left: 0.5rem; |
||||
} |
||||
|
||||
.main-content { |
||||
display: grid; |
||||
grid-template-columns: 2fr 1fr; |
||||
gap: 2rem; |
||||
margin-top: 2rem; |
||||
} |
||||
|
||||
.product-list h2, .cart h2 { |
||||
margin-bottom: 1rem; |
||||
color: #333; |
||||
} |
||||
|
||||
.products-grid { |
||||
display: grid; |
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); |
||||
gap: 1.5rem; |
||||
} |
||||
|
||||
.product-item { |
||||
border: 1px solid #ddd; |
||||
border-radius: 8px; |
||||
padding: 1rem; |
||||
text-align: center; |
||||
background: #f9f9f9; |
||||
} |
||||
|
||||
.App-logo { |
||||
height: 40vmin; |
||||
pointer-events: none; |
||||
.product-item img { |
||||
max-width: 100%; |
||||
height: 200px; |
||||
object-fit: cover; |
||||
border-radius: 4px; |
||||
} |
||||
|
||||
@media (prefers-reduced-motion: no-preference) { |
||||
.App-logo { |
||||
animation: App-logo-spin infinite 20s linear; |
||||
} |
||||
.product-item h3 { |
||||
margin: 0.5rem 0; |
||||
font-size: 1.2rem; |
||||
} |
||||
|
||||
.price { |
||||
font-weight: bold; |
||||
color: #007bff; |
||||
font-size: 1.1rem; |
||||
} |
||||
|
||||
.description { |
||||
color: #666; |
||||
font-size: 0.9rem; |
||||
margin: 0.5rem 0; |
||||
} |
||||
|
||||
.product-item button { |
||||
background-color: #007bff; |
||||
color: white; |
||||
border: none; |
||||
padding: 0.5rem 1rem; |
||||
border-radius: 4px; |
||||
cursor: pointer; |
||||
margin-top: 0.5rem; |
||||
} |
||||
|
||||
.product-item button:hover { |
||||
background-color: #0056b3; |
||||
} |
||||
|
||||
.cart { |
||||
border: 1px solid #ddd; |
||||
border-radius: 8px; |
||||
padding: 1rem; |
||||
background: #f9f9f9; |
||||
} |
||||
|
||||
.App-header { |
||||
background-color: #282c34; |
||||
min-height: 100vh; |
||||
.cart-items { |
||||
list-style: none; |
||||
padding: 0; |
||||
} |
||||
|
||||
.cart-item { |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
justify-content: center; |
||||
font-size: calc(10px + 2vmin); |
||||
padding: 0.5rem 0; |
||||
border-bottom: 1px solid #eee; |
||||
} |
||||
|
||||
.quantity-controls { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
|
||||
.quantity-controls button { |
||||
width: 25px; |
||||
height: 25px; |
||||
margin: 0 0.2rem; |
||||
border: 1px solid #ccc; |
||||
background: white; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.cart-total { |
||||
margin: 1rem 0; |
||||
padding-top: 1rem; |
||||
border-top: 2px solid #333; |
||||
text-align: right; |
||||
} |
||||
|
||||
.checkout-btn { |
||||
width: 100%; |
||||
padding: 0.75rem; |
||||
background-color: #28a745; |
||||
color: white; |
||||
border: none; |
||||
border-radius: 4px; |
||||
font-size: 1rem; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.checkout-btn:hover { |
||||
background-color: #218838; |
||||
} |
||||
|
||||
.App-link { |
||||
color: #61dafb; |
||||
.loading { |
||||
text-align: center; |
||||
padding: 2rem; |
||||
font-size: 1.2rem; |
||||
} |
||||
|
||||
@keyframes App-logo-spin { |
||||
from { |
||||
transform: rotate(0deg); |
||||
@media (max-width: 768px) { |
||||
.main-content { |
||||
grid-template-columns: 1fr; |
||||
} |
||||
to { |
||||
transform: rotate(360deg); |
||||
|
||||
.products-grid { |
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); |
||||
} |
||||
} |
||||
|
||||
.modal-overlay { |
||||
position: fixed; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
background: rgba(0, 0, 0, 0.5); |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
z-index: 1000; |
||||
} |
||||
|
||||
.modal-content { |
||||
background: white; |
||||
padding: 2rem; |
||||
border-radius: 8px; |
||||
width: 90%; |
||||
max-width: 500px; |
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
||||
} |
||||
|
||||
.close-modal { |
||||
margin-top: 1rem; |
||||
padding: 0.5rem 1rem; |
||||
background: #6c757d; |
||||
color: white; |
||||
border: none; |
||||
border-radius: 4px; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.close-modal:hover { |
||||
background: #5a6268; |
||||
} |
||||
|
||||
.form-row { |
||||
margin-bottom: 1rem; |
||||
} |
||||
|
||||
.form-row label { |
||||
display: block; |
||||
margin-bottom: 0.5rem; |
||||
font-weight: bold; |
||||
} |
||||
|
||||
.submit-button { |
||||
width: 100%; |
||||
padding: 1rem; |
||||
background: #007bff; |
||||
color: white; |
||||
border: none; |
||||
border-radius: 4px; |
||||
font-size: 1rem; |
||||
cursor: pointer; |
||||
transition: background 0.3s; |
||||
} |
||||
|
||||
.submit-button:hover:not(:disabled) { |
||||
background: #0056b3; |
||||
} |
||||
|
||||
.submit-button:disabled { |
||||
background: #6c757d; |
||||
cursor: not-allowed; |
||||
} |
||||
|
||||
.error-message { |
||||
color: #dc3545; |
||||
margin-top: 0.5rem; |
||||
padding: 0.5rem; |
||||
background: #f8d7da; |
||||
border-radius: 4px; |
||||
} |
||||
|
||||
.order-complete { |
||||
position: fixed; |
||||
top: 50%; |
||||
left: 50%; |
||||
transform: translate(-50%, -50%); |
||||
background: white; |
||||
padding: 2rem; |
||||
border-radius: 8px; |
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
||||
text-align: center; |
||||
z-index: 1001; |
||||
} |
||||
|
||||
.order-complete button { |
||||
margin-top: 1rem; |
||||
padding: 0.5rem 1rem; |
||||
background: #28a745; |
||||
color: white; |
||||
border: none; |
||||
border-radius: 4px; |
||||
cursor: pointer; |
||||
} |
||||
@ -1,25 +1,134 @@
|
||||
import logo from './logo.svg'; |
||||
import React, { useState, useEffect } from 'react'; |
||||
import { loadStripe } from '@stripe/stripe-js'; |
||||
import { Elements } from '@stripe/react-stripe-js'; |
||||
import ProductList from './components/ProductList'; |
||||
import Cart from './components/Cart'; |
||||
import Header from './components/Header'; |
||||
import CheckoutForm from './components/CheckoutForm'; |
||||
import './App.css'; |
||||
|
||||
function App() { |
||||
// Initialize Stripe (use your publishable key)
|
||||
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PKEY); |
||||
|
||||
const App = () => { |
||||
console.log('stripe', process.env.REACT_APP_STRIPE_PKEY) |
||||
const [products, setProducts] = useState([]); |
||||
const [cart, setCart] = useState([]); |
||||
const [loading, setLoading] = useState(true); |
||||
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false); |
||||
const [orderComplete, setOrderComplete] = useState(false); |
||||
|
||||
// Mock product data
|
||||
const mockProducts = [ |
||||
{ id: 1, name: 'Product 1', price: 29.99, image: '/placeholder1.jpg', description: 'Description 1' }, |
||||
{ id: 2, name: 'Product 2', price: 39.99, image: '/placeholder2.jpg', description: 'Description 2' }, |
||||
{ id: 3, name: 'Product 3', price: 19.99, image: '/placeholder3.jpg', description: 'Description 3' }, |
||||
{ id: 4, name: 'Product 4', price: 49.99, image: '/placeholder4.jpg', description: 'Description 4' }, |
||||
]; |
||||
|
||||
useEffect(() => { |
||||
// Simulate API fetch
|
||||
setTimeout(() => { |
||||
setProducts(mockProducts); |
||||
setLoading(false); |
||||
}, 1000); |
||||
}, []); |
||||
|
||||
const addToCart = (product) => { |
||||
setCart(prevCart => { |
||||
const existingItem = prevCart.find(item => item.id === product.id); |
||||
if (existingItem) { |
||||
return prevCart.map(item => |
||||
item.id === product.id |
||||
? { ...item, quantity: item.quantity + 1 } |
||||
: item |
||||
); |
||||
} |
||||
return [...prevCart, { ...product, quantity: 1 }]; |
||||
}); |
||||
}; |
||||
|
||||
const removeFromCart = (productId) => { |
||||
setCart(prevCart => prevCart.filter(item => item.id !== productId)); |
||||
}; |
||||
|
||||
const updateQuantity = (productId, quantity) => { |
||||
if (quantity < 1) return; |
||||
setCart(prevCart => |
||||
prevCart.map(item => |
||||
item.id === productId ? { ...item, quantity } : item |
||||
) |
||||
); |
||||
}; |
||||
|
||||
const cartTotal = cart.reduce((total, item) => total + (item.price * item.quantity), 0); |
||||
|
||||
const handleCheckout = () => { |
||||
setIsCheckoutOpen(true); |
||||
}; |
||||
|
||||
const handlePaymentSuccess = () => { |
||||
setIsCheckoutOpen(false); |
||||
setOrderComplete(true); |
||||
setCart([]); // Clear cart after successful payment
|
||||
}; |
||||
|
||||
const handlePaymentError = () => { |
||||
setIsCheckoutOpen(false); |
||||
}; |
||||
|
||||
return ( |
||||
<div className="App"> |
||||
<header className="App-header"> |
||||
<img src={logo} className="App-logo" alt="logo" /> |
||||
<p> |
||||
Edit <code>src/App.js</code> and save to reload. |
||||
</p> |
||||
<a |
||||
className="App-link" |
||||
href="https://reactjs.org" |
||||
target="_blank" |
||||
rel="noopener noreferrer" |
||||
> |
||||
Learn React |
||||
</a> |
||||
</header> |
||||
<Header cartCount={cart.length} /> |
||||
<main className="main-content"> |
||||
{loading ? ( |
||||
<div className="loading">Loading products...</div> |
||||
) : ( |
||||
<> |
||||
<ProductList products={products} onAddToCart={addToCart} /> |
||||
<Cart
|
||||
items={cart}
|
||||
onRemove={removeFromCart} |
||||
onUpdateQuantity={updateQuantity} |
||||
total={cartTotal} |
||||
onCheckout={handleCheckout} |
||||
/> |
||||
</> |
||||
)} |
||||
</main> |
||||
|
||||
{/* Stripe Checkout Modal */} |
||||
{isCheckoutOpen && ( |
||||
<div className="modal-overlay"> |
||||
<div className="modal-content"> |
||||
<h2>Checkout</h2> |
||||
<Elements stripe={stripePromise}> |
||||
<CheckoutForm
|
||||
amount={cartTotal * 100} // Stripe expects amount in cents
|
||||
onPaymentSuccess={handlePaymentSuccess} |
||||
onPaymentError={handlePaymentError} |
||||
cartItems={cart} |
||||
/> |
||||
</Elements> |
||||
<button
|
||||
className="close-modal"
|
||||
onClick={() => setIsCheckoutOpen(false)} |
||||
> |
||||
Close |
||||
</button> |
||||
</div> |
||||
</div> |
||||
)} |
||||
|
||||
{orderComplete && ( |
||||
<div className="order-complete"> |
||||
<h2>Order Successful!</h2> |
||||
<p>Thank you for your purchase. Your order is being processed.</p> |
||||
<button onClick={() => setOrderComplete(false)}>Continue Shopping</button> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
}; |
||||
|
||||
export default App; |
||||
export default App; |
||||
@ -0,0 +1,35 @@
|
||||
import React from 'react'; |
||||
|
||||
const Cart = ({ items, onRemove, onUpdateQuantity, onCheckout, total }) => { |
||||
return ( |
||||
<div className="cart"> |
||||
<h2>Shopping Cart</h2> |
||||
{items.length === 0 ? ( |
||||
<p>Your cart is empty</p> |
||||
) : ( |
||||
<> |
||||
<ul className="cart-items"> |
||||
{items.map(item => ( |
||||
<li key={item.id} className="cart-item"> |
||||
<span>{item.name}</span> |
||||
<span>${(item.price * item.quantity).toFixed(2)}</span> |
||||
<div className="quantity-controls"> |
||||
<button onClick={() => onUpdateQuantity(item.id, item.quantity - 1)}>-</button> |
||||
<span>{item.quantity}</span> |
||||
<button onClick={() => onUpdateQuantity(item.id, item.quantity + 1)}>+</button> |
||||
</div> |
||||
<button onClick={() => onRemove(item.id)}>Remove</button> |
||||
</li> |
||||
))} |
||||
</ul> |
||||
<div className="cart-total"> |
||||
<strong>Total: ${total.toFixed(2)}</strong> |
||||
</div> |
||||
<button onClick={() => onCheckout()} className="checkout-btn">Proceed to Checkout</button> |
||||
</> |
||||
)} |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default Cart; |
||||
@ -0,0 +1,82 @@
|
||||
import React, { useState } from 'react'; |
||||
import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; |
||||
|
||||
const CheckoutForm = ({ amount, onPaymentSuccess, onPaymentError, cartItems }) => { |
||||
const stripe = useStripe(); |
||||
const elements = useElements(); |
||||
const [loading, setLoading] = useState(false); |
||||
const [error, setError] = useState(null); |
||||
|
||||
const handleSubmit = async (event) => { |
||||
event.preventDefault(); |
||||
|
||||
if (!stripe || !elements) { |
||||
return; |
||||
} |
||||
|
||||
setLoading(true); |
||||
setError(null); |
||||
|
||||
try { |
||||
// Create payment intent on your backend
|
||||
const response = await fetch('/api/create-payment-intent', { |
||||
method: 'POST', |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
}, |
||||
body: JSON.stringify({ |
||||
amount: amount, |
||||
currency: 'usd', |
||||
items: cartItems |
||||
}), |
||||
}); |
||||
|
||||
const { clientSecret } = await response.json(); |
||||
|
||||
// Confirm card payment
|
||||
const result = await stripe.confirmCardPayment(clientSecret, { |
||||
payment_method: { |
||||
card: elements.getElement(CardElement), |
||||
billing_details: { |
||||
name: 'Customer Name', |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
if (result.error) { |
||||
setError(result.error.message); |
||||
onPaymentError(); |
||||
} else { |
||||
// Payment successful
|
||||
onPaymentSuccess(); |
||||
} |
||||
} catch (error) { |
||||
setError('Payment processing failed. Please try again.'); |
||||
onPaymentError(); |
||||
} finally { |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
return ( |
||||
<form onSubmit={handleSubmit}> |
||||
<div className="form-row"> |
||||
<label htmlFor="card-element"> |
||||
Credit or debit card |
||||
</label> |
||||
<CardElement id="card-element" /> |
||||
{error && <div className="error-message">{error}</div>} |
||||
</div> |
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!stripe || loading} |
||||
className="submit-button" |
||||
> |
||||
{loading ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}`} |
||||
</button> |
||||
</form> |
||||
); |
||||
}; |
||||
|
||||
export default CheckoutForm; |
||||
@ -0,0 +1,17 @@
|
||||
import React from 'react'; |
||||
|
||||
const Header = ({ cartCount }) => { |
||||
return ( |
||||
<header className="header"> |
||||
<div className="header-content"> |
||||
<h1>My Store</h1> |
||||
<div className="cart-icon"> |
||||
<span>Cart</span> |
||||
{cartCount > 0 && <span className="cart-count">{cartCount}</span>} |
||||
</div> |
||||
</div> |
||||
</header> |
||||
); |
||||
}; |
||||
|
||||
export default Header; |
||||
@ -0,0 +1,17 @@
|
||||
import React from 'react'; |
||||
|
||||
const ProductItem = ({ product, onAddToCart }) => { |
||||
return ( |
||||
<div className="product-item"> |
||||
<img src={product.image} alt={product.name} /> |
||||
<h3>{product.name}</h3> |
||||
<p className="price">${product.price.toFixed(2)}</p> |
||||
<p className="description">{product.description}</p> |
||||
<button onClick={() => onAddToCart(product)}> |
||||
Add to Cart |
||||
</button> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default ProductItem; |
||||
@ -0,0 +1,21 @@
|
||||
import React from 'react'; |
||||
import ProductItem from './ProductItem'; |
||||
|
||||
const ProductList = ({ products, onAddToCart }) => { |
||||
return ( |
||||
<div className="product-list"> |
||||
<h2>Products</h2> |
||||
<div className="products-grid"> |
||||
{products.map(product => ( |
||||
<ProductItem
|
||||
key={product.id}
|
||||
product={product}
|
||||
onAddToCart={onAddToCart}
|
||||
/> |
||||
))} |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default ProductList; |
||||
Loading…
Reference in new issue