Shop integration
This commit is contained in:
299
Pottery-website/src/context/StoreContext.tsx
Normal file
299
Pottery-website/src/context/StoreContext.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import { CollectionItem, JournalEntry } from '../../types';
|
||||
import { COLLECTIONS, JOURNAL_ENTRIES } from '../../constants';
|
||||
|
||||
export interface CartItem extends CollectionItem {
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: number;
|
||||
customer_email: string;
|
||||
customer_name: string;
|
||||
shipping_address: any;
|
||||
items: any[];
|
||||
total_amount: number;
|
||||
payment_status: string;
|
||||
shipping_status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface StoreContextType {
|
||||
products: CollectionItem[];
|
||||
articles: JournalEntry[];
|
||||
cart: CartItem[];
|
||||
orders: Order[];
|
||||
isCartOpen: boolean;
|
||||
setCartOpen: (open: boolean) => void;
|
||||
addToCart: (product: CollectionItem) => void;
|
||||
removeFromCart: (productId: number) => void;
|
||||
updateQuantity: (productId: number, quantity: number) => void;
|
||||
clearCart: () => void;
|
||||
placeOrder: (orderData: Partial<Order>) => Promise<Order>;
|
||||
fetchOrders: () => Promise<void>;
|
||||
updateOrderStatus: (id: number, status: string) => Promise<void>;
|
||||
addProduct: (product: CollectionItem) => void;
|
||||
updateProduct: (product: CollectionItem) => void;
|
||||
deleteProduct: (id: number) => void;
|
||||
addArticle: (article: JournalEntry) => void;
|
||||
updateArticle: (article: JournalEntry) => void;
|
||||
deleteArticle: (id: number) => void;
|
||||
}
|
||||
|
||||
const StoreContext = createContext<StoreContextType | undefined>(undefined);
|
||||
|
||||
const API_URL = 'http://localhost:5000/api';
|
||||
|
||||
export const StoreProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [products, setProducts] = useState<CollectionItem[]>([]);
|
||||
const [articles, setArticles] = useState<JournalEntry[]>([]);
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [cart, setCart] = useState<CartItem[]>([]);
|
||||
const [isCartOpen, setCartOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Persist cart (minimal data only)
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
const minimalCart = cart.map(item => ({ id: item.id, quantity: item.quantity }));
|
||||
localStorage.setItem('cart', JSON.stringify(minimalCart));
|
||||
} catch (err) {
|
||||
console.error('Failed to save cart to localStorage:', err);
|
||||
}
|
||||
}, [cart]);
|
||||
|
||||
// Hydrate cart when products are loaded
|
||||
React.useEffect(() => {
|
||||
if (!isLoading && products.length > 0) {
|
||||
const saved = localStorage.getItem('cart');
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
// Handle both old format (full objects) and new format (minimal)
|
||||
const hydrated = parsed.map((m: any) => {
|
||||
const productId = typeof m === 'object' && m !== null ? (m.id || m.productId) : m;
|
||||
const product = products.find(p => p.id === productId);
|
||||
if (product) {
|
||||
return { ...product, quantity: m.quantity || 1 };
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean) as CartItem[];
|
||||
setCart(hydrated);
|
||||
} catch (err) {
|
||||
console.error('Failed to hydrate cart:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isLoading, products]);
|
||||
|
||||
// Initial Fetch
|
||||
React.useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [prodRes, artRes] = await Promise.all([
|
||||
fetch(`${API_URL}/products`),
|
||||
fetch(`${API_URL}/articles`)
|
||||
]);
|
||||
const prods = await prodRes.json();
|
||||
const arts = await artRes.json();
|
||||
setProducts(prods);
|
||||
setArticles(arts);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch data from backend, falling back to static data', err);
|
||||
setProducts(COLLECTIONS);
|
||||
setArticles(JOURNAL_ENTRIES);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// Cart Actions
|
||||
const addToCart = (product: CollectionItem) => {
|
||||
setCart(prev => {
|
||||
const existing = prev.find(item => item.id === product.id);
|
||||
if (existing) {
|
||||
return prev.map(item =>
|
||||
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
|
||||
);
|
||||
}
|
||||
return [...prev, { ...product, quantity: 1 }];
|
||||
});
|
||||
setCartOpen(true);
|
||||
};
|
||||
|
||||
const removeFromCart = (productId: number) => {
|
||||
setCart(prev => prev.filter(item => item.id !== productId));
|
||||
};
|
||||
|
||||
const updateQuantity = (productId: number, quantity: number) => {
|
||||
if (quantity < 1) {
|
||||
removeFromCart(productId);
|
||||
return;
|
||||
}
|
||||
setCart(prev => prev.map(item =>
|
||||
item.id === productId ? { ...item, quantity } : item
|
||||
));
|
||||
};
|
||||
|
||||
const clearCart = () => setCart([]);
|
||||
|
||||
// Order Actions
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/orders`);
|
||||
const data = await res.json();
|
||||
setOrders(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch orders:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const placeOrder = async (orderData: Partial<Order>) => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/orders`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(orderData)
|
||||
});
|
||||
const newOrder = await res.json();
|
||||
setOrders(prev => [newOrder, ...prev]);
|
||||
return newOrder;
|
||||
} catch (err) {
|
||||
console.error('Failed to place order:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const updateOrderStatus = async (id: number, status: string) => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/orders/${id}/status`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ shipping_status: status })
|
||||
});
|
||||
const updatedOrder = await res.json();
|
||||
setOrders(prev => prev.map(o => o.id === id ? updatedOrder : o));
|
||||
} catch (err) {
|
||||
console.error('Failed to update order status:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const addProduct = async (product: CollectionItem) => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/products`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(product)
|
||||
});
|
||||
const newProduct = await res.json();
|
||||
setProducts(prev => [...prev, newProduct]);
|
||||
} catch (err) {
|
||||
console.error('Failed to add product:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const updateProduct = async (updatedProduct: CollectionItem) => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/products/${updatedProduct.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updatedProduct)
|
||||
});
|
||||
const data = await res.json();
|
||||
setProducts(prev => prev.map(p => p.id === data.id ? data : p));
|
||||
} catch (err) {
|
||||
console.error('Failed to update product:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteProduct = async (id: number) => {
|
||||
try {
|
||||
await fetch(`${API_URL}/products/${id}`, { method: 'DELETE' });
|
||||
setProducts(prev => prev.filter(p => p.id !== id));
|
||||
} catch (err) {
|
||||
console.error('Failed to delete product:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const addArticle = async (article: JournalEntry) => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/articles`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(article)
|
||||
});
|
||||
const newArticle = await res.json();
|
||||
setArticles(prev => [...prev, newArticle]);
|
||||
} catch (err) {
|
||||
console.error('Failed to add article:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const updateArticle = async (updatedArticle: JournalEntry) => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/articles/${updatedArticle.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updatedArticle)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
// If the updated article is featured, we must ensure only this one is featured in our local state
|
||||
if (data.isFeatured) {
|
||||
setArticles(prev => prev.map(a => ({
|
||||
...a,
|
||||
isFeatured: a.id === data.id
|
||||
})));
|
||||
} else {
|
||||
setArticles(prev => prev.map(a => a.id === data.id ? data : a));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update article:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteArticle = async (id: number) => {
|
||||
try {
|
||||
await fetch(`${API_URL}/articles/${id}`, { method: 'DELETE' });
|
||||
setArticles(prev => prev.filter(a => a.id !== id));
|
||||
} catch (err) {
|
||||
console.error('Failed to delete article:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StoreContext.Provider value={{
|
||||
products,
|
||||
articles,
|
||||
cart,
|
||||
orders,
|
||||
isCartOpen,
|
||||
setCartOpen,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
updateQuantity,
|
||||
clearCart,
|
||||
placeOrder,
|
||||
fetchOrders,
|
||||
updateOrderStatus,
|
||||
addProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
addArticle,
|
||||
updateArticle,
|
||||
deleteArticle
|
||||
}}>
|
||||
{isLoading ? <div className="fixed inset-0 bg-white z-50 flex items-center justify-center font-display text-xl animate-pulse">Loading Store...</div> : children}
|
||||
</StoreContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useStore = () => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useStore must be used within a StoreProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user