Dataczwartek, 13 sierpnia 2026 Czas18:23:57
← Aplikacje webowe

Lekcja 22. Projekt 3: Sklep internetowy (e-sklep) — katalog, wyszukiwanie i koszyk

Trudny / egzaminacyjny

Po co się tego uczymy?

Podstawa programowa INF.04.7.3 wprost wymienia "funkcjonalności e-sklepu" jako jeden z typów aplikacji, które powinieneś umieć zaprogramować. Ten projekt buduje WŁAŚNIE to — prawdziwy, kompletny sklep internetowy z katalogiem produktów podzielonym na kategorie, wyszukiwarką, i koszykiem zakupowym z podsumowaniem zamówienia.

Teoria

Wymagania projektu:

  • Katalog produktów podzielony na kategorie, z możliwością filtrowania po kategorii;
  • Wyszukiwarka — pole tekstowe filtrujące produkty po nazwie NA BIEŻĄCO, w miarę pisania;
  • Koszyk — dodawanie produktów, zmiana ilości, usuwanie pozycji, widoczny na każdej stronie licznik produktów w koszyku;
  • Podsumowanie zamówienia i "checkout" — ekran z listą zamówionych produktów, sumą do zapłaty, i formularzem danych do wysyłki (imię, adres) z walidacją.

Wyszukiwanie NA BIEŻĄCO — debounceTime. Filtrowanie produktów przy KAŻDYM naciśnięciu klawisza (bez czekania na kliknięcie przycisku "Szukaj") to standardowa funkcjonalność e-sklepu. Naiwna implementacja wysyłałaby żądanie do backendu przy KAŻDEJ literze — marnując zasoby. RxJS (biblioteka używana wewnętrznie przez Angular, poznana pobieżnie przy Observable w lekcji 12) udostępnia operator debounceTime(300), który CZEKA 300 milisekund od OSTATNIEGO naciśnięcia klawisza, zanim faktycznie wykona wyszukiwanie — jeśli użytkownik pisze dalej, poprzednie oczekiwanie jest anulowane. Efekt: żądanie wysyłane jest dopiero, gdy użytkownik faktycznie PRZESTANIE pisać (choćby na chwilę), a nie po każdej literze.

Filtrowanie po stronie backendu (query string). Zamiast pobierać WSZYSTKIE produkty i filtrować je w Angularze (co przy dużym katalogu byłoby nieefektywne), backend przyjmuje parametry zapytania: GET /api/produkty?kategoria=elektronika&szukaj=klawiatura — kontroler odczytuje je jako zwykłe parametry metody C# (ASP.NET Core SAM mapuje parametry query string na parametry akcji o tej samej nazwie) i filtruje zapytanie LINQ/EF Core PRZED wysłaniem wyniku.

Koszyk jako serwis Angular (dokładnie ten wzorzec z lekcji 10 i projektu restauracji, lekcja 23) — KoszykService przechowuje aktualnie dodane produkty i ich ilości, udostępnia metody dodaj(), usun(), obliczSume(), i jest wstrzykiwany zarówno w komponent nagłówka (pokazujący licznik), jak i w komponent samego koszyka/checkout.

Schemat

KatalogComponent
  <input [(ngModel)]="fraza" (ngModelChange)="naZmianeSzukania($event)">
        │
        │  RxJS: this.szukajZmiana$.pipe(debounceTime(300)).subscribe(...)
        ▼
  GET /api/produkty?kategoria=X&szukaj=Y
        │
        ▼
ASP.NET Core: ProduktyController.GetWszystkie(string? kategoria, string? szukaj)
        │
        ├── query = context.Produkty.AsQueryable();
        ├── if (kategoria != null) query = query.Where(p => p.Kategoria == kategoria);
        ├── if (szukaj != null) query = query.Where(p => p.Nazwa.Contains(szukaj));
        │
        ▼
   return Ok(await query.ToListAsync());

KoszykService (singleton)
  dodaj(produkt) / usun(id) / obliczSume()
        │           │
        ▼           ▼
NaglowekComponent   KoszykComponent (checkout)
(licznik produktów) (lista + suma + formularz danych wysyłki)

Przykład z życia

Każdy sklep internetowy (Allegro, Amazon, mniejsze sklepy branżowe) działa dokładnie na tej zasadzie: katalog z kategoriami i wyszukiwarką, koszyk widoczny w nagłówku niezależnie od tego, którą podstronę akurat przeglądasz, i finalne podsumowanie przed złożeniem zamówienia — to jeden z najbardziej rozpoznawalnych i praktycznych wzorców w całym świecie aplikacji webowych.

katalog.component.ts

