add loader
This commit is contained in:
parent
91917289e2
commit
abce2bb6ef
10 changed files with 538 additions and 49 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
|
import time
|
||||||
|
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from flask_jwt_extended import jwt_required, get_jwt_identity, create_access_token, create_refresh_token
|
from flask_jwt_extended import jwt_required, get_jwt_identity, create_access_token, create_refresh_token
|
||||||
from app import db
|
from app import db
|
||||||
|
|
@ -76,7 +78,12 @@ def get_current_user():
|
||||||
@api_bp.route("/products", methods=["GET"])
|
@api_bp.route("/products", methods=["GET"])
|
||||||
def get_products():
|
def get_products():
|
||||||
"""Get all products"""
|
"""Get all products"""
|
||||||
|
|
||||||
|
|
||||||
|
# time.sleep(5) # This adds a 5 second delay
|
||||||
|
|
||||||
products = Product.query.filter_by(is_active=True).all()
|
products = Product.query.filter_by(is_active=True).all()
|
||||||
|
|
||||||
return jsonify([product.to_dict() for product in products]), 200
|
return jsonify([product.to_dict() for product in products]), 200
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,145 @@ function Products() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### API Integration with Loaders and Toasts
|
||||||
|
- **ALWAYS** create custom hooks for API operations that integrate loader and toast logic
|
||||||
|
- **NEVER** handle errors in components when the hook already shows toasts
|
||||||
|
- Custom hooks should use `useLoader` and `useToast` for consistent UX
|
||||||
|
- Hooks should return data, refetch function, and optionally error state for debugging
|
||||||
|
- Components should NOT display error UI when toasts are already shown
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// ✅ CORRECT - Custom hook with loader and toast integration
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import useApi from "./useApi"
|
||||||
|
import { useLoader } from "../context/loaders/useLoader"
|
||||||
|
import { useToast } from "../context/toasts/useToast"
|
||||||
|
|
||||||
|
function useProducts() {
|
||||||
|
const [products, setProducts] = useState([])
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
|
||||||
|
const { getProducts } = useApi()
|
||||||
|
const { withLoader } = useLoader()
|
||||||
|
const { addNotification } = useToast()
|
||||||
|
|
||||||
|
const fetchProducts = async () => {
|
||||||
|
try {
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
// Use withLoader to show loading state
|
||||||
|
const data = await withLoader(
|
||||||
|
() => getProducts(),
|
||||||
|
'Loading products...'
|
||||||
|
)
|
||||||
|
|
||||||
|
setProducts(data)
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Products Loaded',
|
||||||
|
message: `Successfully loaded ${data.length} products.`,
|
||||||
|
duration: 3000,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load products'
|
||||||
|
setError(errorMessage)
|
||||||
|
|
||||||
|
// Show error toast
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error Loading Products',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProducts()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
products,
|
||||||
|
error, // For debugging, not for UI display
|
||||||
|
loading: false, // Loading is handled by global loader
|
||||||
|
refetch: fetchProducts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useProducts
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// ✅ CORRECT - Component using the custom hook
|
||||||
|
function Products() {
|
||||||
|
const { products, refetch } = useProducts()
|
||||||
|
|
||||||
|
// Note: No error UI here - toast is already shown by the hook
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Products</h1>
|
||||||
|
<button onClick={() => refetch()}>Refresh</button>
|
||||||
|
{products.map(product => (
|
||||||
|
<ProductCard key={product.id} product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// ❌ WRONG - Handling errors in component when toast already shown
|
||||||
|
function Products() {
|
||||||
|
const { products, error, refetch } = useProducts()
|
||||||
|
|
||||||
|
// BAD PRACTICE: This duplicates the error message already shown in toast
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-red-400 mb-4">Failed to load products</p>
|
||||||
|
<button onClick={() => refetch()}>Retry</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{products.map(product => (
|
||||||
|
<ProductCard key={product.id} product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is bad practice:**
|
||||||
|
1. **Duplicate UI**: Users see the same error message twice (toast + component error UI)
|
||||||
|
2. **Inconsistent UX**: Some operations show toasts, others show component error messages
|
||||||
|
3. **Confusing Experience**: Users don't know which error message to act on
|
||||||
|
4. **Redundant Code**: You're writing error handling twice (in hook and component)
|
||||||
|
5. **Maintenance Issue**: If you change error handling in hook, component UI doesn't update
|
||||||
|
|
||||||
|
**The error state in hooks is for:**
|
||||||
|
- Debugging purposes
|
||||||
|
- Conditional rendering based on error type (rare cases)
|
||||||
|
- Logging or analytics
|
||||||
|
- NOT for displaying error UI to users
|
||||||
|
|
||||||
|
**Benefits of this pattern:**
|
||||||
|
1. **Consistent UX**: All errors shown via toasts in the same place (top-right)
|
||||||
|
2. **Separation of Concerns**: Hook handles data fetching, component handles rendering
|
||||||
|
3. **Global Loading**: Loader covers entire app, preventing double-submissions
|
||||||
|
4. **Auto-Cleanup**: `withLoader` ensures loader is hidden even on errors
|
||||||
|
5. **Clean Components**: Components focus on displaying data, not handling errors
|
||||||
|
```
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
### Formatting
|
### Formatting
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { ModalProvider } from './context/modals/useModal'
|
||||||
import { ModalRoot } from './context/modals/ModalRoot'
|
import { ModalRoot } from './context/modals/ModalRoot'
|
||||||
import { ToastProvider } from './context/toasts/useToast'
|
import { ToastProvider } from './context/toasts/useToast'
|
||||||
import { ToastRoot } from './context/toasts/ToastRoot'
|
import { ToastRoot } from './context/toasts/ToastRoot'
|
||||||
|
import { LoaderProvider } from './context/loaders/useLoader'
|
||||||
|
import { LoaderRoot } from './context/loaders/LoaderRoot'
|
||||||
import Cart from './pages/Cart'
|
import Cart from './pages/Cart'
|
||||||
import { Navbar } from './components/Navbar'
|
import { Navbar } from './components/Navbar'
|
||||||
import { Home } from './pages/Home'
|
import { Home } from './pages/Home'
|
||||||
|
|
@ -13,6 +15,7 @@ import { Orders } from './pages/Orders'
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
|
<LoaderProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<ModalProvider>
|
<ModalProvider>
|
||||||
<div className="min-h-screen bg-gray-900 text-gray-100">
|
<div className="min-h-screen bg-gray-900 text-gray-100">
|
||||||
|
|
@ -27,11 +30,14 @@ const App = () => {
|
||||||
<Route path="/orders" element={<Orders />} />
|
<Route path="/orders" element={<Orders />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
{/* Order matters for Z-Index: Loader (70) > Toast (60) > Modal (50) */}
|
||||||
|
<LoaderRoot />
|
||||||
<ToastRoot />
|
<ToastRoot />
|
||||||
<ModalRoot />
|
<ModalRoot />
|
||||||
</div>
|
</div>
|
||||||
</ModalProvider>
|
</ModalProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
|
</LoaderProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
192
frontend/src/context/loaders/LoaderExample.tsx
Normal file
192
frontend/src/context/loaders/LoaderExample.tsx
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
import { useLoader } from './useLoader';
|
||||||
|
import { useToast } from '../toasts/useToast';
|
||||||
|
|
||||||
|
export const LoaderExample = () => {
|
||||||
|
const { showLoader, hideLoader, withLoader } = useLoader();
|
||||||
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
|
// Pattern A: Manual Control
|
||||||
|
const handleManualLoad = async () => {
|
||||||
|
showLoader("Processing manual task...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate an async operation
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Manual Task Complete',
|
||||||
|
message: 'The manual loading task finished successfully.',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Manual Task Failed',
|
||||||
|
message: 'Something went wrong during manual loading.',
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pattern B: withLoader Helper (Cleanest)
|
||||||
|
const handleWithLoader = async () => {
|
||||||
|
try {
|
||||||
|
await withLoader(
|
||||||
|
async () => {
|
||||||
|
// Simulate an async operation
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
return 'Success!';
|
||||||
|
},
|
||||||
|
"Processing with withLoader..."
|
||||||
|
);
|
||||||
|
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'withLoader Task Complete',
|
||||||
|
message: 'The task wrapped in withLoader finished successfully.',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'withLoader Task Failed',
|
||||||
|
message: 'Something went wrong during withLoader.',
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pattern C: Long-running task
|
||||||
|
const handleLongLoad = async () => {
|
||||||
|
showLoader("Processing long-running task...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate a longer async operation
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||||
|
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Long Task Complete',
|
||||||
|
message: 'The long-running task finished successfully.',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Long Task Failed',
|
||||||
|
message: 'Something went wrong during long loading.',
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pattern D: Error simulation
|
||||||
|
const handleError = async () => {
|
||||||
|
showLoader("Processing task that will fail...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate an error
|
||||||
|
await new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Simulated error')), 1500)
|
||||||
|
);
|
||||||
|
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Task Complete',
|
||||||
|
message: 'This should not appear.',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Task Failed',
|
||||||
|
message: 'Simulated error occurred as expected.',
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pattern E: Without message
|
||||||
|
const handleNoMessage = async () => {
|
||||||
|
showLoader();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'No Message Task Complete',
|
||||||
|
message: 'Loader shown without custom message.',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Task Failed',
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-4">Loader System Examples</h3>
|
||||||
|
<p className="text-gray-400 mb-6">
|
||||||
|
Click the buttons below to see different loader patterns in action. The loader uses React Context for state management.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleManualLoad}
|
||||||
|
className="px-4 py-2 text-white bg-blue-600 rounded hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Manual Control (2s)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleWithLoader}
|
||||||
|
className="px-4 py-2 text-white bg-green-600 rounded hover:bg-green-700 transition-colors"
|
||||||
|
>
|
||||||
|
withLoader Helper (1.5s)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLongLoad}
|
||||||
|
className="px-4 py-2 text-white bg-purple-600 rounded hover:bg-purple-700 transition-colors"
|
||||||
|
>
|
||||||
|
Long Task (4s)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleError}
|
||||||
|
className="px-4 py-2 text-white bg-red-600 rounded hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
Error Simulation
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleNoMessage}
|
||||||
|
className="px-4 py-2 text-white bg-gray-600 rounded hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
No Message (2s)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 p-4 bg-gray-700 rounded-lg border border-gray-600">
|
||||||
|
<h4 className="text-white font-semibold mb-2">Usage Tips:</h4>
|
||||||
|
<ul className="text-gray-300 text-sm space-y-1 list-disc list-inside">
|
||||||
|
<li><strong>Manual Control:</strong> Use showLoader/hideLoader when you need fine-grained control</li>
|
||||||
|
<li><strong>withLoader:</strong> Use the helper for automatic cleanup (recommended)</li>
|
||||||
|
<li><strong>Message:</strong> Optional - provides context about what's happening</li>
|
||||||
|
<li><strong>Z-Index:</strong> Loader (70) sits above Toasts (60) and Modals (50)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
33
frontend/src/context/loaders/LoaderRoot.tsx
Normal file
33
frontend/src/context/loaders/LoaderRoot.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { useLoader } from './useLoader';
|
||||||
|
|
||||||
|
export const LoaderRoot = () => {
|
||||||
|
const { isLoading, message } = useLoader();
|
||||||
|
|
||||||
|
if (!isLoading) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[70] flex flex-col items-center justify-center bg-black/60 backdrop-blur-sm transition-opacity duration-200"
|
||||||
|
role="alert"
|
||||||
|
aria-busy="true"
|
||||||
|
aria-label={message || "Loading"}
|
||||||
|
>
|
||||||
|
{/* Custom CSS Spinner (No external libs) */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-16 h-16 border-4 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||||
|
{/* Optional inner circle for style */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-8 h-8 border-4 border-white/50 border-b-transparent rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<p className="mt-4 text-white font-medium text-lg animate-pulse">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
};
|
||||||
2
frontend/src/context/loaders/index.ts
Normal file
2
frontend/src/context/loaders/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { LoaderProvider, useLoader } from './useLoader';
|
||||||
|
export { LoaderRoot } from './LoaderRoot';
|
||||||
56
frontend/src/context/loaders/useLoader.tsx
Normal file
56
frontend/src/context/loaders/useLoader.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { createContext, useContext, useState, useCallback, ReactNode, FC } from 'react';
|
||||||
|
|
||||||
|
interface LoaderState {
|
||||||
|
isLoading: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoaderContextType extends LoaderState {
|
||||||
|
showLoader: (message?: string) => void;
|
||||||
|
hideLoader: () => void;
|
||||||
|
// Optional: Helper to wrap async functions automatically
|
||||||
|
withLoader: <T>(fn: () => Promise<T>, message?: string) => Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoaderContext = createContext<LoaderContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
interface LoaderProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoaderProvider: FC<LoaderProviderProps> = ({ children }) => {
|
||||||
|
const [state, setState] = useState<LoaderState>({ isLoading: false });
|
||||||
|
|
||||||
|
const showLoader = useCallback((message?: string) => {
|
||||||
|
setState({ isLoading: true, message });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hideLoader = useCallback(() => {
|
||||||
|
setState({ isLoading: false, message: undefined });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Helper to avoid try/finally blocks everywhere
|
||||||
|
const withLoader = useCallback(async <T,>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
message?: string
|
||||||
|
): Promise<T> => {
|
||||||
|
showLoader(message);
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}, [showLoader, hideLoader]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoaderContext.Provider value={{ ...state, showLoader, hideLoader, withLoader }}>
|
||||||
|
{children}
|
||||||
|
</LoaderContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLoader = () => {
|
||||||
|
const context = useContext(LoaderContext);
|
||||||
|
if (!context) throw new Error('useLoader must be used within a LoaderProvider');
|
||||||
|
return context;
|
||||||
|
};
|
||||||
63
frontend/src/hooks/useProducts.ts
Normal file
63
frontend/src/hooks/useProducts.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useApi } from './useApi';
|
||||||
|
import { useLoader } from '../context/loaders/useLoader';
|
||||||
|
import { useToast } from '../context/toasts/useToast';
|
||||||
|
import { ProductData } from '../types';
|
||||||
|
|
||||||
|
export function useProducts() {
|
||||||
|
const [products, setProducts] = useState<ProductData[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { getProducts } = useApi();
|
||||||
|
const { withLoader } = useLoader();
|
||||||
|
const { addNotification } = useToast();
|
||||||
|
|
||||||
|
const fetchProducts = async () => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Use withLoader to show loading state and handle errors
|
||||||
|
const data = await withLoader(
|
||||||
|
() => getProducts(),
|
||||||
|
'Loading products...'
|
||||||
|
);
|
||||||
|
|
||||||
|
setProducts(data);
|
||||||
|
|
||||||
|
// // Show success toast
|
||||||
|
// addNotification({
|
||||||
|
// type: 'success',
|
||||||
|
// title: 'Products Loaded',
|
||||||
|
// message: `Successfully loaded ${data.length} products.`,
|
||||||
|
// duration: 3000,
|
||||||
|
// });
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load products';
|
||||||
|
setError(errorMessage);
|
||||||
|
|
||||||
|
// Show error toast
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error Loading Products',
|
||||||
|
message: errorMessage,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optionally auto-fetch on mount
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProducts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
products,
|
||||||
|
error,
|
||||||
|
loading: false, // Loading is handled by the global loader
|
||||||
|
refetch: fetchProducts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ModalExample } from '../context/modals/ModalExample'
|
import { ModalExample } from '../context/modals/ModalExample'
|
||||||
import { ToastExample } from '../context/toasts/ToastExample'
|
import { ToastExample } from '../context/toasts/ToastExample'
|
||||||
|
import { LoaderExample } from '../context/loaders/LoaderExample'
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -50,6 +51,14 @@ export function Home() {
|
||||||
</p>
|
</p>
|
||||||
<ToastExample />
|
<ToastExample />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
|
<h2 className="text-2xl font-semibold text-white mb-4">Loader System Demo</h2>
|
||||||
|
<p className="text-gray-400 mb-6">
|
||||||
|
Test our global loading system with this interactive example. The loader uses React Context for state management.
|
||||||
|
</p>
|
||||||
|
<LoaderExample />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,22 @@
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useApp } from '../context/AppContext'
|
import { useApp } from '../context/AppContext'
|
||||||
import { useApi } from '../hooks/useApi'
|
import { useProducts } from '../hooks/useProducts'
|
||||||
import { ProductData, CartItem } from '../types'
|
import { CartItem } from '../types'
|
||||||
|
|
||||||
export function Products() {
|
export function Products() {
|
||||||
const [products, setProducts] = useState<ProductData[]>([])
|
const { products, refetch } = useProducts()
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const { addToCart } = useApp()
|
const { addToCart } = useApp()
|
||||||
const { getProducts } = useApi()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchProducts()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const fetchProducts = async () => {
|
|
||||||
try {
|
|
||||||
const data = await getProducts()
|
|
||||||
setProducts(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching products:', error)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="text-gray-400">Loading products...</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-white mb-8">Products</h1>
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-white">Products</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => refetch()}
|
||||||
|
className="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
{products.map((product) => (
|
{products.map((product) => (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue