Към съдържанието
wannacode

Витрината на магазина

Накратко: Проект на уеб приложение: витрина на магазин с каталог от API, страница на продукта по адрес, обща кошница, поръчка с проверки и „Моите поръчки“.

Проблем

Витрината

Магазинът има API: продукти, категории, поръчки. Липсва витрината, през която купувачът пазарува. Пътят му е:

  1. Търси в каталога или избира категория.
  2. Отваря продукт и го добавя в кошницата.
  3. В кошницата сменя бройките.
  4. Поръчва: име, имейл, адрес. Ако сгреши, вижда къде.
  5. Вижда поръчката си в „Моите поръчки“.

Всяка част вече си я правил в отделен урок. Тук ги събираш в едно приложение и го строиш на пет стъпки: всяка задача започва от решението на предишната. Последната проверка минава целия път.

Идеята

Скелетът

Това е приложението на нулата стъпка: менюто и страниците-заглушки. Мини през менюто.

React

Файловете са подредени като в истински проект:

  • api.js пази всички заявки. Страниците викат getProducts, placeOrder и не знаят адресите. Грешката от сървъра идва като Error със status и errors (грешките по поле).
  • format.js форматира пари, дата и статус.
  • pages/ са страниците, по една за всеки адрес. components/ са частите, които страниците ползват: ProductCard, Filter, Field.
  • store/cart е кошницата, обща за цялото приложение.

Рутерът е в main.jsx: MemoryRouter над CartProvider и App. Адресите са в App.jsx с Routes и Route, както в урока за рутирането.

Пясъчникът няма адресна лента, затова историята е в паметта. В истинския проект е BrowserRouter. Останалото е същото.

Filter и Field са готови от уроците: полета, свързани със състояние, и поле с етикет и съобщение за грешка. Ти пишеш страниците и кошницата.

Упражнение

Стъпка 1: каталогът

Каталогът тегли продуктите от getProducts({ q, category }) и ги показва с ProductCard в списък с клас grid. Филтърът е отгоре. При всяка смяна на търсенето или категорията тръгва нова заявка, а старата се отказва. Докато чака, страницата казва „Зарежда…“. Ако няма нищо, казва „Няма такива продукти.“

Внимавай за две неща от урока за данни от API:

  • Списъкът следва филтъра. Ефектът има [q, category] в зависимостите.
  • Старият отговор не печели. Ако „Млечни“ отговаря бавно, а ти вече си избрал „Напитки“, бавният отговор не трябва да затрие новия. AbortController отказва старата заявка в почистването на ефекта.

Напиши CatalogPage: продуктите от API-то по филтъра, „Зарежда…“, „Няма такива продукти.“ и отказ на старата заявка. Проверката търси „хляб“, после избира „Млечни“ и веднага „Напитки“ (Млечни отговаря за 900 ms), после търси в категория.

React

Упражнение

Стъпка 2: страницата на продукта

Името в ProductCard става връзка към /product/:id. ProductPage взема id от адреса, тегли продукта с getProduct(id) и показва име, категория, цена и наличност: „В наличност: 18 бр.“ или „Изчерпан“. Под него е „Още от категорията“: до три други продукта от същата категория, с връзки.

Внимавай:

  • Връзката е на рутера: <Link to={…}>, не <a href>. Обикновената връзка зарежда цялата страница наново. В пясъчника тя не отива никъде.
  • Същата страница, друг параметър. От „Сирене“ към „Кашкавал“ рутерът не прави нов ProductPage, а сменя само id. Ефектът е с [id].
  • id от адреса е низ: "7", не 7. getProduct(id) го слага в адреса и това е наред. Ако търсиш в масив с ===, сравняваш низ с число.

Подробно е в урока за рутирането.

Направи името в ProductCard връзка и напиши ProductPage. Проверката отваря „Сирене“, после „Кашкавал“ от „Още от категорията“, връща се в каталога и отваря изчерпаната „Баница със сирене“.

React

Упражнение

Стъпка 3: кошницата

ProductPage и менюто вече викат useCart(): бутон „Добави в кошницата“, „В кошницата: 2“ и „Кошница (2)“. Сега useCart е заглушка. Напиши истинската кошница и CartPage.

store/cart.jsx има CartProvider с useState и CartContext, както в урока за общото състояние. useCart() връща useContext(CartContext): items, count, total, add, setQty, remove, clear. count и total се изчисляват от items, не се пазят отделно. Всяка смяна прави нов масив: [...items, …], items.map(…), items.filter(…).

CartPage показва ред за всеки продукт: име, падащо меню „Брой: Сирене“ (от 1 до наличността, най-много 10), сума на реда и бутон „Махни“ с име „Махни Сирене“. Отдолу е „Общо: …“ и връзка „Към поръчката“. Празната кошница казва „Кошницата е празна.“

Напиши store/cart и CartPage. Проверката добавя две сирена и ябълки, отваря кошницата, сменя броя на сиренето на 3 и маха ябълките, като гледа менюто и сумата.

React

Упражнение

Стъпка 4: поръчката

