Zum Inhalt
wannacode

Das Schaufenster des Shops

Kurz gesagt: Ein Projekt für eine Web-App: das Schaufenster eines Shops mit einem Katalog aus einer API, einer Produktseite nach Adresse, einem gemeinsamen Warenkorb, einer Bestellung mit Prüfungen und „Meine Bestellungen“.

Problem

Das Schaufenster des Shops

Der Shop hat eine API: Produkte, Kategorien, Bestellungen. Was fehlt, ist das Schaufenster, durch das der Käufer einkauft. Der Weg des Käufers ist:

  1. Den Katalog durchsuchen oder eine Kategorie wählen.
  2. Ein Produkt öffnen und in den Warenkorb legen.
  3. Im Warenkorb die Mengen ändern.
  4. Bestellen: Name, E-Mail, Adresse. Ein Fehler zeigt sich dort, wo er ist.
  5. Die Bestellung unter „Meine Bestellungen“ sehen.

Jeden Teil hast du in einer eigenen Lektion gebaut. Hier fügst du sie in einer App zusammen und baust sie in fünf Schritten: Jede Aufgabe beginnt mit der Lösung der vorigen.

Die Idee

Das Gerüst

Das ist die App bei Schritt null: das Menü und Platzhalterseiten. Klick durch das Menü.

Vue

Die Dateien sind angelegt wie in einem echten Projekt:

  • api.js enthält jede Anfrage. Die Seiten rufen getProducts, placeOrder auf und kennen die Adressen nicht. Ein Fehler vom Server kommt als Error mit status und errors (die Fehler pro Feld).
  • format.js formatiert Geld, Datum und Status.
  • pages/ sind die Seiten, eine pro Adresse. components/ sind die Teile, die die Seiten benutzen: ProductCard, Filter, Field.
  • store/cart ist der Warenkorb, den die ganze App teilt.

Der Router steht in router.js mit createMemoryHistory(), und main.js setzt createPinia() und router auf die App. Die Seite kommt in <RouterView /> in App.vue, wie in der Routing-Lektion.

Die Sandbox hat keine Adressleiste, deshalb liegt der Verlauf im Speicher. In einem echten Projekt ist es createWebHistory(). Der Rest ist gleich.

Filter und Field sind aus den Lektionen fertig: Felder, die an den Zustand gebunden sind, und ein Feld mit Label und Fehlermeldung. Du schreibst die Seiten und den Warenkorb.

Übung

Schritt 1: der Katalog

Der Katalog lädt die Produkte mit getProducts({ q, category }) und zeigt sie mit ProductCard in einer Liste mit der Klasse grid. Oben steht der Filter. Jede Änderung der Suche oder der Kategorie startet eine neue Anfrage, und die alte wird abgebrochen. Solange sie wartet, sagt die Seite „Lädt…“. Passt nichts, sagt sie „Keine passenden Produkte.“

Achte auf zwei Dinge aus der Lektion über Daten aus einer API:

  • Die Liste folgt dem Filter. Die Anfrage steht in watch([q, category], …, { immediate: true }), nicht in onMounted.
  • Die alte Antwort gewinnt nicht. Wenn „Milchprodukte“ langsam antwortet und du schon „Getränke“ gewählt hast, darf die langsame Antwort die neue nicht auslöschen. Ein AbortController bricht die alte Anfrage ab im onCleanup des watch.

Schreib CatalogPage: die Produkte aus der API nach dem Filter, „Lädt…“, „Keine passenden Produkte.“ und das Abbrechen der alten Anfrage. Die Prüfung sucht nach „brot“, wählt dann „Milchprodukte“ und gleich danach „Getränke“ (Milchprodukte antworten nach 900 ms) und sucht dann innerhalb einer Kategorie.

Vue

Übung

Schritt 2: die Produktseite

Der Name in ProductCard wird zu einem Link auf /product/:id. ProductPage nimmt id aus der Adresse, lädt das Produkt mit getProduct(id) und zeigt Name, Kategorie, Preis und Bestand: „Auf Lager: 18 Stk.“ oder „Ausverkauft“. Darunter steht „Mehr aus dieser Kategorie“: bis zu drei andere Produkte derselben Kategorie, mit Links.

