Shop integration

This commit is contained in:
2026-01-14 17:47:58 +01:00
parent be7f7b7bf7
commit 21b78f8d17
52 changed files with 5288 additions and 198 deletions

View 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;
};