const API_URL =
import.meta.env.VITE_API_URL
|| 'http://localhost:3001';
async function api(path, options = {}) {
const token = localStorage.getItem('token');
const res = await fetch(`${API_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token
? { Authorization: `Bearer ${token}` }
: {}),
...options.headers,
},
});
if (!res.ok) {
const error = await res.json()
.catch(() => ({ error: 'Request failed' }));
throw new Error(
error.error || `HTTP ${res.status}`
);
}
if (res.status === 204) return null;
return res.json();
}
// Usage:
const images = await api('/api/images');
await api('/api/images', {
method: 'POST',
body: JSON.stringify(imageData)
});
await api(`/api/images/${id}`,
{ method: 'DELETE' });
function Gallery() {
const [images, setImages] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
api('/api/images')
.then(setImages)
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
if (error) return <ErrorMessage
message={error}
onRetry={() =>
window.location.reload()} />;
if (images.length === 0) return <EmptyState
message="No images yet.
Upload your first!" />;
return (
<div className="grid grid-cols-4 gap-4 p-4">
{images.map(img => (
<ImageCard key={img._id} image={img} />
))}
</div>
);
}
Register → login → upload image metadata → see it in gallery → delete it. End-to-end CRUD working.
When the API returns 401, redirect to login. Clear the expired token. Show a toast: "Session expired. Please log in again."
git switch -c feature/PIXELCRAFT-048-connect-frontend
git add src/ server/
git commit -m "Connect React frontend to Express API (PIXELCRAFT-048)"
git push origin feature/PIXELCRAFT-048-connect-frontend
# PR → Review → Merge → Close ticket ✅
CORS (Cross-Origin Resource Sharing).
The same-origin policy is a security measure. CORS headers explicitly allow cross-origin requests.