// katalog.component.ts - wyszukiwanie z debounceTime
import { Component, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { ProduktService, Produkt } from '../produkt.service';

@Component({
  selector: 'app-katalog',
  standalone: true,
  templateUrl: './katalog.component.html'
})
export class KatalogComponent implements OnInit {
  produkty: Produkt[] = [];
  fraza = '';
  private szukajZmiana$ = new Subject<string>();

  constructor(private produktService: ProduktService) {}

  ngOnInit(): void {
    this.szukajZmiana$.pipe(debounceTime(300)).subscribe(fraza => {
      this.pobierzProdukty(fraza);
    });
    this.pobierzProdukty('');
  }

  naZmianeSzukania(fraza: string): void {
    this.szukajZmiana$.next(fraza);
  }

  private pobierzProdukty(fraza: string): void {
    this.produktService.szukaj(fraza).subscribe(dane => this.produkty = dane);
  }
}

Controllers/ProduktyController.cs

// Controllers/ProduktyController.cs - filtrowanie przez query string
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace SklepApp.Server.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProduktyController : ControllerBase
{
    private readonly SklepContext _context;
    public ProduktyController(SklepContext context) => _context = context;

    [HttpGet]
    public async Task<ActionResult<List<Produkt>>> GetWszystkie(
        [FromQuery] string? kategoria, [FromQuery] string? szukaj)
    {
        var zapytanie = _context.Produkty.AsQueryable();

        if (!string.IsNullOrWhiteSpace(kategoria))
        {
            zapytanie = zapytanie.Where(p => p.Kategoria == kategoria);
        }

        if (!string.IsNullOrWhiteSpace(szukaj))
        {
            zapytanie = zapytanie.Where(p => p.Nazwa.Contains(szukaj));
        }

        return Ok(await zapytanie.ToListAsync());
    }
}

Komentarz i wyjaśnienie kodu

Subject<string> z RxJS to "kran", do którego można "wlewać" wartości (.next(fraza)) i który jednocześnie jest Observable (można się do niego .subscribe()) — naZmianeSzukania wywoływana przy KAŻDYM naciśnięciu klawisza "wlewa" aktualną frazę, a subskrypcja z debounceTime(300) reaguje dopiero, gdy przez 300ms nic nowego nie "wlano".

[FromQuery] string? kategoria w kontrolerze C# JAWNIE oznacza, że ten parametr ma pochodzić z query string adresu URL (?kategoria=...) — w praktyce ASP.NET Core często wykrywa to automatycznie dla prostych typów jak string/int, ale jawne oznaczenie [FromQuery] jest czytelniejsze i jednoznaczne.

_context.Produkty.AsQueryable() zwraca zapytanie, które JESZCZE nie zostało wykonane — kolejne wywołania .Where(...) DOKŁADAJĄ warunki do tego samego zapytania SQL, a dopiero .ToListAsync() na końcu faktycznie wysyła je do bazy danych; dzięki temu filtrowanie po kategorii i frazie łączy się w JEDNO, efektywne zapytanie SQL, zamiast wielu osobnych.

Ćwiczenie samodzielne

Zbuduj katalog produktów z kategoriami i wyszukiwarką z debounceTime jak w przykładzie. Sprawdź w Narzędziach deweloperskich (zakładka Sieć), że żądanie do backendu wysyłane jest DOPIERO po chwili przerwy w pisaniu, a nie po każdej literze.

Zadania do pracy własnej

  1. Dodaj do katalogu rozwijaną listę (<select>) z kategoriami, filtrującą produkty razem z wyszukiwarką tekstową.

  2. Zbuduj KoszykService i komponent koszyka pokazujący dodane produkty z możliwością zmiany ilości i usunięcia pozycji, z licznikiem widocznym w nagłówku (współdzielony serwis, wzorzec z lekcji 10).

  3. Zbuduj pełny ekran "checkout" — formularz reaktywny (lekcja 11) z danymi wysyłki (imię, adres, telefon) i walidacją, wysyłający kompletne zamówienie (lista produktów + dane klienta) do backendu jako jedno żądanie POST, zapisujące zamówienie w bazie przez EF Core (analogicznie do modelu Zamowienie/PozycjaZamowienia z projektu restauracji, lekcja 23) i pokazujące ekran potwierdzenia z numerem zamówienia.

Typowe błędy

Wysyłanie żądania do backendu przy KAŻDYM naciśnięciu klawisza bez debounceTime — przy szybkim pisaniu generuje dziesiątki niepotrzebnych żądań sieciowych, obciążając niepotrzebnie backend.

Filtrowanie CAŁEGO katalogu po stronie Angulara zamiast na backendzie (przez query string) — działa dla małych katalogów testowych, ale nie skaluje się do prawdziwego sklepu z tysiącami produktów, gdzie pobranie WSZYSTKICH produktów za każdym razem byłoby bardzo nieefektywne.

Przechowywanie koszyka WYŁĄCZNIE w pamięci komponentu (nie w serwisie) — powoduje utratę danych koszyka przy przejściu na inną "stronę" (trasę) aplikacji, bo Angular niszczy komponent po opuszczeniu trasy; serwis (singleton, lekcja 10) przetrwa całą sesję użytkownika.

Nawiązanie do egzaminu zawodowego

To bezpośrednia realizacja "funkcjonalności e-sklepu", wymienionej WPROST w podstawie programowej INF.04.7.3 obok portalu społecznościowego, serwisu ogłoszeniowego i serwisu rezerwacyjnego. Kolejny projekt (lekcja 23) to system zamówień restauracji — pokrewna domena, ale z większym naciskiem na relacje między tabelami w bazie danych.