CheckoutPage има формуляр с три Field: „Име“, „Имейл“, „Адрес“. При „Поръчай“:

  1. Проверка в браузъра. Празно име: „Въведи име.“ Имейл без @: „Въведи имейл.“ Празен адрес: „Въведи адрес.“ Ако има грешка, заявка не тръгва.
  2. Заявката. placeOrder({ name, email, address, items }), където items е [{ productId, qty }] от кошницата.
  3. Отговорът. При 201 страницата казва например „Поръчка № 61 за 15,80 €.“ и кошницата се изчиства. При 400 err.errors е обект по поле, например { email: "Имейлът не е валиден" }, и всяко съобщение отива до своето поле.

Field вече показва error до полето, с aria-invalid и aria-describedby. Формулярът е с noValidate, за да говорят твоите съобщения, а не балончетата на браузъра. Проверката в браузъра е за удобство: хваща празното, преди да тръгне заявка. Последната дума е на сървъра, затова „maria@shop“ минава в браузъра, а сървърът я връща. Как се прави формуляр е в урока за формулярите.

Напиши CheckoutPage. Проверката добавя две сирена, натиска „Поръчай“ с празен формуляр, после с имейл „maria@shop“ (сървърът го връща) и накрая с верен имейл.

React

Упражнение

Стъпка 5: моите поръчки и целият път

OrdersPage тегли getOrders() при всяко отваряне и показва поръчките на клиента ME, най-новата отгоре: „Поръчка № 61“, дата, статус, сума и редовете („Сирене × 3“). Докато чака, „Зарежда…“, а без поръчки, „Още нямаш поръчки.“

Учебното API няма вход и връща поръчките на всички клиенти. Затова страницата филтрира по customerId === ME. Истинският сървър връща само твоите.

Сега приложението е цяло. Проверката минава пътя от началото на урока: гледа старите поръчки, филтрира „Млечни“, отваря сиренето, добавя 2, прави ги 3 в кошницата, поръчва с грешен имейл, вижда грешката до полето, поправя я, поръчва и намира новата поръчка най-горе в „Моите поръчки“.

Напиши OrdersPage. Проверката минава целия път на купувача и накрая търси новата поръчка най-горе в „Моите поръчки“.

React

Идеята

Какво е учебно тук

Приложението е истинско, но около него има опростявания. В истинския магазин:

  • Кой си, знае сървърът. Влизаш с парола и сървърът връща само твоите поръчки по сесията. Филтър в браузъра не пази нищо: всеки може да смени ME.
  • Цената е на сървъра. Поръчката праща само productId и qty. Сумата идва в отговора, смятана от цените в базата.
  • Кошницата оцелява. Тук тя е в паметта и изчезва при презареждане. Истинската е в localStorage или на сървъра.
  • Адресите са истински. BrowserRouter вместо историята в паметта, и хостинг, който връща index.html за всеки адрес.
По-точно, за любопитните

Двете библиотеки. Пътят е един и същ: адреси, данни от API, обща кошница, формуляр с грешки по поле. React го прави с hooks: useEffect със зависимости, Context за кошницата, контролирани полета с value и onChange. Другата библиотека решава същите задачи по свой начин.

Тестовете. Сценариите на стъпките кликат и пишат като човек. Същото пишеш с Testing Library: render, screen.getByRole, userEvent. Как, е в урока за тестовете.

Бързината. Каталогът тегли при всеки натиснат клавиш. С отказа на старата заявка това е вярно, но излишно. В истинския проект търсенето чака 300 ms тишина, преди да тръгне (debounce).

Идеята

Какво следва

Пренеси витрината в истински проект, както в урока „От пясъчника към проект“:

Терминал
npm create vite@latest shop -- --template react
cd shop
npm install react-router

Копирай папките pages/, components/, store/ и файловете api.js, format.js, style.css в src/. В main.jsx смени MemoryRouter с BrowserRouter.

API-то вече не е мнимо. Пусни истински сървър с адресите /api/products, /api/orders или насочи /api към него с server.proxy във vite.config.js. После npm run build и публикувай dist/ с правило, което връща index.html за всеки адрес.

Още задачи

  • Подробности за поръчка. /orders/:id с GET /api/orders/:id и връзка от „Моите поръчки“. Непознат номер показва „Няма такава поръчка.“
  • Кошница, която оцелява. Пази items в localStorage и ги чети при старт.
  • Тестове. Тест за CartPage: смяна на броя сменя сумата. Тест за CheckoutPage: празен формуляр показва трите грешки и не праща заявка.

Тест

Провери се

1. Избираш „Млечни“, а списъкът остава със всички продукти. Ефектът, който тегли продуктите, е с [] накрая. Какво липсва?
2. От „Сирене“ натискаш „Кашкавал“ в „Още от категорията“. Адресът става /product/8, а страницата показва пак сирене. Защо?
3. Проверката в браузъра пусна „maria@shop“, а сървърът върна 400 с { errors: { email: "Имейлът не е валиден" } }. Какво прави страницата?
4. useCart пази кошницата в useState вътре в себе си, без Context. ProductPage добавя, а менюто казва „Кошница (0)“. Защо?
5. Защо поръчката праща само productId и qty, без цените?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·