Achte auf:

  • Der Link gehört dem Router: <RouterLink :to="…">, nicht <a href>. Ein normaler Link lädt die ganze Seite noch einmal. In der Sandbox führt er nirgendwohin.
  • Dieselbe Seite, ein anderer Parameter. Von „Weißkäse“ zu „Kaschkawal“ erzeugt der Router keine neue ProductPage, er ändert nur id. Die Anfrage steht in watch(() => route.params.id, …, { immediate: true }).
  • Die id aus der Adresse ist ein String: "7", nicht 7. getProduct(id) setzt sie in die Adresse, und das ist in Ordnung. Wenn du sie in einem Array mit === suchst, vergleichst du einen String mit einer Zahl.

Die Einzelheiten stehen in der Routing-Lektion.

Mach den Namen in ProductCard zu einem Link und schreib ProductPage. Die Prüfung öffnet „Weißkäse“, dann „Kaschkawal“ aus „Mehr aus dieser Kategorie“, geht zurück zum Katalog und öffnet die ausverkaufte „Banitsa mit Käse“.

Vue

Übung

Schritt 3: der Warenkorb

ProductPage und das Menü rufen schon useCart() auf: ein Button „In den Warenkorb“, „Im Warenkorb: 2“ und „Warenkorb (2)“. Vorerst ist useCart ein Platzhalter. Schreib den echten Warenkorb und CartPage.

store/cart.js ist ein Pinia-Store, erzeugt mit defineStore('cart', () => { … }), wie in der Lektion über gemeinsamen Zustand. Er gibt items, count, total, add, setQty, remove, clear zurück. count und total sind computed aus items, nicht getrennt gehalten.

CartPage zeigt eine Zeile pro Produkt: den Namen, ein Dropdown „Menge: Weißkäse“ (von 1 bis zum Bestand, höchstens 10), die Summe der Zeile und einen Button „Entfernen“ mit dem Namen „Entfernen Weißkäse“. Darunter stehen „Gesamt: …“ und ein Link „Zur Kasse“. Ein leerer Warenkorb sagt „Der Warenkorb ist leer.“

Schreib store/cart und CartPage. Die Prüfung legt zwei Weißkäse und Äpfel hinein, öffnet den Warenkorb, ändert den Käse auf 3 und entfernt die Äpfel, und achtet dabei auf das Menü und die Summe.

Vue

Übung

Schritt 4: die Kasse

CheckoutPage hat ein Formular mit drei Fields: „Name“, „E-Mail“, „Adresse“. Bei „Bestellen“:

  1. Eine Prüfung im Browser. Ein leerer Name: „Gib deinen Namen ein.“ Eine E-Mail ohne @: „Gib deine E-Mail-Adresse ein.“ Eine leere Adresse: „Gib deine Adresse ein.“ Gibt es einen Fehler, geht keine Anfrage raus.
  2. Die Anfrage. placeOrder({ name, email, address, items }), wobei items aus dem Warenkorb [{ productId, qty }] ist.
  3. Die Antwort. Bei 201 sagt die Seite zum Beispiel „Bestellung Nr. 61 über 15,80 €.“, und der Warenkorb wird geleert. Bei 400 ist err.errors ein Objekt pro Feld, zum Beispiel { email: "Die E-Mail-Adresse ist ungültig" }, und jede Meldung kommt neben ihr Feld.

Field zeigt error schon neben dem Feld, mit aria-invalid und aria-describedby. Das Formular hat novalidate, damit deine Meldungen sprechen und nicht die Sprechblasen des Browsers. Die Prüfung im Browser ist eine Bequemlichkeit: Sie fängt die leeren Felder ab, bevor eine Anfrage rausgeht. Der Server hat das letzte Wort, deshalb geht „maria@shop“ im Browser durch, und der Server schickt es zurück. Wie man ein Formular baut, steht in der Formular-Lektion.

Schreib CheckoutPage. Die Prüfung legt zwei Weißkäse hinein, klickt auf „Bestellen“ mit einem leeren Formular, dann mit der E-Mail „maria@shop“ (der Server lehnt sie ab) und zuletzt mit einer gültigen E-Mail.

Vue

Übung

Schritt 5: meine Bestellungen und der ganze Weg

OrdersPage lädt bei jedem Öffnen getOrders() und zeigt die Bestellungen des Kunden ME, die neueste oben: „Bestellung Nr. 61“, das Datum, den Status, die Summe und die Zeilen („Weißkäse × 3“). Solange sie wartet, „Lädt…“, und ohne Bestellungen „Du hast noch keine Bestellungen.“

Die Lern-API hat keine Anmeldung und liefert die Bestellungen jedes Kunden. Deshalb filtert die Seite nach customerId === ME. Ein echter Server liefert nur deine.

Jetzt ist die App ganz. Die Prüfung geht den Weg vom Anfang der Lektion: Sie sieht sich die alten Bestellungen an, filtert „Milchprodukte“, öffnet den Weißkäse, legt 2 hinein, macht im Warenkorb 3 daraus, bestellt mit einer falschen E-Mail, sieht den Fehler neben dem Feld, korrigiert ihn, bestellt und findet die neue Bestellung oben bei „Meine Bestellungen“.

Schreib OrdersPage. Die Prüfung geht den ganzen Weg des Käufers und sucht am Ende die neue Bestellung oben bei „Meine Bestellungen“.

Vue

Die Idee

Was hier vereinfacht ist

Die App ist echt, aber manches drumherum ist vereinfacht. In einem echten Shop:

  • Der Server weiß, wer du bist. Du meldest dich mit einem Passwort an, und der Server liefert nur deine Bestellungen, anhand der Sitzung. Ein Filter im Browser schützt nichts: Jeder kann ME ändern.
  • Der Preis liegt auf dem Server. Die Bestellung schickt nur productId und qty. Die Summe kommt in der Antwort zurück, berechnet aus den Preisen in der Datenbank.
  • Der Warenkorb bleibt erhalten. Hier liegt er im Speicher und ist beim Neuladen weg. Ein echter liegt in localStorage oder auf dem Server.
  • Die Adressen sind echt. createWebHistory() statt des Verlaufs im Speicher, und ein Hosting, das für jede Adresse index.html liefert.
Genauer, für Neugierige

Die zwei Bibliotheken. Der Weg ist derselbe: Adressen, Daten aus einer API, ein gemeinsamer Warenkorb, ein Formular mit Fehlern pro Feld. Vue macht es mit Reaktivität: watch auf den Parameter und den Filter, Pinia für den Warenkorb, v-model und defineModel für die Felder. Die andere Bibliothek löst dieselben Aufgaben auf ihre Weise.

Tests. Die Prüfungen der Schritte klicken und tippen wie ein Mensch. Dasselbe schreibst du mit Testing Library: render, screen.getByRole, userEvent. Wie das geht, steht in der Test-Lektion.

Geschwindigkeit. Der Katalog lädt bei jedem Tastendruck. Mit abgebrochener alter Anfrage ist das korrekt, aber verschwenderisch. In einem echten Projekt wartet die Suche 300 ms Ruhe ab, bevor sie rausgeht (Debounce).

Die Idee

Wie es weitergeht

Bring das Schaufenster in ein echtes Projekt, wie in der Lektion „Von der Sandbox zum Projekt“:

Terminal
npm create vite@latest shop -- --template vue
cd shop
npm install vue-router pinia

Kopiere die Ordner pages/, components/, store/ und die Dateien api.js, format.js, router.js, style.css nach src/. Tausche in router.js createMemoryHistory() gegen createWebHistory().

Die API ist nicht mehr nur gespielt. Starte einen echten Server mit den Adressen /api/products, /api/orders, oder leite /api mit server.proxy in vite.config.js darauf um. Dann npm run build und dist/ veröffentlichen, mit einer Regel, die für jede Adresse index.html liefert.

Weitere Aufgaben

  • Bestelldetails. /orders/:id mit GET /api/orders/:id und einem Link von „Meine Bestellungen“. Eine unbekannte Nummer zeigt „Keine solche Bestellung.“
  • Ein Warenkorb, der erhalten bleibt. Speichere items in localStorage und lies sie beim Start.
  • Tests. Ein Test für CartPage: Eine Mengenänderung ändert die Summe. Ein Test für CheckoutPage: Ein leeres Formular zeigt die drei Fehler und sendet keine Anfrage.

Test

Teste dich

1. Du wählst „Milchprodukte“, und die Liste behält jedes Produkt. Die Produkte werden in onMounted geladen. Was fehlt?
2. Bei „Weißkäse“ klickst du unter „Mehr aus dieser Kategorie“ auf „Kaschkawal“. Die Adresse wird /product/8, doch die Seite zeigt noch den Weißkäse. Warum?
3. Die Prüfung im Browser hat „maria@shop“ durchgelassen, und der Server antwortete 400 mit { errors: { email: "Die E-Mail-Adresse ist ungültig" } }. Was tut die Seite?
4. useCart ist eine Funktion, die bei jedem Aufruf ein ref([]) erzeugt, ohne Pinia. ProductPage fügt hinzu, und das Menü sagt „Warenkorb (0)“. Warum?
5. Warum schickt die Bestellung nur productId und qty, ohne die Preise?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·