Last active
          October 28, 2025 20:49 
        
      - 
      
- 
        Save sunmeat/fecd4f307daf2178a939d6236e4805cc to your computer and use it in GitHub Desktop. 
    IntersectionObserver 
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | import { useState, useEffect, useRef } from 'react'; | |
| import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; | |
| import './App.css'; | |
| const API_URL = 'http://sunmeat.atwebpages.com/react/api.php'; | |
| // екземпляр QueryClient | |
| const queryClient = new QueryClient(); | |
| const truncateText = (text, maxLength) => | |
| text.length > maxLength ? text.slice(0, maxLength) + '...' : text; | |
| // компонента для отримання та відображення продуктів | |
| function ProductList({ addToCart }) { | |
| const [page, setPage] = useState(1); | |
| const [allProducts, setAllProducts] = useState([]); | |
| const loadMoreRef = useRef(null); | |
| // API для отримання продуктів | |
| const fetchProducts = async ({ queryKey }) => { | |
| const [, page] = queryKey; | |
| const response = await fetch(`https://fakestoreapi.com/products?limit=12&offset=${(page - 1) * 12}`); | |
| if (!response.ok) throw new Error('помилка завантаження продуктів'); | |
| return response.json(); | |
| }; | |
| // useQuery для управління даними | |
| const { data, error, isLoading } = useQuery({ | |
| queryKey: ['products', page], | |
| queryFn: fetchProducts, | |
| staleTime: 5 * 60 * 1000, // кешування на 5 хвилин | |
| }); | |
| // оновлення списку продуктів при отриманні нових даних | |
| useEffect(() => { | |
| if (data) { | |
| setAllProducts((prev) => [...prev, ...data]); | |
| } | |
| }, [data]); | |
| // налаштування IntersectionObserver для нескінченної прокрутки | |
| useEffect(() => { | |
| // IntersectionObserver відстежує видимість елемента | |
| // коли елемент стає видимим на 10% (threshold: 0.1) і дані не завантажуються (!isLoading), | |
| // збільшує номер сторінки (setPage), ініціюючи підвантаження нових даних для нескінченної прокрутки | |
| const observer = new IntersectionObserver( | |
| (entries) => { | |
| if (entries[0].isIntersecting && !isLoading) { | |
| setPage((prev) => prev + 1); | |
| } | |
| }, | |
| { threshold: 0.1 } | |
| ); | |
| if (loadMoreRef.current) { | |
| observer.observe(loadMoreRef.current); | |
| } | |
| return () => { | |
| if (loadMoreRef.current) { | |
| observer.unobserve(loadMoreRef.current); | |
| } | |
| }; | |
| }, [isLoading]); | |
| if (error) return <div className="error">помилка: {error.message}</div>; | |
| return ( | |
| <div className="product-list"> | |
| <h2>наші товари:</h2> | |
| <div className="products"> | |
| {allProducts.map((product) => ( | |
| <div key={product.id} className="product-card"> | |
| <img src={product.image} alt={product.title} className="product-image" /> | |
| <h3>{truncateText(product.title, 30)}</h3> | |
| <p className="price">${product.price}</p> | |
| <button onClick={() => addToCart(product)} className="add-to-cart"> | |
| додати до кошика | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| {isLoading && <div className="loading">завантаження...</div>} | |
| <div ref={loadMoreRef} style={{ height: '20px' }} /> | |
| </div> | |
| ); | |
| } | |
| // компонента кошика | |
| function Cart({ cart, removeFromCart }) { | |
| const total = cart.reduce((sum, item) => sum + Number(item.price), 0).toFixed(2); | |
| return ( | |
| <div className="cart"> | |
| <h2>кошик</h2> | |
| {cart.length === 0 ? ( | |
| <p>кошик порожній</p> | |
| ) : ( | |
| <div> | |
| {cart.map((item) => ( | |
| <div key={item.id} className="cart-item"> | |
| <span>{truncateText(item.title, 30)}</span> | |
| <span>${item.price}</span> | |
| <button onClick={() => removeFromCart(item.id)} className="remove-from-cart"> | |
| видалити | |
| </button> | |
| </div> | |
| ))} | |
| <p className="total">разом: ${total}</p> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| function App() { | |
| const [cart, setCart] = useState([]); | |
| const [error, setError] = useState(null); | |
| // завантаження кошика з API при ініціалізації | |
| useEffect(() => { | |
| const loadCart = async () => { | |
| try { | |
| const response = await fetch(API_URL); | |
| if (!response.ok) throw new Error('помилка завантаження кошика'); | |
| const data = await response.json(); | |
| if (data.error) throw new Error(data.error); | |
| setCart(data); | |
| } catch (err) { | |
| setError(err.message); | |
| } | |
| }; | |
| loadCart(); | |
| }, []); | |
| // додавання товару до кошика через API | |
| const addToCart = async (product) => { | |
| try { | |
| const newItem = { | |
| id: product.id, | |
| title: product.title, | |
| price: Number(product.price), | |
| image: product.image, | |
| }; | |
| const response = await fetch(API_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(newItem), | |
| }); | |
| if (!response.ok) throw new Error('помилка додавання до кошика'); | |
| const result = await response.json(); | |
| if (result.error) throw new Error(result.error); | |
| setCart((prevCart) => [ | |
| ...prevCart, | |
| { | |
| ...newItem, | |
| product_id: newItem.id, | |
| id: result.id, | |
| }, | |
| ]); | |
| } catch (err) { | |
| setError(err.message); | |
| } | |
| }; | |
| // видалення товару з кошика через API | |
| const removeFromCart = async (cartItemId) => { | |
| try { | |
| const response = await fetch(API_URL, { | |
| method: 'DELETE', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ id: cartItemId }), | |
| }); | |
| if (!response.ok) throw new Error('помилка видалення з кошика'); | |
| const result = await response.json(); | |
| if (result.error) throw new Error(result.error); | |
| setCart((prevCart) => prevCart.filter((item) => item.id !== cartItemId)); | |
| } catch (err) { | |
| setError(err.message); | |
| } | |
| }; | |
| return ( | |
| <QueryClientProvider client={queryClient}> | |
| <div className="app"> | |
| <h1>інтернет-магазин ReactExpress</h1> | |
| {error && <div className="error">помилка: {error}</div>} | |
| <ProductList addToCart={addToCart} /> | |
| <Cart cart={cart} removeFromCart={removeFromCart} /> | |
| </div> | |
| </QueryClientProvider> | |
| ); | |
| } | |
| export default App; | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment