branch-coexist: 3 neue decks 223015b-2026 / 223015c-2026 / dhbw-2026 daneben main
per user-request einen branch der main-content UND rewrite-content trägt — saubere separierung statt symlink/merge-conflict. struktur: - slides/<course>/ → main-content restored, eigene assets (kein new-demo-leak) - slides/<course>-2026/ → rewrite-content, eigene assets (komplett kopiert + new demos) Makefile: COURSES += 223015b-2026 223015c-2026 dhbw-2026 mit eigenen NAME/KAPITEL/DEPLOY/KLAUSUR-konfig. Klausur für b+c=1, dhbw=leer. Deploy-paths /hdm/223015b-2026 etc. make build durchlaufen für alle 6 decks, 0 fehler. dedup: 223015c+dhbw assets identisch in beiden ordnern (kein neu-demos auf b ergänzt). 223015b 86 → 120 files in -2026 (= +34 neue eroeffner/usb-c-drei-achsen/container-vs-codec/etc).
No files matched your search
@@ -0,0 +1,837 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: Web Engineering
|
||||
---
|
||||
|
||||
<style>
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; --color-dimmed: #4a4a6a; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
section.invert h2 { color: #f48fb1; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: invert -->
|
||||
<!-- _header: '' -->
|
||||
<!-- _backgroundColor: #000 -->
|
||||
|
||||

|
||||
|
||||
# Web Engineering
|
||||
|
||||
**DHBW Stuttgart** · Informatik / Wirtschaftsinformatik
|
||||
**Sommersemester 2026**
|
||||
|
||||
---
|
||||
|
||||
# Ressourcen zum Selbstlernen
|
||||
|
||||
- **CODE CRISPIES** — [codecrispi.es](https://codecrispi.es/)
|
||||
- **Online Code-Editor** — [codepen.io](https://codepen.io/pen/)
|
||||
- **MDN** — [developer.mozilla.org](https://developer.mozilla.org/de/)
|
||||
- **Flexbox-Spiel** — [flexboxfroggy.com](https://flexboxfroggy.com/)
|
||||
- **Grid-Spiel** — [cssgridgarden.com](https://cssgridgarden.com/)
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Herzlich Willkommen!
|
||||
|
||||
## 1. Sitzung: 08.05.2026
|
||||
|
||||
---
|
||||
|
||||
# Über mich
|
||||
|
||||

|
||||
|
||||
Michael Czechowski
|
||||
|
||||
- Software Engineer, ehemals freischaffend
|
||||
- Lektor an HdM Stuttgart + DHBW
|
||||
- librete.ch
|
||||
|
||||
---
|
||||
|
||||
# Vorlesungsplan (Auszug)
|
||||
|
||||
| Termin | Thema |
|
||||
|--------|-------|
|
||||
| 1 | Web Engineering (heute) |
|
||||
| 2 | CSS Extended |
|
||||
| 3 | Node.js Basics |
|
||||
| 4 | Node.js Advanced |
|
||||
| 5 | Testing |
|
||||
| 6 | TypeScript |
|
||||
| 7 | Docker |
|
||||
| 8 | Best Practices |
|
||||
|
||||
---
|
||||
|
||||
# Prüfungsleistung — 75 P + 10 Bonus
|
||||
|
||||
**Projekt-Abgabe** statt Klausur.
|
||||
|
||||
| Bereich | Punkte |
|
||||
|---------|-------:|
|
||||
| Projekt-Struktur + README | 10 |
|
||||
| Code-Qualität | 15 |
|
||||
| Git-Hygiene | 10 |
|
||||
| Tests | 10 |
|
||||
| Docker | 10 |
|
||||
| HTTP / API | 10 |
|
||||
| Frontend + A11y | 10 |
|
||||
| Präsentation + Bonus | 5 + 10 |
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Kontext: was ist „das Web"?
|
||||
|
||||
---
|
||||
|
||||
# Internet-Timeline
|
||||
|
||||
| Jahr | Was |
|
||||
|------|-----|
|
||||
| 1969 | ARPANET — 4 Knoten |
|
||||
| 1971 | Email (Ray Tomlinson, `@`) |
|
||||
| 1983 | TCP/IP-Standard |
|
||||
| **1989** | **WWW** (Berners-Lee, CERN) |
|
||||
| 1993 | Mosaic-Browser → Web wird populär |
|
||||
|
||||
---
|
||||
|
||||
# Das WWW (1989)
|
||||
|
||||
**Tim Berners-Lee am CERN.** Drei Standards:
|
||||
|
||||
- **HTTP** — Protokoll
|
||||
- **HTML** — Format
|
||||
- **URL** — Adresse
|
||||
|
||||
Patentfrei → Web konnte explodieren.
|
||||
|
||||
---
|
||||
|
||||
# Heute: physische Infrastruktur
|
||||
|
||||
- ~400 Unterseekabel
|
||||
- 1,3 Mio km Glasfaser weltweit
|
||||
- DE-CIX Frankfurt: größter Internet-Knoten (~12 Tbit/s)
|
||||
|
||||
Das Web ist **physisch.**
|
||||
|
||||
---
|
||||
|
||||
# URL → Pixel in 7 Schritten
|
||||
|
||||
1. **URL parsen** — Schema, Host, Port, Path
|
||||
2. **DNS-Lookup** — Host-Name → IP
|
||||
3. **TCP-Handshake** mit Server
|
||||
4. **TLS-Handshake** (https)
|
||||
5. **HTTP-Request** senden
|
||||
6. Server antwortet mit **HTML + CSS + JS**
|
||||
7. Browser **parsed + rendert** → Pixel
|
||||
|
||||
---
|
||||
|
||||
# HTTP — Anatomie
|
||||
|
||||
```http
|
||||
GET /index.html HTTP/1.1
|
||||
Host: librete.ch
|
||||
User-Agent: Mozilla/5.0 ...
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/html
|
||||
Content-Length: 1234
|
||||
|
||||
<!DOCTYPE html>...
|
||||
```
|
||||
|
||||
**Statuscodes:** 200 OK · 301 Moved · 404 Not Found · 500 Server Error
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Onboarding
|
||||
## Tools, Runtimes, Workflows
|
||||
|
||||
---
|
||||
|
||||
# Werkzeuge & Betriebssystem
|
||||
|
||||
| Bereich | Empfehlung |
|
||||
|---------|------------|
|
||||
| Editor | **VS Code** · JetBrains (edu lizenz frei) |
|
||||
| Browser | **Chromium / Firefox** |
|
||||
| Terminal | macOS Terminal · Linux Terminal · Windows Terminal |
|
||||
| Paket-Manager | macOS: brew · Linux: apt/dnf · Windows: winget/scoop/choco |
|
||||
| Versions-Control | Git + GitHub/Gitea |
|
||||
|
||||
---
|
||||
|
||||
# Node.js installieren
|
||||
|
||||
**Best Practice:** Versions-Manager statt Direkt-Install.
|
||||
|
||||
| OS | Empfehlung |
|
||||
|----|------------|
|
||||
| macOS | `brew install fnm` → `fnm install --lts` |
|
||||
| Linux | `curl -fsSL https://fnm.vercel.app/install \| bash` |
|
||||
| Windows | `winget install Schniz.fnm` |
|
||||
|
||||
**Aktuelle LTS:** Node 24.
|
||||
|
||||
---
|
||||
|
||||
# Warum Versions-Manager?
|
||||
|
||||
**Direkt-Install** (z.B. `brew install node`) bringt Probleme:
|
||||
- Projekt-A braucht Node 18, Projekt-B braucht Node 22 → Konflikt
|
||||
- Update bricht alte Projekte
|
||||
- Kein automatisches Switching pro Projekt
|
||||
|
||||
**fnm / nvm / volta:** pro Projekt Node-Version, automatisch via `.nvmrc`.
|
||||
|
||||
---
|
||||
|
||||
# Alternative Runtimes
|
||||
|
||||
| Runtime | Stärke |
|
||||
|---------|--------|
|
||||
| **Node** | Standard, größtes Ökosystem |
|
||||
| **Deno** | Sicher by default, TypeScript nativ |
|
||||
| **Bun** | Schnell, all-in-one (runtime + bundler + tester) |
|
||||
|
||||
Für DHBW-Kurs: **Node 24 LTS.**
|
||||
|
||||
---
|
||||
|
||||
# Browser DevTools
|
||||
|
||||
**F12** öffnet das Power-Tool jeder Webentwicklung.
|
||||
|
||||
| Tab | Wofür |
|
||||
|-----|-------|
|
||||
| Elements | DOM-Baum inspizieren + live editieren |
|
||||
| Console | JS ausführen, Errors lesen |
|
||||
| Network | HTTP-Requests sehen, Latenz |
|
||||
| Performance | Render + Memory profilen |
|
||||
| Lighthouse | A11y · SEO · Perf-Audit |
|
||||
|
||||
---
|
||||
|
||||
# HTTP-Clients
|
||||
|
||||
Tools zum Testen von APIs:
|
||||
|
||||
- **curl** — CLI, überall
|
||||
- **Hoppscotch** — Browser, Open Source
|
||||
- **Postman** — GUI, kommerziell
|
||||
- **VS Code REST Client** — Plugin
|
||||
|
||||
---
|
||||
|
||||
# Git Setup
|
||||
|
||||
```sh
|
||||
git config --global user.name "Dein Name"
|
||||
git config --global user.email "du@dhbw.de"
|
||||
git config --global init.defaultBranch main
|
||||
git config --global core.autocrlf input # Windows: true
|
||||
```
|
||||
|
||||
**SSH-Key** für GitHub/Gitea ohne Passwort-Prompt:
|
||||
```sh
|
||||
ssh-keygen -t ed25519 -C "du@dhbw.de"
|
||||
cat ~/.ssh/id_ed25519.pub # zu GitHub kopieren
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Projekt-Skeleton
|
||||
|
||||
```
|
||||
mein-projekt/
|
||||
├── .git/
|
||||
├── .gitignore # node_modules/, .env, build/
|
||||
├── README.md # was, wie, warum
|
||||
├── package.json # npm metadata + scripts
|
||||
├── src/ # eigener Code
|
||||
├── public/ # statische Assets (für Web)
|
||||
├── tests/ # Unit + Integration
|
||||
└── Dockerfile # für Container-Deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# HTML
|
||||
## Hypertext Markup Language
|
||||
|
||||
---
|
||||
|
||||
# Was ist HTML?
|
||||
|
||||
**Tim Berners-Lee, 1989.** Struktur-Format des Webs.
|
||||
|
||||
**Syntax:** Tags mit Attributen umfassen Inhalt.
|
||||
**Browser parst HTML** → DOM (Document Object Model) → rendert.
|
||||
|
||||
Plain-Text mit Tags. Lesbar mit jedem Editor.
|
||||
|
||||
---
|
||||
|
||||
# HTML-Anatomie
|
||||
|
||||
```html
|
||||
<a href="https://librete.ch" target="_blank">Mein Link</a>
|
||||
```
|
||||
|
||||
- `<a` — Opening-Tag mit Tag-Namen
|
||||
- `href="..."` — Attribut (Name + Wert)
|
||||
- `Mein Link` — Inhalt
|
||||
- `</a>` — Closing-Tag
|
||||
- Alles zusammen = **Element**
|
||||
|
||||
---
|
||||
|
||||
# Self-closing Tags
|
||||
|
||||
```html
|
||||
<img src="foto.jpg" alt="Sonnenuntergang">
|
||||
<br>
|
||||
<input type="text" name="username">
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
```
|
||||
|
||||
Kein Closing-Tag. Inhalt aus Attributen + (für `<input>`) User-Eingabe.
|
||||
|
||||
---
|
||||
|
||||
# Document-Grundgerüst
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Meine Seite</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hallo Welt</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Semantische HTML-Tags
|
||||
|
||||
| Tag | Bedeutung |
|
||||
|-----|-----------|
|
||||
| `<header>` | Kopfbereich (Logo, Nav) |
|
||||
| `<nav>` | Navigation |
|
||||
| `<main>` | Hauptinhalt |
|
||||
| `<section>` | Thematischer Abschnitt |
|
||||
| `<article>` | eigenständiger Inhalt |
|
||||
| `<aside>` | Seiten-Inhalt |
|
||||
| `<footer>` | Fußbereich |
|
||||
|
||||
**Pflicht** für SEO + A11y + Wartbarkeit.
|
||||
|
||||
---
|
||||
|
||||
# Native HTML-Elemente — nicht vergessen!
|
||||
|
||||
Bevor du eine Custom-Component baust: **gibt's das schon nativ?**
|
||||
|
||||
| Element | Was |
|
||||
|---------|-----|
|
||||
| `<details>` / `<summary>` | Accordion ohne JS |
|
||||
| `<dialog>` | Modal mit Native Backdrop |
|
||||
| `<input type="date">` | Date-Picker, native, A11y |
|
||||
| `<input type="color">` | Color-Picker |
|
||||
| `<input type="range">` | Slider |
|
||||
| `<datalist>` | Autocomplete-Vorschläge |
|
||||
| `<progress>` | Lade-Balken |
|
||||
|
||||
**Faustregel:** Native zuerst, Custom nur wenn nötig.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Barrierefreiheit
|
||||
## A11y — Accessibility
|
||||
|
||||
---
|
||||
|
||||
# BFSG + EAA — Pflicht ab 28.06.2025
|
||||
|
||||
**Barrierefreiheitsstärkungsgesetz** (DE) setzt **European Accessibility Act** (EU) um.
|
||||
|
||||
**Pflicht:**
|
||||
- Online-Shops, Banken, Versicherungen
|
||||
- E-Books, Buchungssysteme
|
||||
- Smartphone-Apps mit kommerziellem Bezug
|
||||
|
||||
**Ausnahme:** < 10 Mitarbeiter + < 2 Mio € Umsatz.
|
||||
|
||||
**Strafe:** bis 100.000 € Bußgeld + Klage durch Behindertenverbände.
|
||||
|
||||
---
|
||||
|
||||
# Wie nutzen Menschen das Web?
|
||||
|
||||
- **Sehen:** Screen-Reader (NVDA, JAWS, VoiceOver)
|
||||
- **Hören:** Untertitel + Transkripte
|
||||
- **Motorik:** Tastatur statt Maus, Switch-Control
|
||||
- **Kognitiv:** klare Sprache, viel Whitespace
|
||||
- **Vorübergehend:** Sonne im Bildschirm, gebrochener Arm, ÖPNV-Lärm
|
||||
|
||||
~10 % der Bevölkerung haben relevante Beeinträchtigungen. *Du auch — irgendwann.*
|
||||
|
||||
---
|
||||
|
||||
# WCAG · 4 Prinzipien (POUR)
|
||||
|
||||
| Prinzip | Was |
|
||||
|---------|-----|
|
||||
| **P**erceivable | Alt-Text · Untertitel · Kontrast |
|
||||
| **O**perable | Keyboard · Skip-Links |
|
||||
| **U**nderstandable | klare Sprache · Fehler-Hilfe |
|
||||
| **R**obust | semantisches HTML · Standards-konform |
|
||||
|
||||
**WCAG 2.1 Level AA** = Mindest-Standard für BFSG.
|
||||
|
||||
---
|
||||
|
||||
# Perceivable — wahrnehmbar
|
||||
|
||||
- **Alt-Text** für jedes Bild (`<img alt="...">`)
|
||||
- **Untertitel** für jedes Video
|
||||
- **Kontrast 4.5:1** für Text (Level AA)
|
||||
- **Skalierbar** bis 200 %
|
||||
|
||||
```html
|
||||
<img src="foto.jpg" alt="Sonnenuntergang am Bodensee">
|
||||
<!-- LEER alt="" für rein dekorative Bilder -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Operable — bedienbar
|
||||
|
||||
- Alle interaktiven Elemente **mit Tastatur erreichbar**
|
||||
- **Fokus sichtbar** (`:focus-visible` mit Outline)
|
||||
- **Skip-Links** für Screen-Reader (Hauptnav überspringen)
|
||||
- **Genug Zeit** für Aufgaben (Auto-Logout pausierbar)
|
||||
|
||||
```css
|
||||
button:focus-visible {
|
||||
outline: 3px solid hotpink;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Understandable — verständlich
|
||||
|
||||
- **Lese-Niveau** angemessen (Level AA: 9. Klasse)
|
||||
- **Klare Fehlermeldungen** (was war falsch, wie korrigieren)
|
||||
- **Konsistente Navigation** (Header bleibt gleich)
|
||||
- **`lang`-Attribut** im html-Element
|
||||
|
||||
```html
|
||||
<html lang="de">
|
||||
<p lang="en">This sentence is in English.</p>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Robust — Tech-stabil
|
||||
|
||||
- **Semantisches HTML** statt div-Suppe
|
||||
- **ARIA** *nur als Ergänzung*, nicht als Ersatz
|
||||
- **Standards-konform** (Validator: validator.w3.org)
|
||||
|
||||
```html
|
||||
<!-- ❌ semantisch falsch -->
|
||||
<div onclick="...">Klick</div>
|
||||
|
||||
<!-- ✓ semantisch korrekt -->
|
||||
<button onclick="...">Klick</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# A11y-Testen — Werkzeuge
|
||||
|
||||
| Tool | Was |
|
||||
|------|-----|
|
||||
| **Lighthouse** (Chrome DevTools) | automatischer Audit |
|
||||
| **axe DevTools** | Plugin, präziser |
|
||||
| **WAVE** | Browser-Plugin, visuell |
|
||||
| **Manuell** | Maus weglegen, nur Tab |
|
||||
| **Screen-Reader** | macOS: VoiceOver (Cmd+F5) · Win: NVDA |
|
||||
|
||||
**Automatisierte Tools** finden ~30 %. Rest = manuell.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# CSS
|
||||
## Cascading Style Sheets
|
||||
|
||||
*(Vertiefung in Kap 2 — CSS Extended)*
|
||||
|
||||
---
|
||||
|
||||
# CSS einbinden
|
||||
|
||||
```html
|
||||
<!-- External (best practice) -->
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
|
||||
<!-- Internal -->
|
||||
<style>
|
||||
body { background: #fafafa; }
|
||||
</style>
|
||||
|
||||
<!-- Inline (vermeiden) -->
|
||||
<button style="background: hotpink">Klick</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# CSS-Anatomie
|
||||
|
||||
```css
|
||||
button {
|
||||
background-color: hotpink;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
- `button` = **Selektor** (welche Elemente)
|
||||
- `background-color` = **Property**
|
||||
- `hotpink` = **Value**
|
||||
- alles in `{ }` = **Declaration Block**
|
||||
|
||||
---
|
||||
|
||||
# Selektoren
|
||||
|
||||
| Selektor | Was |
|
||||
|----------|-----|
|
||||
| `p` | alle `<p>` |
|
||||
| `.btn` | alle mit `class="btn"` |
|
||||
| `#header` | das Element mit `id="header"` |
|
||||
| `button:hover` | Button beim Hover |
|
||||
| `nav > a` | direkte `<a>` unter `<nav>` |
|
||||
| `*` | alles |
|
||||
|
||||
---
|
||||
|
||||
# Spezifizität — wer gewinnt?
|
||||
|
||||
```css
|
||||
p { color: blue; } /* 1 */
|
||||
.notice { color: red; } /* 10 */
|
||||
#warn { color: green; } /* 100 */
|
||||
[style] /* inline */ /* 1000 */
|
||||
```
|
||||
|
||||
**Inline > ID > Klasse > Element.** Bei Gleichstand: **letzte Regel.**
|
||||
|
||||
`!important` umgeht alles — *vermeiden.*
|
||||
|
||||
---
|
||||
|
||||
# Box-Model
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ margin │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ border │ │
|
||||
│ │ ┌──────────┐ │ │
|
||||
│ │ │ padding │ │ │
|
||||
│ │ │ content │ │ │
|
||||
│ │ └──────────┘ │ │
|
||||
│ └────────────────┘ │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
**Best Practice:** `* { box-sizing: border-box; }` global.
|
||||
|
||||
---
|
||||
|
||||
# Farben
|
||||
|
||||
| Notation | Beispiel |
|
||||
|----------|----------|
|
||||
| Hex | `#FF5733` |
|
||||
| RGB | `rgb(255, 87, 51)` |
|
||||
| RGBA | `rgb(255, 87, 51, 0.5)` |
|
||||
| HSL | `hsl(11, 100%, 60%)` |
|
||||
| OKLCH (modern) | `oklch(0.7 0.2 30)` |
|
||||
| Schlüsselwort | `hotpink`, `tomato` |
|
||||
|
||||
---
|
||||
|
||||
# Einheiten
|
||||
|
||||
| Einheit | Was |
|
||||
|---------|-----|
|
||||
| `px` | Pixel, fix |
|
||||
| `rem` | relativ zu Wurzel-Font (16 px default) |
|
||||
| `em` | relativ zu Eltern-Font |
|
||||
| `%` | relativ zur Eltern-Größe |
|
||||
| `vw` / `vh` | Viewport-Breite/-Höhe |
|
||||
| `ch` | Breite eines Zeichens |
|
||||
|
||||
**Faustregel:** Font + Spacing in `rem`. Layout in `%` oder `fr`.
|
||||
|
||||
---
|
||||
|
||||
# Pseudo-Klassen & -Elemente
|
||||
|
||||
```css
|
||||
/* Pseudo-Klassen (Zustand) */
|
||||
a:hover { color: hotpink; }
|
||||
input:focus { outline: 2px solid; }
|
||||
button:disabled { opacity: 0.5; }
|
||||
li:nth-child(2n) { background: #eee; }
|
||||
|
||||
/* Pseudo-Elemente (Virtuelles) */
|
||||
p::first-letter { font-size: 2em; }
|
||||
.tooltip::after { content: " 💡"; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# JavaScript
|
||||
## Die Sprache des Webs
|
||||
|
||||
*(Vertiefung in Kap 3+4 — Node.js)*
|
||||
|
||||
---
|
||||
|
||||
# ECMAScript
|
||||
|
||||
**ECMA International** (Schweiz) standardisiert die Sprache.
|
||||
|
||||
| Version | Jahr | Highlight |
|
||||
|---------|------|-----------|
|
||||
| ES5 | 2009 | strict mode, JSON |
|
||||
| ES6 / 2015 | 2015 | `let` `const`, arrow, classes, modules |
|
||||
| ES2017 | 2017 | async/await |
|
||||
| ES2020 | 2020 | optional chaining `?.` |
|
||||
| ES15 | 2024 | aktuell |
|
||||
|
||||
---
|
||||
|
||||
# JavaScript-Grundlagen
|
||||
|
||||
```javascript
|
||||
// Variablen
|
||||
const name = "Ada";
|
||||
let alter = 56;
|
||||
|
||||
// Funktionen
|
||||
const begrüßen = (name) => `Hallo ${name}`;
|
||||
|
||||
// Arrays + Objects
|
||||
const fruits = ["A", "B"];
|
||||
const user = { name: "Ada", admin: true };
|
||||
|
||||
// Schleifen
|
||||
fruits.map(f => f.toUpperCase());
|
||||
|
||||
// async
|
||||
async function laden() {
|
||||
const res = await fetch('/api');
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# DOM Manipulation
|
||||
|
||||
```javascript
|
||||
// Element finden
|
||||
const btn = document.querySelector('#myBtn');
|
||||
|
||||
// Inhalt ändern
|
||||
btn.textContent = 'Neu';
|
||||
btn.classList.add('active');
|
||||
|
||||
// Event-Handler
|
||||
btn.addEventListener('click', () => {
|
||||
console.log('Klick!');
|
||||
});
|
||||
|
||||
// Element erzeugen
|
||||
const li = document.createElement('li');
|
||||
li.textContent = 'Item';
|
||||
document.querySelector('ul').appendChild(li);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Fetch + JSON
|
||||
|
||||
```javascript
|
||||
// GET
|
||||
const res = await fetch('https://api.github.com/users/torvalds');
|
||||
const data = await res.json();
|
||||
console.log(data.public_repos);
|
||||
|
||||
// POST
|
||||
await fetch('/api/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: 'Hallo', body: '...' })
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Frameworks-Vorschau
|
||||
|
||||
---
|
||||
|
||||
# Framework-Landschaft
|
||||
|
||||
| Framework | Typ | Use-Case |
|
||||
|-----------|-----|----------|
|
||||
| **React** | Library | dominant, große Community |
|
||||
| **Vue 3** | Framework | sanfter Einstieg |
|
||||
| **Svelte** | Compiler | kein Virtual DOM, kleines Bundle |
|
||||
| **Astro** | Hybrid | Content-First, Multi-Framework |
|
||||
| **Angular** | Framework | Enterprise, Google |
|
||||
|
||||
Alle: **Komponenten + reaktive State + Re-Render.**
|
||||
|
||||
---
|
||||
|
||||
# Counter — Vanilla + React
|
||||
|
||||
**Vanilla JS:**
|
||||
```javascript
|
||||
let count = 0;
|
||||
btn.onclick = () => { count++; display.textContent = count; };
|
||||
```
|
||||
|
||||
**React (JSX):**
|
||||
```jsx
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Counter — Vue + Svelte
|
||||
|
||||
**Vue 3 (Composition):**
|
||||
```vue
|
||||
<script setup>const count = ref(0)</script>
|
||||
<template><button @click="count++">{{ count }}</button></template>
|
||||
```
|
||||
|
||||
**Svelte:**
|
||||
```svelte
|
||||
<script>let count = 0</script>
|
||||
<button on:click={() => count++}>{count}</button>
|
||||
```
|
||||
|
||||
Vier Frameworks, **dasselbe Pattern:** State → UI re-rendert.
|
||||
|
||||
---
|
||||
|
||||
# Build-Tools
|
||||
|
||||
| Tool | Stärke |
|
||||
|------|--------|
|
||||
| **Vite** | schnell, modern, Default für React/Vue/Svelte |
|
||||
| **Webpack** | etabliert, mächtig, verbreitet |
|
||||
| **Parcel** | zero-config |
|
||||
| **Bun** | all-in-one, sehr schnell |
|
||||
|
||||
**Heute:** Vite ist der Default für neue Projekte.
|
||||
|
||||
---
|
||||
|
||||
# Rendering-Modi
|
||||
|
||||
| Modus | Wann |
|
||||
|-------|------|
|
||||
| **CSR** (Client-Side) | Single-Page-Apps (Dashboard, Editor) |
|
||||
| **SSR** (Server-Side) | SEO + erste Latenz wichtig (News, Shop) |
|
||||
| **SSG** (Static Site Gen) | reine Content-Sites (Blog, Doku) |
|
||||
| **ISR** (Incremental SSG) | Hybrid: Content selten ändert |
|
||||
|
||||
Astro / Next.js / Nuxt unterstützen alle.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — Erste eigene Seite
|
||||
|
||||
1. **Mappe anlegen** `meine-seite/`
|
||||
2. `index.html` mit dem **Grundgerüst** (DOCTYPE, head, body, h1, p)
|
||||
3. `style.css` daneben — mit Hotpink-Background
|
||||
4. **VS Code Live Server** starten → im Browser öffnen
|
||||
5. **DevTools** auf, F12, Tab Elements → DOM-Baum anschauen
|
||||
|
||||
**Bonus:** Lighthouse Audit laufen lassen. Welchen Score erreicht ihr?
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- Internet-Geschichte → WWW → URL→Pixel-7-Schritte
|
||||
- Onboarding-Stack: Node (fnm), VS Code, Git, DevTools
|
||||
- HTML: Anatomie, semantisch, A11y-Foundation
|
||||
- A11y: BFSG/EAA-Pflicht, WCAG POUR
|
||||
- CSS-Basics (Vertiefung in Kap 2)
|
||||
- JS-Basics (Vertiefung in Kap 3+4)
|
||||
- Framework-Landschaft (Vorschau)
|
||||
|
||||
**Nächste Stunde:** CSS deeper — Flexbox, Grid, Responsive, Animations.
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
---
|
||||
|
||||
<style>
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; --color-dimmed: #4a4a6a; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
section.invert h2 { color: #f48fb1; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Kapitel 2 — CSS Extended
|
||||
## Box-Model · Flexbox · Grid · Responsive · Animations
|
||||
|
||||
---
|
||||
|
||||
# Recap Kap 1
|
||||
|
||||
| Bereich | Was wir kennen |
|
||||
|---------|----------------|
|
||||
| Anatomie | `selector { property: value }` |
|
||||
| Selektoren | element, `.class`, `#id`, `:hover` |
|
||||
| Spezifizität | inline > id > class > element |
|
||||
| Farben | hex, rgb, hsl, oklch |
|
||||
| Einheiten | px, rem, em, %, vw, vh |
|
||||
|
||||
**Heute:** Layout + Animation in Tiefe.
|
||||
|
||||
---
|
||||
|
||||
# Box-Model — die Erinnerung
|
||||
|
||||
```
|
||||
┌──────────────────┐
|
||||
│ margin │
|
||||
│ ┌────────────┐ │
|
||||
│ │ border │ │
|
||||
│ │ ┌────────┐ │ │
|
||||
│ │ │padding │ │ │
|
||||
│ │ │content │ │ │
|
||||
│ │ └────────┘ │ │
|
||||
│ └────────────┘ │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
`box-sizing: border-box` global setzen.
|
||||
|
||||
---
|
||||
|
||||
# Box-Model in Code
|
||||
|
||||
```css
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
.card {
|
||||
width: 320px; /* inkl. padding + border */
|
||||
padding: 1.5rem;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 0.75rem;
|
||||
margin: 1rem auto; /* Außen + horizontal zentriert */
|
||||
}
|
||||
```
|
||||
|
||||
Inspector im DevTools zeigt das Box-Model live an.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Flexbox
|
||||
## 1D-Layout (Reihe ODER Spalte)
|
||||
|
||||
---
|
||||
|
||||
# Flexbox — Basics
|
||||
|
||||
```css
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 1rem; /* Abstand zwischen Items */
|
||||
flex-direction: row; /* row | column */
|
||||
justify-content: space-between; /* Haupt-Achse */
|
||||
align-items: center; /* Quer-Achse */
|
||||
flex-wrap: wrap; /* umbruchen erlaubt */
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<div class="container">
|
||||
<div>1</div>
|
||||
<div>2</div>
|
||||
<div>3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Flexbox — Achsen
|
||||
|
||||
**`flex-direction: row` (default):** Haupt-Achse horizontal.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ◄── justify-content ──► │
|
||||
│ ▲ │
|
||||
│ │ │
|
||||
│ align-items │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
`column`: Achsen drehen sich 90°.
|
||||
|
||||
---
|
||||
|
||||
# Flexbox — Item-Properties
|
||||
|
||||
```css
|
||||
.item {
|
||||
flex: 1; /* nimm gleiche Anteile vom verfügbaren Raum */
|
||||
flex: 0 0 200px; /* fest 200px, kein wachsen, kein schrumpfen */
|
||||
flex-grow: 2; /* doppelte Anteile */
|
||||
order: -1; /* visuell zuerst, im DOM hinten */
|
||||
align-self: end; /* override align-items für DIESES Item */
|
||||
}
|
||||
```
|
||||
|
||||
**Shorthand:** `flex: <grow> <shrink> <basis>`.
|
||||
|
||||
---
|
||||
|
||||
# Typische Flex-Patterns
|
||||
|
||||
```css
|
||||
/* Navigation: Logo links, Items rechts */
|
||||
.nav { display: flex; justify-content: space-between; }
|
||||
|
||||
/* Karte: Bild + Text nebeneinander */
|
||||
.card { display: flex; gap: 1rem; align-items: center; }
|
||||
|
||||
/* Button-Gruppe: alle gleich groß */
|
||||
.btn-group { display: flex; gap: 0.5rem; }
|
||||
.btn-group button { flex: 1; }
|
||||
|
||||
/* Footer: gleichmäßig verteilt */
|
||||
.footer { display: flex; justify-content: space-around; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Grid
|
||||
## 2D-Layout (Reihe UND Spalte)
|
||||
|
||||
---
|
||||
|
||||
# Grid — Basics
|
||||
|
||||
```css
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr; /* 3 Spalten, Verhältnis 1:2:1 */
|
||||
grid-template-rows: auto 1fr auto; /* Header, Body, Footer */
|
||||
gap: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
`fr` = Fraction Unit — verbleibender Raum aufgeteilt.
|
||||
|
||||
---
|
||||
|
||||
# Grid — Areas (named layout)
|
||||
|
||||
```css
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"nav main"
|
||||
"footer footer";
|
||||
grid-template-columns: 200px 1fr;
|
||||
}
|
||||
|
||||
.layout header { grid-area: header; }
|
||||
.layout nav { grid-area: nav; }
|
||||
.layout main { grid-area: main; }
|
||||
.layout footer { grid-area: footer; }
|
||||
```
|
||||
|
||||
Lesbarer als Spalten-/Zeilen-Nummern.
|
||||
|
||||
---
|
||||
|
||||
# Grid — Auto-Layout
|
||||
|
||||
```css
|
||||
/* So viele Spalten wie reinpassen, jede min. 200px */
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
Mobile: 1 Spalte. Tablet: 2-3. Desktop: 4-5. **Ohne Media Queries.**
|
||||
|
||||
---
|
||||
|
||||
# Flexbox vs. Grid
|
||||
|
||||
| | Flexbox | Grid |
|
||||
|--|---------|------|
|
||||
| Dimension | 1D | 2D |
|
||||
| Use-Case | Navigation, Karten-Reihe, Button-Gruppe | Seiten-Layout, komplexes Raster |
|
||||
| Item-Größe | flexibel | meist fest |
|
||||
| Direction | row/column | rows + columns |
|
||||
|
||||
**Beide in einem Layout möglich:** Grid für Seite, Flexbox in Komponenten.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Responsive Design
|
||||
|
||||
---
|
||||
|
||||
# Mobile-First mit Media Queries
|
||||
|
||||
```css
|
||||
/* Default: Mobile */
|
||||
.sidebar { display: none; }
|
||||
.content { padding: 1rem; }
|
||||
|
||||
/* Tablet aufwärts */
|
||||
@media (min-width: 768px) {
|
||||
.sidebar { display: block; width: 240px; }
|
||||
.content { padding: 2rem; }
|
||||
}
|
||||
|
||||
/* Desktop */
|
||||
@media (min-width: 1280px) {
|
||||
.sidebar { width: 320px; }
|
||||
}
|
||||
```
|
||||
|
||||
**Mobile-First**: kleinste Größe als Default, dann größer.
|
||||
|
||||
---
|
||||
|
||||
# Viewport-Meta
|
||||
|
||||
```html
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
```
|
||||
|
||||
**Pflicht** im `<head>`. Ohne dieses Meta zoomt iPhone & Android alles auf 980 px Breite.
|
||||
|
||||
---
|
||||
|
||||
# Fluid Sizing — ohne Media Queries
|
||||
|
||||
```css
|
||||
/* Font: skaliert zwischen 14px und 22px */
|
||||
h1 { font-size: clamp(1.4rem, 2.5vw + 1rem, 2.5rem); }
|
||||
|
||||
/* Padding: skaliert mit Viewport */
|
||||
.card { padding: clamp(1rem, 3vw, 3rem); }
|
||||
|
||||
/* Breite: nie mehr als 70 Zeichen für Lesetext */
|
||||
.prose { max-width: 70ch; margin: auto; }
|
||||
```
|
||||
|
||||
`clamp(min, ideal, max)` ist modernes Allzweck-Werkzeug.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Animationen
|
||||
|
||||
---
|
||||
|
||||
# Transitions — sanfter Wechsel
|
||||
|
||||
```css
|
||||
button {
|
||||
background: hotpink;
|
||||
transition: background 0.2s ease, transform 0.15s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: deeppink;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
```
|
||||
|
||||
**Was animiert performant:** `transform`, `opacity`. **Was nicht:** `width`, `height`, `top/left`.
|
||||
|
||||
---
|
||||
|
||||
# Keyframe-Animationen
|
||||
|
||||
```css
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.1); opacity: 0.7; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.notification {
|
||||
animation: pulse 1.5s ease infinite;
|
||||
}
|
||||
```
|
||||
|
||||
`animation: <name> <duration> <timing> <iteration>`.
|
||||
|
||||
---
|
||||
|
||||
# Reduced Motion — Pflicht
|
||||
|
||||
Nicht alle wollen Animationen. (Vestibuläre Probleme, ADHS, Migräne.)
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Standard-Bestandteil** jeder Seite — ein User-Setting respektieren.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# CSS-Variablen + Modern
|
||||
|
||||
---
|
||||
|
||||
# Custom Properties (CSS Variables)
|
||||
|
||||
```css
|
||||
:root {
|
||||
--primary: hotpink;
|
||||
--gap: 1rem;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--primary);
|
||||
padding: var(--gap);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* Dark Mode: nur Variablen umsetzen */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --primary: deeppink; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# CSS Functions
|
||||
|
||||
```css
|
||||
.box {
|
||||
width: calc(100% - 2rem); /* Mathe */
|
||||
font-size: clamp(1rem, 2vw, 2rem); /* min, ideal, max */
|
||||
background: linear-gradient(45deg, hotpink, deeppink);
|
||||
color: oklch(0.7 0.2 30); /* moderner Farbraum */
|
||||
filter: blur(2px) brightness(1.2); /* Bildeffekte */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# CSS-Frameworks
|
||||
|
||||
| Framework | Stil |
|
||||
|-----------|------|
|
||||
| **Tailwind CSS** | Utility-Klassen direkt im HTML |
|
||||
| **Bootstrap** | Komponenten-Bibliothek (klassisch) |
|
||||
| **Pico** | klassen-frei, semantisches HTML reicht |
|
||||
| **Open Props** | CSS-Variablen-Set |
|
||||
|
||||
```html
|
||||
<!-- Tailwind: alle Styles als Klassen -->
|
||||
<button class="bg-pink-500 hover:bg-pink-600 px-4 py-2 rounded-md">
|
||||
Klick
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Tailwind — Argumente dafür/dagegen
|
||||
|
||||
**Pro:**
|
||||
- Schnell, weniger Context-Switch (HTML + CSS in einer Datei)
|
||||
- Konsistent (Design-System eingebaut)
|
||||
- Tree-Shaking → kleines Bundle
|
||||
|
||||
**Contra:**
|
||||
- HTML voller Klassen — schwerer zu lesen
|
||||
- Lernkurve eigene
|
||||
- Versions-Lock-in mit Setup
|
||||
|
||||
**Faustregel:** für neue Projekte ausprobieren, vorhandene Projekte nicht zwanghaft migrieren.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — Layout-Übung
|
||||
|
||||
1. **Flexbox-Spiel:** [flexboxfroggy.com](https://flexboxfroggy.com) — alle 24 Level durch.
|
||||
2. **Grid-Spiel:** [cssgridgarden.com](https://cssgridgarden.com) — 28 Level durch.
|
||||
3. **Eigene Karten-Galerie:** `repeat(auto-fit, minmax(...))` Pattern anwenden.
|
||||
|
||||
**Bonus:** Dark-Mode mit CSS-Variablen + `prefers-color-scheme`.
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- Box-Model + `box-sizing: border-box` global
|
||||
- **Flexbox** für 1D-Layouts (Navigation, Karten-Reihen)
|
||||
- **Grid** für 2D-Layouts (Seiten-Layout, Galerien)
|
||||
- Mobile-First Media Queries + `clamp()` für fluid
|
||||
- Transitions + Keyframes — `prefers-reduced-motion` respektieren
|
||||
- CSS-Variablen für Theming
|
||||
- Tailwind & Co. als moderner Framework-Stil
|
||||
|
||||
**Nächste Stunde:** Node.js — JavaScript außerhalb des Browsers.
|
||||
@@ -0,0 +1,361 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
---
|
||||
|
||||
<style>
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; --color-dimmed: #4a4a6a; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Kapitel 3 — Node.js Basics
|
||||
## JavaScript außerhalb des Browsers
|
||||
|
||||
---
|
||||
|
||||
# Was ist Node.js?
|
||||
|
||||
**Ryan Dahl, 2009.** JavaScript-Runtime auf V8 (Chrome's JS-Engine).
|
||||
|
||||
Vorher: JS war Browser-only.
|
||||
Mit Node: JS auch als Server, CLI-Tools, Build-Skripte.
|
||||
|
||||
**Heute:** Node ist Standard-Backend für viele Web-Apps. Netflix, LinkedIn, NASA, Walmart.
|
||||
|
||||
---
|
||||
|
||||
# Warum Node?
|
||||
|
||||
| Vorteil | Was bedeutet das |
|
||||
|---------|------------------|
|
||||
| **JS überall** | gleiche Sprache Frontend + Backend |
|
||||
| **NPM** | größtes Package-Ökosystem (~3 Mio Packages) |
|
||||
| **Non-blocking I/O** | viele Verbindungen gleichzeitig |
|
||||
| **Schnell genug** | V8 optimiert JS aggressiv |
|
||||
| **Cross-platform** | Linux, macOS, Windows |
|
||||
|
||||
---
|
||||
|
||||
# Node Versions-Manager
|
||||
|
||||
**fnm** (oder `nvm`, `volta`) — pro Projekt Node-Version.
|
||||
|
||||
```sh
|
||||
# Installieren (macOS/Linux)
|
||||
brew install fnm # oder curl-script
|
||||
|
||||
# Aktuelle LTS
|
||||
fnm install --lts
|
||||
fnm use lts-latest # 24.x
|
||||
|
||||
# Pro Projekt
|
||||
echo "24" > .nvmrc
|
||||
fnm use # liest .nvmrc
|
||||
```
|
||||
|
||||
**Aktuell:** Node 24 LTS.
|
||||
|
||||
---
|
||||
|
||||
# Projekt initialisieren
|
||||
|
||||
```sh
|
||||
mkdir mein-projekt && cd mein-projekt
|
||||
npm init -y # erzeugt package.json
|
||||
|
||||
# Abhängigkeiten installieren
|
||||
npm install express # → dependencies
|
||||
npm install -D vitest # → devDependencies (nur dev)
|
||||
|
||||
# Skripte ausführen
|
||||
npm run start
|
||||
npm test
|
||||
```
|
||||
|
||||
`-y` = alle Defaults akzeptieren.
|
||||
|
||||
---
|
||||
|
||||
# package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mein-projekt",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node --watch src/server.js",
|
||||
"start": "node src/server.js",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`"type": "module"` aktiviert ESM.
|
||||
|
||||
---
|
||||
|
||||
# ESM — modern
|
||||
|
||||
```javascript
|
||||
// utils.js
|
||||
export function greet(name) {
|
||||
return `Hallo ${name}`;
|
||||
}
|
||||
|
||||
export const VERSION = "1.0";
|
||||
|
||||
// main.js
|
||||
import { greet, VERSION } from './utils.js';
|
||||
console.log(greet('Ada'), VERSION);
|
||||
```
|
||||
|
||||
**Pflicht:** `.js`-Endung in import-Pfad.
|
||||
|
||||
---
|
||||
|
||||
# CommonJS — Legacy
|
||||
|
||||
```javascript
|
||||
// utils.cjs
|
||||
function greet(name) {
|
||||
return `Hallo ${name}`;
|
||||
}
|
||||
module.exports = { greet };
|
||||
|
||||
// main.cjs
|
||||
const { greet } = require('./utils.cjs');
|
||||
```
|
||||
|
||||
**Wann CJS?** Alte Codebases, npm-Packages, die nicht migriert sind. **Default heute:** ESM.
|
||||
|
||||
---
|
||||
|
||||
# Built-in Modules
|
||||
|
||||
```javascript
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { createServer } from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
```
|
||||
|
||||
`node:` Prefix macht explizit klar — kein npm-Package.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# HTTP-Server
|
||||
|
||||
---
|
||||
|
||||
# Eingebauter HTTP-Server
|
||||
|
||||
```javascript
|
||||
import { createServer } from 'node:http';
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hallo Welt');
|
||||
});
|
||||
|
||||
server.listen(3000, () => {
|
||||
console.log('Server läuft auf :3000');
|
||||
});
|
||||
```
|
||||
|
||||
Nackt — ohne Framework. **Funktioniert.**
|
||||
|
||||
---
|
||||
|
||||
# Express — der Klassiker
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
const app = express();
|
||||
|
||||
app.use(express.json()); // parsed JSON-Body
|
||||
app.use(express.static('public')); // statische Dateien
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Hallo Welt');
|
||||
});
|
||||
|
||||
app.post('/api/echo', (req, res) => {
|
||||
res.json({ youSent: req.body });
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# REST mit Express
|
||||
|
||||
```javascript
|
||||
const users = [{ id: 1, name: 'Ada' }];
|
||||
|
||||
app.get('/users', (req, res) => res.json(users));
|
||||
|
||||
app.get('/users/:id', (req, res) => {
|
||||
const user = users.find(u => u.id == req.params.id);
|
||||
user ? res.json(user) : res.sendStatus(404);
|
||||
});
|
||||
|
||||
app.post('/users', (req, res) => {
|
||||
const user = { id: users.length + 1, ...req.body };
|
||||
users.push(user);
|
||||
res.status(201).json(user);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Alternative Frameworks
|
||||
|
||||
| Framework | Stärke |
|
||||
|-----------|--------|
|
||||
| **Express** | etabliert, größte Community |
|
||||
| **Fastify** | schneller, schemas eingebaut |
|
||||
| **Hono** | leichtgewichtig, Edge-deployable |
|
||||
| **Koa** | von Express-Autoren, modern |
|
||||
|
||||
Für DHBW: **Express** oder **Hono**.
|
||||
|
||||
---
|
||||
|
||||
# Routing-Pattern
|
||||
|
||||
```javascript
|
||||
import { Router } from 'express';
|
||||
const userRouter = Router();
|
||||
|
||||
userRouter.get('/', (req, res) => { /* alle Users */ });
|
||||
userRouter.get('/:id', (req, res) => { /* ein User */ });
|
||||
userRouter.post('/', (req, res) => { /* User anlegen */ });
|
||||
userRouter.put('/:id', (req, res) => { /* User update */ });
|
||||
userRouter.delete('/:id', (req, res) => { /* User löschen */ });
|
||||
|
||||
app.use('/users', userRouter); // mountet unter /users/*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Fetch in Node
|
||||
|
||||
---
|
||||
|
||||
# fetch — global ab Node 18
|
||||
|
||||
```javascript
|
||||
// GET
|
||||
const res = await fetch('https://api.github.com/users/torvalds');
|
||||
const data = await res.json();
|
||||
console.log(data.public_repos);
|
||||
|
||||
// POST
|
||||
const post = await fetch('https://api.example.com/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: 'Hallo' })
|
||||
});
|
||||
```
|
||||
|
||||
Genau wie im Browser. **Kein extra Package** (vorher: `node-fetch`, `axios`).
|
||||
|
||||
---
|
||||
|
||||
# Error-Handling
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API call failed:', error.message);
|
||||
throw error; // re-throw für caller
|
||||
}
|
||||
```
|
||||
|
||||
`res.ok` ist `true` für 2xx-Statuscodes.
|
||||
|
||||
---
|
||||
|
||||
# Environment Variables
|
||||
|
||||
```javascript
|
||||
// .env (gitignored!)
|
||||
DATABASE_URL=postgres://...
|
||||
API_KEY=sk-...
|
||||
PORT=3000
|
||||
|
||||
// src/config.js
|
||||
import 'dotenv/config';
|
||||
|
||||
export const config = {
|
||||
port: process.env.PORT || 3000,
|
||||
dbUrl: process.env.DATABASE_URL,
|
||||
apiKey: process.env.API_KEY,
|
||||
};
|
||||
```
|
||||
|
||||
**Niemals** API-Keys ins Repo committen.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — Mini-API
|
||||
|
||||
1. **Projekt initialisieren:** `npm init -y`, ESM aktivieren
|
||||
2. **Express installieren**
|
||||
3. **REST-API** für eine Ressource (Books / Movies / TODO)
|
||||
- `GET /items`, `GET /items/:id`, `POST /items`, `DELETE /items/:id`
|
||||
4. **In-Memory-Store** als Array (echte DB kommt Kap 4)
|
||||
5. **Mit Hoppscotch oder curl** testen
|
||||
|
||||
**Bonus:** `npm run dev` mit `--watch`-Flag.
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- Node.js als JS-Runtime außerhalb Browser
|
||||
- **fnm + Node 24 LTS** als Versions-Setup
|
||||
- **npm init**, package.json, scripts, dependencies
|
||||
- **ESM** (import/export) als modernes Modul-System
|
||||
- Built-in HTTP-Server + **Express** für REST
|
||||
- **fetch** global ab Node 18
|
||||
- **.env + dotenv** für Secrets
|
||||
|
||||
**Nächste Stunde:** async/await, Filesystem, Datenbank, Auth.
|
||||
@@ -0,0 +1,380 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
---
|
||||
|
||||
<style>
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; --color-dimmed: #4a4a6a; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Kapitel 4 — Node.js Advanced
|
||||
## async · Files · Database · Auth · Errors
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Asynchron + Promises
|
||||
|
||||
---
|
||||
|
||||
# Synchron vs. Asynchron
|
||||
|
||||
**Synchron** (blockierend):
|
||||
```javascript
|
||||
const data = readFileSync('huge.txt'); // wartet
|
||||
console.log('weiter'); // erst danach
|
||||
```
|
||||
|
||||
**Asynchron** (non-blocking):
|
||||
```javascript
|
||||
const data = await readFile('huge.txt'); // wartet ohne blockieren
|
||||
console.log('weiter'); // erst danach
|
||||
```
|
||||
|
||||
Node's Stärke: **non-blocking I/O** für viele parallele Operationen.
|
||||
|
||||
---
|
||||
|
||||
# Promise — Anatomie
|
||||
|
||||
```javascript
|
||||
const promise = fetch('https://api.example.com');
|
||||
|
||||
// then-style (alt)
|
||||
promise
|
||||
.then(res => res.json())
|
||||
.then(data => console.log(data))
|
||||
.catch(err => console.error(err));
|
||||
|
||||
// async/await (modern)
|
||||
try {
|
||||
const res = await fetch('https://api.example.com');
|
||||
const data = await res.json();
|
||||
console.log(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Sequenziell vs. Parallel
|
||||
|
||||
```javascript
|
||||
// SEQUENZIELL — langsam (warten + warten + warten)
|
||||
const a = await fetch('/api/a');
|
||||
const b = await fetch('/api/b');
|
||||
const c = await fetch('/api/c');
|
||||
// Gesamtzeit: a + b + c
|
||||
|
||||
// PARALLEL — schnell (alle gleichzeitig starten)
|
||||
const [a, b, c] = await Promise.all([
|
||||
fetch('/api/a'),
|
||||
fetch('/api/b'),
|
||||
fetch('/api/c'),
|
||||
]);
|
||||
// Gesamtzeit: max(a, b, c)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Promise-Methoden
|
||||
|
||||
| Methode | Was |
|
||||
|---------|-----|
|
||||
| `Promise.all([…])` | alle erfolgreich ODER ein Fehler |
|
||||
| `Promise.allSettled([…])` | alle, Resultat = { status, value/reason } |
|
||||
| `Promise.race([…])` | erstes Resultat gewinnt (auch Fehler) |
|
||||
| `Promise.any([…])` | erstes erfolgreiches, ignore Fehler |
|
||||
|
||||
```javascript
|
||||
const fastest = await Promise.race([
|
||||
fetch('/api'),
|
||||
new Promise((_, rej) => setTimeout(() => rej('timeout'), 3000))
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Filesystem
|
||||
|
||||
---
|
||||
|
||||
# fs/promises — Files lesen + schreiben
|
||||
|
||||
```javascript
|
||||
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
||||
|
||||
// Lesen
|
||||
const text = await readFile('input.txt', 'utf-8');
|
||||
const buf = await readFile('image.png'); // Buffer
|
||||
|
||||
// Schreiben
|
||||
await writeFile('output.txt', 'Hallo Welt');
|
||||
await writeFile('data.json', JSON.stringify({ x: 1 }, null, 2));
|
||||
|
||||
// Verzeichnis lesen
|
||||
const files = await readdir('./uploads');
|
||||
|
||||
// Verzeichnis anlegen
|
||||
await mkdir('./tmp', { recursive: true });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Streams für große Dateien
|
||||
|
||||
```javascript
|
||||
import { createReadStream, createWriteStream } from 'node:fs';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
|
||||
// 10 GB Logfile ohne RAM-Explosion verarbeiten
|
||||
await pipeline(
|
||||
createReadStream('huge.log'),
|
||||
// ... transform-stream ...
|
||||
createWriteStream('output.log')
|
||||
);
|
||||
```
|
||||
|
||||
Streams = Daten **stückweise** verarbeiten, nicht alles in RAM.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Datenbank
|
||||
|
||||
---
|
||||
|
||||
# Welche DB für DHBW-Projekte?
|
||||
|
||||
| DB | Wann |
|
||||
|----|------|
|
||||
| **SQLite** | klein, embedded, eine Datei. Lokal/dev/demo. |
|
||||
| **PostgreSQL** | Standard für „echte" Apps. Open Source, mächtig. |
|
||||
| **MongoDB** | NoSQL, dokumentenorientiert. Selten erste Wahl. |
|
||||
| **Redis** | Cache, Queue, schnell. |
|
||||
|
||||
Für Prüfungsleistung: **SQLite** (kein Setup) oder **Postgres** (production-realistisch).
|
||||
|
||||
---
|
||||
|
||||
# SQLite mit `better-sqlite3`
|
||||
|
||||
```javascript
|
||||
import Database from 'better-sqlite3';
|
||||
const db = new Database('app.db');
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Insert
|
||||
const insert = db.prepare('INSERT INTO users (name) VALUES (?)');
|
||||
insert.run('Ada');
|
||||
|
||||
// Query
|
||||
const users = db.prepare('SELECT * FROM users').all();
|
||||
const ada = db.prepare('SELECT * FROM users WHERE id = ?').get(1);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# ORM / Query-Builder
|
||||
|
||||
Für komplexere Apps: ORM statt Raw-SQL.
|
||||
|
||||
| Tool | Typ |
|
||||
|------|-----|
|
||||
| **Drizzle** | TypeScript-first, modern, lightweight |
|
||||
| **Prisma** | umfangreich, eigenes Schema |
|
||||
| **Knex** | Query-Builder, kein ORM |
|
||||
| **TypeORM** | klassisches ORM |
|
||||
|
||||
```typescript
|
||||
// Drizzle Beispiel
|
||||
const result = await db.select().from(users).where(eq(users.id, 1));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# CRUD-Pattern mit Express + SQLite
|
||||
|
||||
```javascript
|
||||
app.get('/users', (req, res) => {
|
||||
const users = db.prepare('SELECT * FROM users').all();
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
app.get('/users/:id', (req, res) => {
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
||||
user ? res.json(user) : res.sendStatus(404);
|
||||
});
|
||||
|
||||
app.post('/users', (req, res) => {
|
||||
const { name } = req.body;
|
||||
const result = db.prepare('INSERT INTO users (name) VALUES (?)').run(name);
|
||||
res.status(201).json({ id: result.lastInsertRowid, name });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Authentifizierung
|
||||
|
||||
---
|
||||
|
||||
# Auth-Strategien
|
||||
|
||||
| Strategie | Wann |
|
||||
|-----------|------|
|
||||
| **Session-Cookie** | klassische Web-App, Server-Side Render |
|
||||
| **JWT** (JSON Web Token) | API + SPA, stateless |
|
||||
| **OAuth** | „Mit Google einloggen", Drittpartei-Login |
|
||||
| **Magic Link** | Email statt Passwort, modern |
|
||||
| **Passkeys** | passwordless, FIDO2, Zukunft |
|
||||
|
||||
Für DHBW-Projekt: **Session-Cookie** (express-session) ODER **JWT**.
|
||||
|
||||
---
|
||||
|
||||
# Passwörter — *niemals plaintext*
|
||||
|
||||
```javascript
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
// Registrieren
|
||||
const hashed = await bcrypt.hash('mein-passwort', 12);
|
||||
db.prepare('INSERT INTO users (email, password) VALUES (?, ?)').run(email, hashed);
|
||||
|
||||
// Login
|
||||
const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
|
||||
const valid = await bcrypt.compare(password, user.password);
|
||||
```
|
||||
|
||||
**bcrypt** oder **argon2** für Passwort-Hashing. Niemals MD5/SHA1.
|
||||
|
||||
---
|
||||
|
||||
# JWT — kurzer Überblick
|
||||
|
||||
```javascript
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
// Signieren (beim Login)
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id, role: user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '1d' }
|
||||
);
|
||||
res.cookie('token', token, { httpOnly: true, secure: true });
|
||||
|
||||
// Verifizieren (Middleware)
|
||||
const token = req.cookies.token;
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = payload;
|
||||
```
|
||||
|
||||
`httpOnly + secure + sameSite` — XSS/CSRF-Schutz.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Error-Handling
|
||||
|
||||
---
|
||||
|
||||
# try/catch + Error-Klassen
|
||||
|
||||
```javascript
|
||||
class NotFoundError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'NotFoundError';
|
||||
this.status = 404;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await getUser(id);
|
||||
if (!user) throw new NotFoundError(`User ${id} not found`);
|
||||
res.json(user);
|
||||
} catch (err) {
|
||||
if (err instanceof NotFoundError) return res.status(404).json({ error: err.message });
|
||||
console.error(err);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Globaler Express-Error-Handler
|
||||
|
||||
```javascript
|
||||
// Am Ende aller Routes!
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err);
|
||||
|
||||
if (err.status) {
|
||||
return res.status(err.status).json({ error: err.message });
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
});
|
||||
```
|
||||
|
||||
4 Argumente: `(err, req, res, next)` → Express erkennt Error-Handler.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — REST-API mit DB
|
||||
|
||||
Erweitert eure Kap-3-API:
|
||||
|
||||
1. **SQLite-DB** mit `better-sqlite3`
|
||||
2. **CRUD** mit DB statt In-Memory-Array
|
||||
3. **Validierung** (z.B. mit `zod`) für POST-Body
|
||||
4. **Auth:** einfache Login-Route mit bcrypt + JWT
|
||||
5. **Global Error-Handler** für saubere Responses
|
||||
|
||||
**Bonus:** Async-Race-Condition simulieren + Promise.all nutzen.
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- **async/await + Promise** sauber anwenden, sequenziell vs. parallel
|
||||
- **fs/promises + Streams** für Dateien
|
||||
- **SQLite + Prepared Statements** als pragmatische DB
|
||||
- **bcrypt** für Passwörter, **JWT** als Token-Pattern
|
||||
- **Error-Klassen + Globaler Handler** in Express
|
||||
|
||||
**Nächste Stunde:** Testing — Vitest, TDD, Integration, E2E.
|
||||
@@ -0,0 +1,383 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: Testing
|
||||
---
|
||||
|
||||
<style>
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Kapitel 5 — Testing
|
||||
## Unit · Integration · E2E · TDD
|
||||
|
||||
---
|
||||
|
||||
# Warum testen?
|
||||
|
||||
| Vorteil | Was bedeutet das |
|
||||
|---------|------------------|
|
||||
| **Confidence** | Refactor ohne Angst |
|
||||
| **Regression-Schutz** | alter Bug taucht nicht wieder auf |
|
||||
| **Living Documentation** | Tests zeigen, was Code soll |
|
||||
| **CI/CD-Voraussetzung** | automatisch vor jedem Deploy |
|
||||
|
||||
**„Untested Code is broken by design."**
|
||||
|
||||
---
|
||||
|
||||
# Test-Pyramide
|
||||
|
||||
```
|
||||
/\
|
||||
/E2E\ wenige (langsam, fragil)
|
||||
/-----\
|
||||
/ Integ \ einige (mittel)
|
||||
/---------\
|
||||
/ Unit \ viele (schnell, isoliert)
|
||||
/-------------\
|
||||
```
|
||||
|
||||
**Faustregel:** 70 % Unit, 20 % Integration, 10 % E2E.
|
||||
|
||||
---
|
||||
|
||||
# Vitest — modernes JS-Testing
|
||||
|
||||
```sh
|
||||
npm install -D vitest
|
||||
```
|
||||
|
||||
```javascript
|
||||
// utils.test.js
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { greet } from './utils.js';
|
||||
|
||||
describe('greet', () => {
|
||||
it('returns greeting with name', () => {
|
||||
expect(greet('Ada')).toBe('Hallo Ada');
|
||||
});
|
||||
|
||||
it('handles empty string', () => {
|
||||
expect(greet('')).toBe('Hallo ');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```sh
|
||||
npx vitest
|
||||
npx vitest --coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Test-Struktur (AAA)
|
||||
|
||||
```javascript
|
||||
it('adds two numbers', () => {
|
||||
// ARRANGE — Setup
|
||||
const a = 2;
|
||||
const b = 3;
|
||||
|
||||
// ACT — die Operation
|
||||
const result = add(a, b);
|
||||
|
||||
// ASSERT — Erwartung
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
```
|
||||
|
||||
**Eine Erwartung pro Test** (Faustregel).
|
||||
|
||||
---
|
||||
|
||||
# Vitest-Matchers
|
||||
|
||||
| Matcher | Was |
|
||||
|---------|-----|
|
||||
| `.toBe(x)` | strict equal (`===`) |
|
||||
| `.toEqual(x)` | deep equal (Objects, Arrays) |
|
||||
| `.toBeNull()` / `.toBeUndefined()` | |
|
||||
| `.toBeTruthy()` / `.toBeFalsy()` | |
|
||||
| `.toContain(item)` | Array enthält |
|
||||
| `.toHaveLength(n)` | |
|
||||
| `.toThrow()` | wirft Fehler |
|
||||
| `.toMatchSnapshot()` | Snapshot-Test |
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# TDD — Test-Driven Development
|
||||
|
||||
---
|
||||
|
||||
# TDD-Cycle: Red → Green → Refactor
|
||||
|
||||
1. **Red:** Test schreiben (schlägt fehl)
|
||||
2. **Green:** minimaler Code, damit Test besteht
|
||||
3. **Refactor:** Code säubern, Test bleibt grün
|
||||
|
||||
**Kent Beck, 2002.** Steile Lernkurve, aber stark fokussiert.
|
||||
|
||||
```javascript
|
||||
// 1. Red — Test ohne Implementierung
|
||||
it('isEven returns true for even', () => {
|
||||
expect(isEven(4)).toBe(true);
|
||||
});
|
||||
// → ReferenceError: isEven is not defined
|
||||
|
||||
// 2. Green — minimale Lösung
|
||||
function isEven(n) { return n % 2 === 0; }
|
||||
|
||||
// 3. Refactor — Edge Cases, Lesbarkeit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Integration-Tests
|
||||
|
||||
---
|
||||
|
||||
# API testen mit Supertest
|
||||
|
||||
```javascript
|
||||
import request from 'supertest';
|
||||
import { app } from './app.js';
|
||||
|
||||
describe('GET /users', () => {
|
||||
it('returns users array', async () => {
|
||||
const res = await request(app).get('/users');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toBeInstanceOf(Array);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /users', () => {
|
||||
it('creates a user', async () => {
|
||||
const res = await request(app)
|
||||
.post('/users')
|
||||
.send({ name: 'Ada' });
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.name).toBe('Ada');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Test-Datenbank
|
||||
|
||||
```javascript
|
||||
import { beforeAll, afterEach, afterAll } from 'vitest';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
let db;
|
||||
|
||||
beforeAll(() => {
|
||||
db = new Database(':memory:'); // in-RAM, super schnell
|
||||
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.exec('DELETE FROM users'); // sauberer Start
|
||||
});
|
||||
|
||||
afterAll(() => db.close());
|
||||
```
|
||||
|
||||
`:memory:`-DB = jeder Test isoliert, blitzschnell.
|
||||
|
||||
---
|
||||
|
||||
# Mocking
|
||||
|
||||
```javascript
|
||||
import { vi, it, expect } from 'vitest';
|
||||
|
||||
it('calls fetch with right URL', async () => {
|
||||
const mockFetch = vi.spyOn(global, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ name: 'Ada' })
|
||||
});
|
||||
|
||||
await getUserData(1);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api/users/1');
|
||||
});
|
||||
```
|
||||
|
||||
**Mock sparsam** — übermockt-Tests testen Mocks, nicht Code.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# E2E — End-to-End
|
||||
|
||||
---
|
||||
|
||||
# Playwright — moderne E2E
|
||||
|
||||
```sh
|
||||
npm init playwright@latest
|
||||
```
|
||||
|
||||
```javascript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('login flow', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
await page.getByLabel('Email').fill('ada@example.com');
|
||||
await page.getByLabel('Password').fill('secret123');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page.getByText('Hallo Ada')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
Browser tatsächlich öffnen + interagieren. **Realer User-Flow.**
|
||||
|
||||
---
|
||||
|
||||
# Playwright-Locators
|
||||
|
||||
```javascript
|
||||
// Bevorzugt: rollenbasiert (A11y-konform!)
|
||||
page.getByRole('button', { name: 'Submit' });
|
||||
page.getByLabel('Username');
|
||||
page.getByText('Welcome');
|
||||
|
||||
// Fallback: CSS-Selektor
|
||||
page.locator('.submit-btn');
|
||||
page.locator('#username');
|
||||
```
|
||||
|
||||
**Rollenbasierte Locators** finden Bugs, die A11y betreffen.
|
||||
|
||||
---
|
||||
|
||||
# E2E vs. Unit — Trade-offs
|
||||
|
||||
| | Unit | E2E |
|
||||
|--|------|-----|
|
||||
| Geschwindigkeit | ms | s |
|
||||
| Isolation | hoch | gering |
|
||||
| Realismus | gering | hoch |
|
||||
| Wartung | wenig | viel |
|
||||
| Wann | jede Funktion | kritische Flows |
|
||||
|
||||
E2E nicht für jede Detail-Logik — für **happy path + Login + Bezahlung**.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Coverage + CI
|
||||
|
||||
---
|
||||
|
||||
# Coverage-Report
|
||||
|
||||
```sh
|
||||
npx vitest --coverage
|
||||
```
|
||||
|
||||
```
|
||||
File | % Stmts | % Branch | % Funcs | Uncovered
|
||||
-------------+---------+----------+---------+-----------
|
||||
utils.js | 100 | 100 | 100 |
|
||||
api.js | 75.3 | 60 | 80 | 42-48
|
||||
auth.js | 91.2 | 85 | 100 | 15
|
||||
```
|
||||
|
||||
**100 % ≠ keine Bugs.** Aber 0 % = sicher Bugs.
|
||||
|
||||
---
|
||||
|
||||
# Coverage-Ziel realistisch
|
||||
|
||||
| Code-Typ | Realistisch |
|
||||
|----------|-------------|
|
||||
| Pure Funktionen | 100 % |
|
||||
| Business-Logik | 80-90 % |
|
||||
| API-Routes | 70-80 % |
|
||||
| UI-Komponenten | 60-70 % |
|
||||
| Drittpartei-Wrapper | 0 % |
|
||||
| Konfig-Files | 0 % |
|
||||
|
||||
**Faustregel:** über 80 % Gesamt = gut. Unter 50 % = gefährlich.
|
||||
|
||||
---
|
||||
|
||||
# CI: Tests auf jedem Push
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run test:e2e
|
||||
```
|
||||
|
||||
Tests sind grün → PR mergebar. Sonst: rot, mergen verboten.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — Tests schreiben
|
||||
|
||||
Erweitert eure API:
|
||||
|
||||
1. **3+ Unit-Tests** für eure Helper-Funktionen (`utils.test.js`)
|
||||
2. **3+ Integration-Tests** für eure REST-Endpunkte (mit Supertest)
|
||||
3. **In-Memory-DB** in `beforeAll`/`afterEach`
|
||||
4. **1 E2E-Test** mit Playwright (z.B. Login-Flow)
|
||||
5. **Coverage-Report** generieren
|
||||
|
||||
**Bonus:** GitHub-Actions-Workflow für CI.
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- **Test-Pyramide:** viele Unit, einige Integration, wenige E2E
|
||||
- **Vitest** als modernes Test-Runner-Setup
|
||||
- **AAA-Pattern** + Matchers + Lifecycle-Hooks
|
||||
- **TDD-Cycle** Red → Green → Refactor
|
||||
- **Supertest** für API-Integration-Tests
|
||||
- **Playwright** für E2E mit rollenbasierten Locators
|
||||
- **Coverage** als Werkzeug, nicht als Ziel
|
||||
- **CI** mit GitHub-Actions
|
||||
|
||||
**Nächste Stunde:** TypeScript — Typen über JavaScript.
|
||||
@@ -0,0 +1,360 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: TypeScript
|
||||
---
|
||||
|
||||
<style>
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Kapitel 6 — TypeScript
|
||||
## Typen über JavaScript
|
||||
|
||||
---
|
||||
|
||||
# Was ist TypeScript?
|
||||
|
||||
**Microsoft, 2012.** Aufsatz auf JavaScript mit **statischer Typprüfung.**
|
||||
|
||||
- Schreibst TypeScript (`.ts`)
|
||||
- Compiler (`tsc`) prüft Typen + erzeugt JavaScript (`.js`)
|
||||
- Browser/Node führt das JS aus
|
||||
|
||||
**Heute:** Standard für mittlere + große JS-Projekte. ~95 % der React-Repos.
|
||||
|
||||
---
|
||||
|
||||
# Warum TypeScript?
|
||||
|
||||
| Vorteil | Was bedeutet das |
|
||||
|---------|------------------|
|
||||
| **Fehler vor Laufzeit** | Compiler fängt typos, falsche Aufrufe |
|
||||
| **Bessere IDE** | Autocompletion, Refactoring, Jump-to-Definition |
|
||||
| **Selbst-Doku** | Typ-Signaturen sind ausführbare Doku |
|
||||
| **Refactoring sicher** | Compiler findet alle Auswirkungen |
|
||||
|
||||
**Nachteil:** Build-Step + Lernkurve.
|
||||
|
||||
---
|
||||
|
||||
# Setup
|
||||
|
||||
```sh
|
||||
npm install -D typescript @types/node tsx
|
||||
npx tsc --init # tsconfig.json erzeugen
|
||||
```
|
||||
|
||||
`tsx` = TypeScript direkt ausführen ohne Compile-Step (Dev).
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "tsx --watch src/server.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# tsconfig.json — Wichtigste
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`strict: true`** = alle strengen Checks an. **Pflicht** für neue Projekte.
|
||||
|
||||
---
|
||||
|
||||
# Basis-Typen
|
||||
|
||||
```typescript
|
||||
let name: string = "Ada";
|
||||
let age: number = 56;
|
||||
let admin: boolean = true;
|
||||
let nothing: null = null;
|
||||
let unset: undefined = undefined;
|
||||
|
||||
let hobbies: string[] = ["math", "chess"];
|
||||
let tuple: [string, number] = ["Ada", 56];
|
||||
|
||||
let anyValue: any = "vermeiden"; // KEIN typ-check
|
||||
let unknownValue: unknown = await fetch(); // sicherer als any
|
||||
```
|
||||
|
||||
`any` deaktiviert TS-Schutz — vermeiden.
|
||||
|
||||
---
|
||||
|
||||
# Object-Typen + Interfaces
|
||||
|
||||
```typescript
|
||||
// inline
|
||||
function greet(user: { name: string; age?: number }) {
|
||||
return `Hallo ${user.name}`;
|
||||
}
|
||||
|
||||
// Interface
|
||||
interface User {
|
||||
name: string;
|
||||
age?: number; // optional
|
||||
readonly id: number; // schreibgeschützt
|
||||
}
|
||||
|
||||
const ada: User = { id: 1, name: "Ada", age: 56 };
|
||||
|
||||
// Type-Alias (Interface mit anderem Namen, mehr Flexibilität)
|
||||
type Status = "pending" | "active" | "deleted";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Function-Typen
|
||||
|
||||
```typescript
|
||||
// Argument + Return
|
||||
function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Arrow + ausgeschrieben
|
||||
const multiply: (a: number, b: number) => number = (a, b) => a * b;
|
||||
|
||||
// async-Funktionen geben Promise zurück
|
||||
async function fetchUser(id: number): Promise<User> {
|
||||
const res = await fetch(`/users/${id}`);
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Union + Intersection
|
||||
|
||||
```typescript
|
||||
// Union — "ODER"
|
||||
type Id = string | number;
|
||||
function lookup(id: string | number) { /* ... */ }
|
||||
|
||||
// Discriminated Union
|
||||
type Shape =
|
||||
| { kind: "circle"; radius: number }
|
||||
| { kind: "square"; size: number };
|
||||
|
||||
function area(s: Shape) {
|
||||
if (s.kind === "circle") return Math.PI * s.radius ** 2;
|
||||
return s.size ** 2; // TS weiß: hier ist's square
|
||||
}
|
||||
|
||||
// Intersection — "UND"
|
||||
type Admin = User & { permissions: string[] };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Generics — typ-flexible Funktionen
|
||||
|
||||
```typescript
|
||||
// T ist ein Platzhalter
|
||||
function first<T>(arr: T[]): T | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
first([1, 2, 3]); // T = number
|
||||
first(["a", "b"]); // T = string
|
||||
|
||||
// Generic-Constraints
|
||||
function longest<T extends { length: number }>(a: T, b: T): T {
|
||||
return a.length >= b.length ? a : b;
|
||||
}
|
||||
|
||||
longest("hello", "hi"); // OK
|
||||
longest([1, 2], [3, 4, 5]); // OK
|
||||
longest(5, 10); // FEHLER: number hat kein length
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Built-in Generic-Typen
|
||||
|
||||
```typescript
|
||||
// Array<T> = T[]
|
||||
const nums: Array<number> = [1, 2, 3];
|
||||
|
||||
// Promise<T>
|
||||
async function getUser(): Promise<User> { /* ... */ }
|
||||
|
||||
// Partial<T> — alle Properties optional
|
||||
type UserUpdate = Partial<User>;
|
||||
|
||||
// Required<T> — alle Pflicht
|
||||
// Pick<T, K> — nur ausgewählte
|
||||
// Omit<T, K> — ohne ausgewählte
|
||||
type PublicUser = Omit<User, "password">;
|
||||
|
||||
// Record<K, V> — Dictionary
|
||||
const config: Record<string, string> = { host: "...", port: "8080" };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Typ-Inference
|
||||
|
||||
TS errät den Typ oft selbst — **nicht überall annotieren.**
|
||||
|
||||
```typescript
|
||||
let name = "Ada"; // automatisch: string
|
||||
const numbers = [1, 2, 3]; // automatisch: number[]
|
||||
|
||||
function double(n: number) { // Return-Typ inferiert: number
|
||||
return n * 2;
|
||||
}
|
||||
|
||||
// Wann annotieren?
|
||||
// - Funktion-Args (immer)
|
||||
// - Public-API Return-Typen (Pflicht für Library)
|
||||
// - wenn Inference falsch oder unklar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Typen für REST-API
|
||||
|
||||
```typescript
|
||||
// Schema einmal definieren
|
||||
interface CreateUserBody {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface UserResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// Express-Route typisiert
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
app.post('/users', (req: Request<{}, {}, CreateUserBody>, res: Response<UserResponse>) => {
|
||||
const { name, email } = req.body; // typsicher
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Zod — Runtime-Validation
|
||||
|
||||
TypeScript prüft zur Compile-Time. **Was kommt aus dem Netz?**
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const UserSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
type User = z.infer<typeof UserSchema>; // Typ aus Schema
|
||||
|
||||
app.post('/users', (req, res) => {
|
||||
const result = UserSchema.safeParse(req.body);
|
||||
if (!result.success) return res.status(400).json(result.error);
|
||||
|
||||
const user = result.data; // typsicher + validated
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# `any` vs `unknown` vs `never`
|
||||
|
||||
```typescript
|
||||
let a: any = "test";
|
||||
a.toFixed(); // OK — TS prüft NICHTS
|
||||
|
||||
let u: unknown = "test";
|
||||
u.toFixed(); // FEHLER — erst type-narrow
|
||||
if (typeof u === 'number') u.toFixed(); // OK
|
||||
|
||||
function fail(msg: string): never {
|
||||
throw new Error(msg); // kommt nie zurück
|
||||
}
|
||||
```
|
||||
|
||||
**`unknown` statt `any`** — sicherer Default für fremde Daten.
|
||||
|
||||
---
|
||||
|
||||
# Best Practices
|
||||
|
||||
- **`strict: true`** in tsconfig — keine Diskussion
|
||||
- **Argument-Typen** annotieren, Return-Typen nur public-API
|
||||
- **Interfaces** für Objekt-Strukturen, **Type-Aliases** für Unions
|
||||
- **`unknown`** statt `any`
|
||||
- **Zod / Valibot** für Runtime-Validation
|
||||
- **Typen nicht doppelt:** ein Schema, davon Typ ableiten (`z.infer`)
|
||||
- **CI-Step:** `tsc --noEmit` muss grün sein
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — eure API zu TS migrieren
|
||||
|
||||
1. **TypeScript installieren:** `npm install -D typescript @types/node @types/express tsx`
|
||||
2. **tsconfig.json** mit `strict: true`
|
||||
3. **`.js` → `.ts`** umbenennen, fehlende Typen ergänzen
|
||||
4. **Zod-Schema** für POST-Bodies
|
||||
5. **CI-Step:** `tsc --noEmit` in Tests
|
||||
|
||||
**Bonus:** Drizzle-ORM verwenden (TypeScript-first).
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- TS als statischer Typ-Aufsatz auf JS
|
||||
- **`strict: true`** als Baseline
|
||||
- Basis-Typen, Interfaces, Type-Aliases
|
||||
- **Union** + **Generics** als zentrale Konzepte
|
||||
- Built-in Generics (Partial, Omit, Record)
|
||||
- **Typ-Inference** nutzen — nicht über-annotieren
|
||||
- **Zod** für Runtime-Validation
|
||||
- `unknown` statt `any`
|
||||
|
||||
**Nächste Stunde:** Docker — Container für reproducible Setups.
|
||||
@@ -0,0 +1,383 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: Docker und Service Orchestration
|
||||
---
|
||||
|
||||
<style>
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Kapitel 7 — Docker
|
||||
## Container für reproducible Setups
|
||||
|
||||
---
|
||||
|
||||
# „Auf meinem Rechner läuft's"
|
||||
|
||||
Das klassische Software-Problem:
|
||||
|
||||
- Entwickler: Node 22 + Postgres 16 lokal
|
||||
- Server: Node 18 + Postgres 14
|
||||
- → bricht beim Deploy
|
||||
|
||||
**Lösung:** App + Abhängigkeiten als **Container** verpacken.
|
||||
|
||||
---
|
||||
|
||||
# Container vs. VM
|
||||
|
||||
| | Container | Virtual Machine |
|
||||
|--|-----------|-----------------|
|
||||
| Startzeit | ms | min |
|
||||
| Größe | MB | GB |
|
||||
| Kernel | shared (Host-Linux) | eigener |
|
||||
| Isolation | Prozess-Level | Hardware-Level |
|
||||
| Overhead | minimal | hoch |
|
||||
|
||||
Container = leichtgewichtige Prozess-Isolation.
|
||||
|
||||
---
|
||||
|
||||
# Docker — die Idee
|
||||
|
||||
- **Image** = Schnappschuss (App + Files + OS-Layer)
|
||||
- **Container** = laufende Instanz eines Images
|
||||
- **Dockerfile** = Anleitung zum Image-Bauen
|
||||
- **Registry** = Image-Speicher (Docker Hub, GHCR)
|
||||
|
||||
```sh
|
||||
docker pull node:24
|
||||
docker run node:24 node --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Dockerfile — Hallo Welt
|
||||
|
||||
```dockerfile
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "src/server.js"]
|
||||
```
|
||||
|
||||
```sh
|
||||
docker build -t mein-app .
|
||||
docker run -p 3000:3000 mein-app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Dockerfile-Schichten verstehen
|
||||
|
||||
Jedes `RUN`/`COPY`/`ADD` = neue Schicht.
|
||||
|
||||
```dockerfile
|
||||
# Schlecht — npm install nach JEDER Code-Änderung
|
||||
COPY . .
|
||||
RUN npm ci
|
||||
|
||||
# Gut — package.json zuerst, npm install gecached
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
```
|
||||
|
||||
**Build-Cache** spart Minuten.
|
||||
|
||||
---
|
||||
|
||||
# Multi-Stage Build
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Build mit allem
|
||||
FROM node:24-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN npm ci && npm run build
|
||||
|
||||
# Stage 2: nur Runtime
|
||||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY package*.json ./
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/server.js"]
|
||||
```
|
||||
|
||||
Finales Image: kleiner, sicherer (kein Build-Tools drin).
|
||||
|
||||
---
|
||||
|
||||
# Image-Größen reduzieren
|
||||
|
||||
| Base | Größe |
|
||||
|------|------:|
|
||||
| `node:24` | ~1.1 GB |
|
||||
| `node:24-slim` | ~280 MB |
|
||||
| `node:24-alpine` | **~180 MB** |
|
||||
| Distroless | ~150 MB |
|
||||
|
||||
**`alpine`** = Alpine Linux, sehr klein. **Distroless** = nur Runtime, kein Shell.
|
||||
|
||||
---
|
||||
|
||||
# .dockerignore
|
||||
|
||||
Verhindert dass Müll ins Image kommt:
|
||||
|
||||
```
|
||||
node_modules
|
||||
.git
|
||||
.env
|
||||
*.log
|
||||
build/
|
||||
dist/
|
||||
.DS_Store
|
||||
README.md
|
||||
.github/
|
||||
```
|
||||
|
||||
Schneller Build, kleineres Image, weniger Secrets im Image.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# docker compose
|
||||
|
||||
---
|
||||
|
||||
# Multi-Container — die Realität
|
||||
|
||||
Eine App braucht oft:
|
||||
- App-Server
|
||||
- Datenbank
|
||||
- Cache (Redis)
|
||||
- Reverse Proxy
|
||||
|
||||
**docker compose** orchestriert das.
|
||||
|
||||
---
|
||||
|
||||
# compose.yml — Beispiel
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgres://user:pass@db:5432/app
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: pass
|
||||
POSTGRES_DB: app
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# compose-Commands
|
||||
|
||||
```sh
|
||||
docker compose up # alles starten (Foreground)
|
||||
docker compose up -d # detached (background)
|
||||
docker compose down # alles stoppen + entfernen
|
||||
docker compose down -v # ... und Volumes löschen
|
||||
|
||||
docker compose logs -f app # Logs streamen
|
||||
docker compose exec app sh # Shell im laufenden Container
|
||||
docker compose ps # Status sehen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Networking in compose
|
||||
|
||||
Standard: alle Services im selben `default`-Netzwerk.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
# `db` ist der Service-Name → DNS-Hostname
|
||||
DATABASE_URL: postgres://...@db:5432/app
|
||||
```
|
||||
|
||||
**Service-Namen werden zu Hostnamen.** Kein localhost zwischen Containern.
|
||||
|
||||
---
|
||||
|
||||
# Volumes — Daten überleben
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
db-data: # named volume (managed by Docker)
|
||||
|
||||
services:
|
||||
db:
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data # named
|
||||
- ./backups:/backups # bind mount
|
||||
```
|
||||
|
||||
**Named Volumes** für DB-Daten. **Bind Mounts** für lokale Config.
|
||||
|
||||
---
|
||||
|
||||
# Environment + Secrets
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
env_file: .env # ganze .env-Datei laden
|
||||
environment:
|
||||
NODE_ENV: production # einzeln setzen
|
||||
DATABASE_URL: ${DATABASE_URL} # vom Host
|
||||
```
|
||||
|
||||
**Secrets nicht ins compose.yml.** Lieber `.env` (gitignored) oder Docker Secrets.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# CI/CD
|
||||
|
||||
---
|
||||
|
||||
# Docker in GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: Build + Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
tags: user/app:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Container-Registry
|
||||
|
||||
| Registry | Stärke |
|
||||
|----------|--------|
|
||||
| **Docker Hub** | Standard, kostenlos für Public |
|
||||
| **GHCR** (GitHub) | mit Repo verbunden |
|
||||
| **GitLab Registry** | mit Repo verbunden |
|
||||
| **AWS ECR** | für AWS-Hosting |
|
||||
|
||||
```sh
|
||||
# Tag + Push
|
||||
docker tag mein-app ghcr.io/user/mein-app:v1.0.0
|
||||
docker push ghcr.io/user/mein-app:v1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Health-Checks
|
||||
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
||||
```
|
||||
|
||||
```javascript
|
||||
// In Express
|
||||
app.get('/health', (req, res) => res.json({ ok: true }));
|
||||
```
|
||||
|
||||
Orchestrators (k8s, ECS, Coolify) entscheiden via HealthCheck, ob neu starten.
|
||||
|
||||
---
|
||||
|
||||
# Best Practices
|
||||
|
||||
- **`alpine`** oder **distroless** als Base
|
||||
- **Multi-stage builds** für kleinere Images
|
||||
- **`.dockerignore`** anlegen
|
||||
- **Non-root user** im Container (`USER node`)
|
||||
- **`COPY` nach `RUN npm ci`** für Cache-Hit
|
||||
- **Tags statt `latest`** für Reproducibility
|
||||
- **Health-Checks** für Orchestration
|
||||
- **Secrets via env, niemals im Image**
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — App dockerizen
|
||||
|
||||
1. **Dockerfile** für eure Node-API (multi-stage)
|
||||
2. **.dockerignore** mit node_modules, .env, .git
|
||||
3. **compose.yml** mit App + Postgres
|
||||
4. **`docker compose up`** lokal starten
|
||||
5. **Health-Check** auf `/health`-Endpoint
|
||||
|
||||
**Bonus:** GitHub-Actions-Workflow, der Image bei jedem Push baut.
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- Container vs. VM — Prozess-Isolation, leicht + schnell
|
||||
- **Dockerfile** mit `FROM`, `COPY`, `RUN`, `CMD`
|
||||
- **Multi-stage Builds** für kleine Production-Images
|
||||
- **alpine** + Distroless als Base
|
||||
- **docker compose** für Multi-Container-Setups
|
||||
- **Volumes** für persistente Daten
|
||||
- **Service-Namen als Hostnamen** im compose-Netzwerk
|
||||
- **CI/CD** mit GitHub Actions + Registry
|
||||
|
||||
**Nächste Stunde:** Best Practices — Git, README, SOLID, 12-Factor.
|
||||
@@ -0,0 +1,477 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: Best Practices
|
||||
---
|
||||
|
||||
<style>
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Kapitel 8 — Best Practices
|
||||
## Git · README · SOLID · 12-Factor · SemVer
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Git-Hygiene
|
||||
|
||||
---
|
||||
|
||||
# Sinnvolle Commits
|
||||
|
||||
**Schlecht:**
|
||||
```
|
||||
fix
|
||||
wip
|
||||
asdf
|
||||
final
|
||||
final2
|
||||
final-final
|
||||
```
|
||||
|
||||
**Gut:**
|
||||
```
|
||||
fix(auth): handle expired JWT correctly
|
||||
feat(api): add /users/:id/posts endpoint
|
||||
docs(readme): update Docker setup instructions
|
||||
test(utils): cover edge cases in greet()
|
||||
```
|
||||
|
||||
**Eine Commit-Message = Was + Warum, nicht Wie.**
|
||||
|
||||
---
|
||||
|
||||
# Conventional Commits
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
**Types:**
|
||||
- `feat:` neue Funktion
|
||||
- `fix:` Bugfix
|
||||
- `docs:` Doku
|
||||
- `refactor:` Code-Restrukturierung
|
||||
- `test:` Tests
|
||||
- `chore:` Build/Tools
|
||||
- `perf:` Performance
|
||||
- `style:` Formatierung
|
||||
|
||||
Automatisches Changelog + SemVer-Bump möglich.
|
||||
|
||||
---
|
||||
|
||||
# .gitignore — was nie rein gehört
|
||||
|
||||
```
|
||||
# Dependencies + Build
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.cache/
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
.env.local
|
||||
*.pem *.key
|
||||
|
||||
# Editor + OS
|
||||
.vscode/ .idea/
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
```
|
||||
|
||||
Vorlagen: [github.com/github/gitignore](https://github.com/github/gitignore)
|
||||
|
||||
---
|
||||
|
||||
# Branches + PRs
|
||||
|
||||
```sh
|
||||
# Neuer Feature-Branch
|
||||
git checkout -b feat/user-profile
|
||||
|
||||
# Arbeit, Commits
|
||||
git add .
|
||||
git commit -m "feat(profile): add avatar upload"
|
||||
|
||||
# Push + PR
|
||||
git push -u origin feat/user-profile
|
||||
# → PR via GitHub/Gitea-UI
|
||||
|
||||
# Nach Merge: aufräumen
|
||||
git checkout main
|
||||
git pull
|
||||
git branch -d feat/user-profile
|
||||
```
|
||||
|
||||
**Faustregel:** kurzlebige Branches (Tage, nicht Monate).
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# README
|
||||
|
||||
---
|
||||
|
||||
# README — das Visitenkarten-Dokument
|
||||
|
||||
Wenn jemand euer Projekt im Browser sieht: **die README ist die erste und oft einzige Berührung.**
|
||||
|
||||
Schlechte README = Repo nicht verwendet.
|
||||
|
||||
**Eine gute README beantwortet:**
|
||||
1. Was tut das Projekt?
|
||||
2. Wie installiere ich es?
|
||||
3. Wie starte ich es?
|
||||
4. Wie teste ich es?
|
||||
5. Wie deploye ich es?
|
||||
6. Lizenz?
|
||||
|
||||
---
|
||||
|
||||
# README-Template
|
||||
|
||||
```markdown
|
||||
# Mein Projekt
|
||||
|
||||
> Eine Zeile Beschreibung.
|
||||
|
||||
## Features
|
||||
|
||||
- ✓ etwas
|
||||
- ✓ etwas anderes
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
git clone https://...
|
||||
cd projekt
|
||||
cp .env.example .env
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
npm test
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
## Stack
|
||||
|
||||
Node 24, Express, SQLite, Vitest.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# SOLID — OO-Prinzipien
|
||||
|
||||
---
|
||||
|
||||
# SOLID — die fünf
|
||||
|
||||
| | Prinzip | Bedeutung |
|
||||
|--|---------|-----------|
|
||||
| **S** | Single Responsibility | jede Klasse/Modul = eine Verantwortung |
|
||||
| **O** | Open / Closed | offen für Erweiterung, geschlossen für Änderung |
|
||||
| **L** | Liskov Substitution | Subtyp ersetzbar durch Basistyp |
|
||||
| **I** | Interface Segregation | kleine, fokussierte Interfaces |
|
||||
| **D** | Dependency Inversion | abhängig von Abstraktionen |
|
||||
|
||||
Robert C. Martin („Uncle Bob"), 2000er.
|
||||
|
||||
---
|
||||
|
||||
# S — Single Responsibility
|
||||
|
||||
```javascript
|
||||
// Schlecht — alles in einer Klasse
|
||||
class UserManager {
|
||||
validate() { /* ... */ }
|
||||
saveToDb() { /* ... */ }
|
||||
sendEmail() { /* ... */ }
|
||||
generateReport() { /* ... */ }
|
||||
}
|
||||
|
||||
// Besser — getrennte Verantwortlichkeiten
|
||||
class UserValidator { /* ... */ }
|
||||
class UserRepository { /* ... */ }
|
||||
class EmailService { /* ... */ }
|
||||
class ReportGenerator { /* ... */ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# D — Dependency Inversion
|
||||
|
||||
```javascript
|
||||
// Schlecht — direkt von konkreter Klasse
|
||||
class OrderService {
|
||||
charge(order) {
|
||||
const stripe = new StripeClient();
|
||||
stripe.charge(order.amount);
|
||||
}
|
||||
}
|
||||
|
||||
// Besser — Abhängigkeit injizieren
|
||||
class OrderService {
|
||||
constructor(paymentProvider) {
|
||||
this.payment = paymentProvider;
|
||||
}
|
||||
charge(order) {
|
||||
this.payment.charge(order.amount);
|
||||
}
|
||||
}
|
||||
|
||||
new OrderService(new StripeClient()); // im Production
|
||||
new OrderService(new MockClient()); // im Test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# 12-Factor App
|
||||
|
||||
---
|
||||
|
||||
# 12-Factor — die wichtigsten
|
||||
|
||||
| # | Faktor | Was |
|
||||
|---|--------|-----|
|
||||
| 1 | Codebase | ein Repo pro App, mehrere Deploys |
|
||||
| 2 | Dependencies | explizit deklariert (package.json) |
|
||||
| 3 | **Config** | über Environment Variables, nicht im Code |
|
||||
| 4 | Backing Services | als Resources behandeln (z.B. DB-URL) |
|
||||
| 6 | **Processes** | stateless |
|
||||
| 9 | Disposability | schnell startbar + stoppbar |
|
||||
| 12 | Admin Processes | als One-Off im selben Stack |
|
||||
|
||||
Heroku, 2011. Heute Industrie-Standard für Cloud-Apps.
|
||||
|
||||
---
|
||||
|
||||
# Factor 3 — Config in env
|
||||
|
||||
```javascript
|
||||
// SCHLECHT — Hardcoded
|
||||
const db = new Pool({ host: 'localhost', user: 'dev' });
|
||||
|
||||
// GUT — aus env
|
||||
const db = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
```
|
||||
|
||||
`.env` lokal, **Environment-Variablen** auf Server. Gleicher Code überall.
|
||||
|
||||
---
|
||||
|
||||
# Factor 6 — Stateless Processes
|
||||
|
||||
**Schlecht:** Session-State im RAM.
|
||||
```javascript
|
||||
const sessions = new Map(); // pro Server-Instanz!
|
||||
```
|
||||
→ Load-Balancer mit 2 Servern: User-Session geht verloren beim 2. Request.
|
||||
|
||||
**Gut:** State in DB / Redis.
|
||||
```javascript
|
||||
await redis.set(`session:${id}`, JSON.stringify(data));
|
||||
```
|
||||
|
||||
Server-Restart → kein Datenverlust. Horizontale Skalierung möglich.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# SemVer — semantisches Versioning
|
||||
|
||||
---
|
||||
|
||||
# SemVer — Format
|
||||
|
||||
```
|
||||
MAJOR.MINOR.PATCH
|
||||
1 . 4 . 3
|
||||
```
|
||||
|
||||
| Teil | Bump bei |
|
||||
|------|----------|
|
||||
| **MAJOR** | breaking change (API-incompatible) |
|
||||
| **MINOR** | neues Feature (rückwärts-kompatibel) |
|
||||
| **PATCH** | Bugfix (rückwärts-kompatibel) |
|
||||
|
||||
**Pre-Release:** `1.0.0-beta.1`, `1.0.0-rc.2`.
|
||||
|
||||
---
|
||||
|
||||
# SemVer in package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"express": "^5.0.0", // ≥ 5.0.0, < 6.0.0
|
||||
"lodash": "~4.17.0", // ≥ 4.17.0, < 4.18.0
|
||||
"react": "5.2.1", // exakt
|
||||
"vite": "*" // jede (vermeiden)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Faustregel:** `^` (Caret) ist Default — major-version-lock.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Code-Reviews
|
||||
|
||||
---
|
||||
|
||||
# Constructive Code-Reviews
|
||||
|
||||
**Geben:**
|
||||
- Konkret: Zeilennummern + Beispiel
|
||||
- Wertschätzend: gut gemachte Stellen erwähnen
|
||||
- Fragend statt fordernd: „Wäre `X` hier robuster?"
|
||||
- Klar zwischen MUST/SHOULD/CONSIDER unterscheiden
|
||||
|
||||
**Nehmen:**
|
||||
- Egoless: Code ≠ Person
|
||||
- Frage zurück bei Unklarheit
|
||||
- Bedanken für detaillierte Feedback
|
||||
|
||||
---
|
||||
|
||||
# Code-Review-Checkliste
|
||||
|
||||
| Bereich | Was prüfen |
|
||||
|---------|------------|
|
||||
| **Correctness** | macht's, was es soll? Edge-cases? |
|
||||
| **Readability** | Namen, Struktur, Kommentare? |
|
||||
| **Tests** | abgedeckt? Sinnvolle Assertions? |
|
||||
| **Security** | Input-Validation, Secrets, Auth |
|
||||
| **Performance** | N+1, Sync-IO, Memory-Leak |
|
||||
| **A11y** (Frontend) | Semantik, Kontrast, Keyboard |
|
||||
| **Doku** | README aktualisiert? CHANGELOG? |
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Doku lesen lernen
|
||||
|
||||
---
|
||||
|
||||
# Wo finde ich was?
|
||||
|
||||
| Ziel | Quelle |
|
||||
|------|--------|
|
||||
| Web-APIs (HTML, CSS, JS) | **MDN** — developer.mozilla.org |
|
||||
| Node-APIs | **nodejs.org/docs** |
|
||||
| npm-Package | `npmjs.com/package/<name>` + GitHub README |
|
||||
| Sprach-Standards (ES) | **tc39.es** |
|
||||
| Browser-Kompatibilität | **caniuse.com** |
|
||||
| Fehler-Suche | Stack Overflow + GitHub Issues |
|
||||
|
||||
**Faustregel:** offizielle Doku zuerst, dann Stack Overflow.
|
||||
|
||||
---
|
||||
|
||||
# Stack Overflow gut nutzen
|
||||
|
||||
**Bevor du fragst:**
|
||||
- Existiert die Frage schon? (Suche oben)
|
||||
- Gibt es ein Minimal-Reproduction-Beispiel?
|
||||
- Welche Versionen (Node, OS, Library)?
|
||||
- Was hast du schon versucht?
|
||||
|
||||
**Bei der Frage:**
|
||||
- Konkreter Titel (nicht "JavaScript Hilfe")
|
||||
- Code-Block für Code
|
||||
- Was war erwartet, was passiert
|
||||
- Error-Message als Text (nicht Screenshot)
|
||||
|
||||
---
|
||||
|
||||
# AI-Assistenten (ChatGPT, Claude, Copilot)
|
||||
|
||||
**Pro:**
|
||||
- Schneller Antworten zu allgemeinen Fragen
|
||||
- Boilerplate-Code in Sekunden
|
||||
- Erklärung fremden Codes
|
||||
|
||||
**Contra:**
|
||||
- Halluziniert APIs, die nicht existieren
|
||||
- Verstärkt Anti-Patterns aus dem Training
|
||||
- Lernkurve geht zurück, wenn nur kopiert wird
|
||||
|
||||
**Faustregel:** als Sparring-Partner, nicht als Wahrheitsquelle. **Verstehen, nicht kopieren.**
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — Projekt-Hygiene-Check
|
||||
|
||||
Geht euer Projekt durch:
|
||||
|
||||
1. **README:** hat alle 6 Sektionen?
|
||||
2. **.gitignore:** komplett, keine Secrets im Repo?
|
||||
3. **Conventional Commits:** letzte 10 Commits durchgehen
|
||||
4. **package.json:** `version`, `description`, `scripts`?
|
||||
5. **Tests:** Coverage > 70 %?
|
||||
6. **Dockerfile + compose.yml:** funktionieren?
|
||||
7. **Environment:** `.env.example` vorhanden, `.env` gitignored?
|
||||
|
||||
**Bonus:** GitHub-Actions-Workflow für Tests + Build.
|
||||
|
||||
---
|
||||
|
||||
# Abschluss
|
||||
|
||||
**Über 8 Termine gelernt:**
|
||||
1. Web Engineering — Foundation
|
||||
2. CSS Extended — Layout + Animation
|
||||
3. Node.js Basics — Runtime + REST
|
||||
4. Node.js Advanced — async + DB + Auth
|
||||
5. Testing — Unit + Integration + E2E
|
||||
6. TypeScript — Typen + Schemas
|
||||
7. Docker — Container + compose
|
||||
8. Best Practices — Git + README + SOLID + 12-Factor
|
||||
|
||||
**Nächste Schritte:** eigenes Projekt für die Prüfungsleistung. Code-Abgabe + Präsentation.
|
||||
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Browser–Server HTTP Sequenz</title>
|
||||
<style>
|
||||
:root {
|
||||
--dark: #1a1a2e;
|
||||
--muted: #6b7280;
|
||||
--border: #cbd5e1;
|
||||
--bg-soft: #f8fafc;
|
||||
--hl: #005f8a;
|
||||
--req: #2563eb;
|
||||
--res: #16a34a;
|
||||
}
|
||||
html, body { margin: 0; padding: 0; background: #fff; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; color: var(--dark); }
|
||||
body { padding: 24px 32px; min-height: 100vh; display: flex; flex-direction: column; justify-content: center; }
|
||||
h1 { margin: 0 0 4px; font-size: 1.45rem; text-align: center; }
|
||||
.sub { color: var(--muted); margin-bottom: 24px; font-size: 0.88rem; text-align: center; }
|
||||
|
||||
.diagram { display: grid; grid-template-columns: 140px 1fr 140px; gap: 0; align-items: start; }
|
||||
|
||||
/* Actors */
|
||||
.actor {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||
font-weight: 700; font-size: 0.9rem;
|
||||
}
|
||||
.actor-box {
|
||||
border: 2px solid var(--dark); border-radius: 8px;
|
||||
padding: 8px 18px; background: var(--bg-soft);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lifeline {
|
||||
width: 2px; background: var(--border);
|
||||
height: 100%; min-height: 280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages { display: flex; flex-direction: column; gap: 0; padding-top: 48px; }
|
||||
.msg { display: flex; flex-direction: column; margin-bottom: 6px; }
|
||||
.msg-arrow {
|
||||
display: flex; align-items: center; gap: 0; position: relative;
|
||||
}
|
||||
.msg-line {
|
||||
flex: 1; height: 2px;
|
||||
}
|
||||
.msg-line.req { background: var(--req); }
|
||||
.msg-line.res { background: var(--res); background: repeating-linear-gradient(90deg, var(--res) 0 8px, transparent 8px 13px); }
|
||||
|
||||
.arrowhead-r { width: 0; height: 0; border-top: 6px solid transparent; border-bottom: 6px solid transparent; border-left: 10px solid var(--req); }
|
||||
.arrowhead-l { width: 0; height: 0; border-top: 6px solid transparent; border-bottom: 6px solid transparent; border-right: 10px solid var(--res); }
|
||||
|
||||
.msg-label {
|
||||
font-size: 0.75rem; font-family: ui-monospace, monospace;
|
||||
padding: 2px 0 6px; text-align: center;
|
||||
}
|
||||
.msg-label.req { color: var(--req); }
|
||||
.msg-label.res { color: var(--res); }
|
||||
|
||||
.gap { height: 10px; }
|
||||
|
||||
.legend {
|
||||
margin-top: 20px; display: flex; gap: 24px; justify-content: center;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.leg { display: flex; align-items: center; gap: 8px; }
|
||||
.leg-line { width: 28px; height: 2px; }
|
||||
.leg-line.req { background: var(--req); }
|
||||
.leg-line.res { background: repeating-linear-gradient(90deg, var(--res) 0 6px, transparent 6px 10px); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Browser–Server: HTTP Kommunikation</h1>
|
||||
<p class="sub">Jede Ressource = eigener Request — Browser blockiert nicht, lädt parallel</p>
|
||||
|
||||
<div class="diagram">
|
||||
|
||||
<!-- Browser actor -->
|
||||
<div class="actor">
|
||||
<div class="actor-box">🌐 Browser</div>
|
||||
<div class="lifeline"></div>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div class="messages">
|
||||
|
||||
<div class="msg">
|
||||
<div class="msg-arrow"><div class="msg-line req"></div><div class="arrowhead-r"></div></div>
|
||||
<div class="msg-label req">GET /products</div>
|
||||
</div>
|
||||
<div class="msg">
|
||||
<div class="msg-arrow"><div class="arrowhead-l"></div><div class="msg-line res"></div></div>
|
||||
<div class="msg-label res">HTTP 200 + index.html</div>
|
||||
</div>
|
||||
|
||||
<div class="gap"></div>
|
||||
|
||||
<div class="msg">
|
||||
<div class="msg-arrow"><div class="msg-line req"></div><div class="arrowhead-r"></div></div>
|
||||
<div class="msg-label req">GET /style.css</div>
|
||||
</div>
|
||||
<div class="msg">
|
||||
<div class="msg-arrow"><div class="arrowhead-l"></div><div class="msg-line res"></div></div>
|
||||
<div class="msg-label res">HTTP 200 + style.css</div>
|
||||
</div>
|
||||
|
||||
<div class="gap"></div>
|
||||
|
||||
<div class="msg">
|
||||
<div class="msg-arrow"><div class="msg-line req"></div><div class="arrowhead-r"></div></div>
|
||||
<div class="msg-label req">GET /script.js</div>
|
||||
</div>
|
||||
<div class="msg">
|
||||
<div class="msg-arrow"><div class="arrowhead-l"></div><div class="msg-line res"></div></div>
|
||||
<div class="msg-label res">HTTP 200 + script.js</div>
|
||||
</div>
|
||||
|
||||
<div class="gap"></div>
|
||||
|
||||
<div class="msg">
|
||||
<div class="msg-arrow"><div class="msg-line req"></div><div class="arrowhead-r"></div></div>
|
||||
<div class="msg-label req">GET /api/products</div>
|
||||
</div>
|
||||
<div class="msg">
|
||||
<div class="msg-arrow"><div class="arrowhead-l"></div><div class="msg-line res"></div></div>
|
||||
<div class="msg-label res">HTTP 200 + JSON</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Server actor -->
|
||||
<div class="actor">
|
||||
<div class="actor-box">🖥️ Server</div>
|
||||
<div class="lifeline"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="leg"><div class="leg-line req"></div><span style="color:var(--req)">Request (Browser → Server)</span></div>
|
||||
<div class="leg"><div class="leg-line res"></div><span style="color:var(--res)">Response (Server → Browser)</span></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,167 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Docker: Service Orchestration</title>
|
||||
<style>
|
||||
:root {
|
||||
--dark: #1a1a2e;
|
||||
--muted: #6b7280;
|
||||
--border: #cbd5e1;
|
||||
--bg-soft: #f8fafc;
|
||||
--blue: #2563eb;
|
||||
--red: #dc2626;
|
||||
}
|
||||
html, body { margin: 0; padding: 0; background: #fff; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; color: var(--dark); }
|
||||
body { padding: 28px 36px; min-height: 100vh; display: flex; flex-direction: column; justify-content: center; }
|
||||
h1 { margin: 0 0 4px; font-size: 1.45rem; }
|
||||
.sub { color: var(--muted); margin-bottom: 28px; font-size: 0.88rem; }
|
||||
|
||||
.diagram { display: flex; align-items: center; gap: 0; }
|
||||
|
||||
.node {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
border: 2px solid var(--dark); border-radius: 8px;
|
||||
padding: 12px 16px; background: var(--bg-soft);
|
||||
font-weight: 700; font-size: 0.9rem; text-align: center; min-width: 90px;
|
||||
}
|
||||
.node .sub-label { font-size: 0.72rem; font-weight: 400; color: var(--muted); margin-top: 3px; }
|
||||
|
||||
.client { border-radius: 50%; width: 72px; height: 72px; padding: 0; border-color: var(--red); }
|
||||
.proxy { background: #eff6ff; border-color: var(--blue); min-width: 100px; }
|
||||
.docker-box {
|
||||
border: 2px dashed var(--blue); border-radius: 12px;
|
||||
padding: 16px 20px; display: flex; align-items: center; gap: 0;
|
||||
background: #f0f7ff;
|
||||
position: relative;
|
||||
}
|
||||
.docker-label {
|
||||
position: absolute; top: -11px; left: 14px;
|
||||
background: #fff; padding: 0 6px;
|
||||
font-size: 0.72rem; font-weight: 700; color: var(--blue);
|
||||
text-transform: uppercase; letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.services { display: flex; flex-direction: column; gap: 12px; }
|
||||
.db-col { display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
||||
|
||||
/* arrows */
|
||||
.arr { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0 6px; }
|
||||
.arr-h { display: flex; flex-direction: column; gap: 3px; align-items: center; }
|
||||
.line { height: 2px; width: 44px; }
|
||||
.line.blue { background: var(--blue); }
|
||||
.line.red { background: var(--red); }
|
||||
.line.dashed-blue { background: repeating-linear-gradient(90deg, var(--blue) 0 6px, transparent 6px 10px); }
|
||||
.line.dashed-red { background: repeating-linear-gradient(90deg, var(--red) 0 6px, transparent 6px 10px); }
|
||||
.arrowhead-r::after { content: '▶'; font-size: 0.65rem; color: inherit; }
|
||||
.arrowhead-l::before { content: '◀'; font-size: 0.65rem; color: inherit; }
|
||||
.arr-label { font-size: 0.65rem; color: var(--muted); white-space: nowrap; }
|
||||
|
||||
.arr-v { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 2px 0; }
|
||||
.line-v { width: 2px; height: 28px; }
|
||||
.line-v.blue { background: var(--blue); }
|
||||
.line-v.dashed-blue { background: repeating-linear-gradient(180deg, var(--blue) 0 5px, transparent 5px 9px); }
|
||||
.arr-v .arrowhead-d::after { content: '▼'; font-size: 0.65rem; color: var(--blue); }
|
||||
|
||||
.gap-col { display: flex; flex-direction: column; gap: 12px; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Docker: Service Orchestration</h1>
|
||||
<p class="sub">Client kommuniziert nur mit dem Reverse Proxy — interne Services bleiben isoliert</p>
|
||||
|
||||
<div class="diagram">
|
||||
|
||||
<!-- Client -->
|
||||
<div class="node client" style="color:var(--red); border-color:var(--red);">
|
||||
Client
|
||||
<div class="sub-label">Browser /<br>Web App</div>
|
||||
</div>
|
||||
|
||||
<!-- arrows client → proxy -->
|
||||
<div class="arr">
|
||||
<div class="arr-h" style="color:var(--red)">
|
||||
<div style="display:flex;align-items:center;gap:2px">
|
||||
<div class="line red" style="width:36px"></div>
|
||||
<span class="arrowhead-r" style="color:var(--red)"></span>
|
||||
</div>
|
||||
<div class="arr-label">localhost:10007</div>
|
||||
</div>
|
||||
<div style="height:8px"></div>
|
||||
<div class="arr-h" style="color:var(--red)">
|
||||
<div style="display:flex;align-items:center;gap:2px">
|
||||
<span class="arrowhead-l" style="color:var(--red)"></span>
|
||||
<div class="line dashed-red" style="width:36px"></div>
|
||||
</div>
|
||||
<div class="arr-label">response</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docker compose boundary -->
|
||||
<div class="docker-box">
|
||||
<div class="docker-label">docker compose</div>
|
||||
|
||||
<!-- Reverse Proxy -->
|
||||
<div class="node proxy">
|
||||
Reverse<br>Proxy
|
||||
<div class="sub-label">:10007</div>
|
||||
</div>
|
||||
|
||||
<!-- arrows proxy → services -->
|
||||
<div class="gap-col" style="padding: 0 8px;">
|
||||
<div class="arr-h" style="color:var(--blue)">
|
||||
<div style="display:flex;align-items:center;gap:2px">
|
||||
<div class="line blue" style="width:28px"></div>
|
||||
<span class="arrowhead-r" style="color:var(--blue)"></span>
|
||||
</div>
|
||||
<div class="arr-label">/api/*</div>
|
||||
</div>
|
||||
<div style="height:8px"></div>
|
||||
<div class="arr-h" style="color:var(--blue)">
|
||||
<div style="display:flex;align-items:center;gap:2px">
|
||||
<div class="line blue" style="width:28px"></div>
|
||||
<span class="arrowhead-r" style="color:var(--blue)"></span>
|
||||
</div>
|
||||
<div class="arr-label">/*</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services column -->
|
||||
<div class="services">
|
||||
<div class="node" style="background:#dbeafe; border-color:var(--blue);">
|
||||
Express API
|
||||
<div class="sub-label">:3000</div>
|
||||
</div>
|
||||
<div class="node" style="background:#dcfce7; border-color:#16a34a;">
|
||||
Web App
|
||||
<div class="sub-label">:5173</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- arrows → DB -->
|
||||
<div class="gap-col" style="padding: 0 6px;">
|
||||
<div class="arr-h" style="color:var(--blue)">
|
||||
<div style="display:flex;align-items:center;gap:2px">
|
||||
<div class="line dashed-blue" style="width:24px"></div>
|
||||
<span class="arrowhead-r" style="color:var(--blue)"></span>
|
||||
</div>
|
||||
<div class="arr-label">:5432</div>
|
||||
</div>
|
||||
<div style="height:8px"></div>
|
||||
<!-- web app does not connect to DB -->
|
||||
<div style="width:48px"></div>
|
||||
</div>
|
||||
|
||||
<!-- DB -->
|
||||
<div class="db-col">
|
||||
<div class="node" style="background:#fef9c3; border-color:#ca8a04; min-width:70px;">
|
||||
DB
|
||||
<div class="sub-label">PostgreSQL</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /docker-box -->
|
||||
</div><!-- /diagram -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>JavaScript Frameworks – Übersicht</title>
|
||||
<style>
|
||||
:root {
|
||||
--dark: #1a1a2e;
|
||||
--muted: #6b7280;
|
||||
}
|
||||
html, body { margin: 0; padding: 0; background: #fff; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; color: var(--dark); }
|
||||
body { padding: 24px 28px; min-height: 100vh; display: flex; flex-direction: column; justify-content: center; }
|
||||
h1 { margin: 0 0 4px; font-size: 1.45rem; text-align: center; }
|
||||
.sub { color: var(--muted); margin-bottom: 24px; font-size: 0.88rem; text-align: center; }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
|
||||
|
||||
.col {
|
||||
border-radius: 12px; padding: 18px 12px 16px;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
.col.active { outline: 2px solid var(--dark); outline-offset: 2px; }
|
||||
|
||||
.col-title {
|
||||
font-size: 0.78rem; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 0.06em; color: var(--dark); text-align: center;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.12); padding-bottom: 8px; margin-bottom: 2px;
|
||||
}
|
||||
.items { display: flex; flex-direction: column; gap: 6px; }
|
||||
.item {
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
font-size: 0.82rem; font-weight: 600;
|
||||
}
|
||||
.item .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.item .note { font-size: 0.7rem; font-weight: 400; color: var(--muted); display: block; }
|
||||
|
||||
.css-col { background: #f1f5f9; }
|
||||
.fe-col { background: #fce7f3; }
|
||||
.render-col { background: #ede9fe; }
|
||||
.build-col { background: #dbeafe; }
|
||||
.be-col { background: #d1fae5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>JavaScript Frameworks – Übersicht</h1>
|
||||
<p class="sub">Kategorien des modernen JS-Ökosystems</p>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<div class="col css-col">
|
||||
<div class="col-title">CSS Frameworks</div>
|
||||
<div class="items">
|
||||
<div class="item"><div class="dot" style="background:#38bdf8"></div><span>Tailwind CSS<span class="note">Utility-first</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#7c3aed"></div><span>Bootstrap<span class="note">Komponenten</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#e879f9"></div><span>Pico CSS<span class="note">Classless</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#f97316"></div><span>UnoCSS<span class="note">Atomic</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col fe-col">
|
||||
<div class="col-title">Frontend Frameworks</div>
|
||||
<div class="items">
|
||||
<div class="item"><div class="dot" style="background:#61dafb"></div><span>React<span class="note">Meta / Virtual DOM</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#ff3e00"></div><span>Svelte<span class="note">Compiler-based</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#42b883"></div><span>Vue<span class="note">Progressive</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#dd0031"></div><span>Angular<span class="note">Google / Full</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col render-col">
|
||||
<div class="col-title">Rendering Frameworks</div>
|
||||
<div class="items">
|
||||
<div class="item"><div class="dot" style="background:#000"></div><span>Next.js<span class="note">React / SSR</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#ff3e00"></div><span>SvelteKit<span class="note">Svelte / SSR</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#42b883"></div><span>Nuxt<span class="note">Vue / SSR</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#ff5d01"></div><span>Astro<span class="note">Islands Arch.</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#0ea5e9"></div><span>Gatsby<span class="note">React / SSG</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col build-col">
|
||||
<div class="col-title">Build Tools & Bundler</div>
|
||||
<div class="items">
|
||||
<div class="item"><div class="dot" style="background:#646cff"></div><span>Vite<span class="note">Dev-Server / HMR</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#2b3a42"></div><span>Webpack<span class="note">Bundler</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#f9b640"></div><span>esbuild<span class="note">Go / ultra-fast</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#3178c6"></div><span>tsc<span class="note">TypeScript</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#cc3534"></div><span>Rollup<span class="note">Libraries</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col be-col active">
|
||||
<div class="col-title">Backend Frameworks</div>
|
||||
<div class="items">
|
||||
<div class="item"><div class="dot" style="background:#68a063"></div><span>Express<span class="note">Minimal / de-facto</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#3178c6"></div><span>Fastify<span class="note">Performant</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#5a45ff"></div><span>AdonisJS<span class="note">Full-stack</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#e11d48"></div><span>NestJS<span class="note">Angular-style</span></span></div>
|
||||
<div class="item"><div class="dot" style="background:#000"></div><span>Hono<span class="note">Edge-ready</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Node.js – Einsatzgebiete</title>
|
||||
<style>
|
||||
:root {
|
||||
--dark: #1a1a2e;
|
||||
--muted: #6b7280;
|
||||
}
|
||||
html, body { margin: 0; padding: 0; background: #fff; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; color: var(--dark); }
|
||||
body { padding: 28px 32px; min-height: 100vh; display: flex; flex-direction: column; justify-content: center; }
|
||||
h1 { margin: 0 0 4px; font-size: 1.45rem; text-align: center; }
|
||||
.sub { color: var(--muted); margin-bottom: 28px; font-size: 0.88rem; text-align: center; }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
|
||||
|
||||
.card {
|
||||
border-radius: 12px; padding: 28px 16px 24px;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
.card .icon { font-size: 2rem; line-height: 1; }
|
||||
.card .title { font-size: 1.1rem; font-weight: 700; }
|
||||
.card .desc { font-size: 0.8rem; color: var(--muted); line-height: 1.5; }
|
||||
.card .tags { display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; margin-top: 4px; }
|
||||
.tag { font-size: 0.72rem; background: rgba(0,0,0,0.07); border-radius: 99px; padding: 2px 9px; font-family: ui-monospace, monospace; }
|
||||
|
||||
.cli { background: #fce7f3; }
|
||||
.api { background: #ede9fe; }
|
||||
.web { background: #dbeafe; }
|
||||
.ws { background: #d1fae5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Node.js – Einsatzgebiete</h1>
|
||||
<p class="sub">Vier Hauptkategorien serverseitiger JavaScript-Anwendungen</p>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card cli">
|
||||
<div class="icon">⌨️</div>
|
||||
<div class="title">CLI</div>
|
||||
<div class="desc">Kommandozeilen-Tools, Skripte, Build-Prozesse, Automatisierung</div>
|
||||
<div class="tags">
|
||||
<span class="tag">npm scripts</span>
|
||||
<span class="tag">process.argv</span>
|
||||
<span class="tag">fs</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card api">
|
||||
<div class="icon">🔌</div>
|
||||
<div class="title">API Server</div>
|
||||
<div class="desc">REST- und GraphQL-APIs, JSON-Endpunkte, CRUD-Operationen</div>
|
||||
<div class="tags">
|
||||
<span class="tag">express</span>
|
||||
<span class="tag">GET/POST</span>
|
||||
<span class="tag">JSON</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card web">
|
||||
<div class="icon">🌐</div>
|
||||
<div class="title">Web Server</div>
|
||||
<div class="desc">Statische Dateien ausliefern, Server-Side Rendering, Reverse Proxy</div>
|
||||
<div class="tags">
|
||||
<span class="tag">http</span>
|
||||
<span class="tag">SSR</span>
|
||||
<span class="tag">next.js</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card ws">
|
||||
<div class="icon">⚡</div>
|
||||
<div class="title">Web Sockets</div>
|
||||
<div class="desc">Echtzeit-Kommunikation, Chat, Live-Updates, bidirektional</div>
|
||||
<div class="tags">
|
||||
<span class="tag">ws</span>
|
||||
<span class="tag">socket.io</span>
|
||||
<span class="tag">events</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test Automation Trophy</title>
|
||||
<style>
|
||||
:root {
|
||||
--dark: #1a1a2e;
|
||||
--muted: #6b7280;
|
||||
--border: #cbd5e1;
|
||||
--bg-soft: #f8fafc;
|
||||
}
|
||||
html, body { margin: 0; padding: 0; background: #fff; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; color: var(--dark); }
|
||||
body { padding: 28px 32px; display: flex; flex-direction: column; align-items: center; }
|
||||
h1 { margin: 0 0 4px; font-size: 1.55rem; text-align: center; }
|
||||
.sub { color: var(--muted); margin-bottom: 28px; font-size: 0.92rem; text-align: center; }
|
||||
|
||||
.trophy { display: flex; flex-direction: column; align-items: center; gap: 0; width: 480px; }
|
||||
|
||||
.layer {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; font-size: 1.05rem; color: var(--dark);
|
||||
border-radius: 6px; transition: opacity 0.2s;
|
||||
}
|
||||
.layer .count {
|
||||
font-size: 0.78rem; font-weight: 400; color: var(--muted);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.e2e { background: #fca5a5; width: 40%; height: 54px; clip-path: polygon(10% 100%, 90% 100%, 100% 0%, 0% 0%); }
|
||||
.integration{ background: #60a5fa; width: 80%; height: 86px; }
|
||||
.unit { background: #c4b5fd; width: 40%; height: 54px; clip-path: polygon(0% 100%, 100% 100%, 85% 0%, 15% 0%); }
|
||||
.static { background: #fde68a; width: 28%; height: 60px; clip-path: polygon(0% 0%, 100% 0%, 80% 100%, 20% 100%); }
|
||||
|
||||
.legend {
|
||||
margin-top: 28px; display: grid; grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 24px; font-size: 0.88rem; width: 480px;
|
||||
}
|
||||
.leg { display: flex; align-items: center; gap: 10px; }
|
||||
.swatch { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; }
|
||||
.leg strong { display: block; }
|
||||
.leg span { color: var(--muted); font-size: 0.82rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Automation Trophy</h1>
|
||||
<p class="sub">Schicht-Modell nach Kent C. Dodds — Breite = empfohlener Anteil</p>
|
||||
|
||||
<div class="trophy">
|
||||
<div class="layer e2e">E2E</div>
|
||||
<div class="layer integration">Integration</div>
|
||||
<div class="layer unit">Unit</div>
|
||||
<div class="layer static">Static</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="leg"><div class="swatch" style="background:#fde68a"></div><div><strong>Static</strong><span>Linting, Type-Check, Format</span></div></div>
|
||||
<div class="leg"><div class="swatch" style="background:#c4b5fd"></div><div><strong>Unit</strong><span>Einzelne Funktionen / Komponenten</span></div></div>
|
||||
<div class="leg"><div class="swatch" style="background:#60a5fa"></div><div><strong>Integration</strong><span>Zusammenspiel mehrerer Teile ← Fokus</span></div></div>
|
||||
<div class="leg"><div class="swatch" style="background:#fca5a5"></div><div><strong>End to End</strong><span>Ganzer User-Flow im Browser</span></div></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 85 KiB |
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>TypeScript → JavaScript Compilation</title>
|
||||
<style>
|
||||
:root {
|
||||
--dark: #1a1a2e;
|
||||
--muted: #6b7280;
|
||||
--border: #cbd5e1;
|
||||
--bg-soft: #f8fafc;
|
||||
--ts: #3178c6;
|
||||
--js: #f7df1e;
|
||||
--remove: #dc2626;
|
||||
}
|
||||
html, body { margin: 0; padding: 0; background: #fff; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; color: var(--dark); }
|
||||
body { padding: 24px 28px; min-height: 100vh; display: flex; flex-direction: column; justify-content: center; }
|
||||
h1 { margin: 0 0 4px; font-size: 1.45rem; text-align: center; }
|
||||
.sub { color: var(--muted); margin-bottom: 24px; font-size: 0.88rem; text-align: center; }
|
||||
|
||||
.panels { display: grid; grid-template-columns: 1fr 48px 1fr 48px 1fr; align-items: start; gap: 0; }
|
||||
|
||||
.panel { border: 2px solid var(--border); border-radius: 10px; overflow: hidden; }
|
||||
.panel-head {
|
||||
padding: 8px 14px; font-size: 0.78rem; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.07em;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.panel-body { padding: 14px; min-height: 100vh; display: flex; flex-direction: column; justify-content: center; }
|
||||
|
||||
.ts .panel-head { background: var(--ts); color: #fff; }
|
||||
.strip .panel-head { background: #fee2e2; color: var(--remove); }
|
||||
.js .panel-head { background: var(--js); color: var(--dark); }
|
||||
|
||||
pre {
|
||||
margin: 0; font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
font-size: 0.78rem; line-height: 1.65; white-space: pre;
|
||||
}
|
||||
.removed { color: var(--remove); text-decoration: line-through; background: #fee2e2; border-radius: 2px; }
|
||||
.kept { color: #166534; }
|
||||
.kw { color: var(--ts); }
|
||||
.kw-js { color: #ca8a04; }
|
||||
.type-ann{ color: #7c3aed; }
|
||||
.str { color: #16a34a; }
|
||||
.fn { color: #0369a1; }
|
||||
|
||||
.arrow-col {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; padding-top: 52px; gap: 4px;
|
||||
}
|
||||
.arrow-shaft { width: 2px; height: 32px; background: var(--muted); }
|
||||
.arrow-head { width: 0; height: 0; border-left: 7px solid transparent; border-right: 7px solid transparent; border-top: 10px solid var(--muted); }
|
||||
.arrow-label { font-size: 0.68rem; color: var(--muted); writing-mode: vertical-rl; text-orientation: mixed; letter-spacing: 0.05em; margin-top: 4px; white-space: nowrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>TypeScript → JavaScript Compilation</h1>
|
||||
<p class="sub">Der TypeScript-Compiler entfernt alle Typ-Annotationen — nur JavaScript bleibt</p>
|
||||
|
||||
<div class="panels">
|
||||
|
||||
<!-- TypeScript -->
|
||||
<div class="panel ts">
|
||||
<div class="panel-head">📘 TypeScript-Datei</div>
|
||||
<div class="panel-body">
|
||||
<pre><span class="kw">type</span> <span class="type-ann">Result</span> = <span class="str">"pass"</span> | <span class="str">"fail"</span>;
|
||||
|
||||
<span class="kw">function</span> <span class="fn">verify</span>(result: <span class="type-ann">Result</span>) {
|
||||
<span class="kw">if</span> (result === <span class="str">"pass"</span>) {
|
||||
console.<span class="fn">log</span>(<span class="str">"Passed"</span>);
|
||||
} <span class="kw">else</span> {
|
||||
console.<span class="fn">log</span>(<span class="str">"Failed"</span>);
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow 1 -->
|
||||
<div class="arrow-col">
|
||||
<div class="arrow-shaft"></div>
|
||||
<div class="arrow-head"></div>
|
||||
<div class="arrow-label">tsc</div>
|
||||
</div>
|
||||
|
||||
<!-- Strip phase -->
|
||||
<div class="panel strip">
|
||||
<div class="panel-head">✂️ Typen werden entfernt</div>
|
||||
<div class="panel-body">
|
||||
<pre><span class="removed">type Result = "pass" | "fail";</span>
|
||||
|
||||
<span class="kw">function</span> <span class="fn">verify</span>(result<span class="removed">: Result</span>) {
|
||||
<span class="kw">if</span> (result === <span class="str">"pass"</span>) {
|
||||
console.<span class="fn">log</span>(<span class="str">"Passed"</span>);
|
||||
} <span class="kw">else</span> {
|
||||
console.<span class="fn">log</span>(<span class="str">"Failed"</span>);
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow 2 -->
|
||||
<div class="arrow-col">
|
||||
<div class="arrow-shaft"></div>
|
||||
<div class="arrow-head"></div>
|
||||
<div class="arrow-label">output</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<div class="panel js">
|
||||
<div class="panel-head">📄 JavaScript-Datei</div>
|
||||
<div class="panel-body">
|
||||
<pre><span class="kw-js">function</span> <span class="fn">verify</span>(result) {
|
||||
<span class="kw-js">if</span> (result === <span class="str">"pass"</span>) {
|
||||
console.<span class="fn">log</span>(<span class="str">"Passed"</span>);
|
||||
} <span class="kw-js">else</span> {
|
||||
console.<span class="fn">log</span>(<span class="str">"Failed"</span>);
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 125 KiB |
@@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Web Ecosystem – Schichten</title>
|
||||
<style>
|
||||
:root {
|
||||
--dark: #1a1a2e;
|
||||
--muted: #6b7280;
|
||||
}
|
||||
html, body { margin: 0; padding: 0; background: #fff; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; color: var(--dark); }
|
||||
body { padding: 20px 28px; display: flex; flex-direction: column; align-items: center; }
|
||||
h1 { margin: 0 0 4px; font-size: 1.45rem; text-align: center; }
|
||||
.sub { color: var(--muted); margin-bottom: 20px; font-size: 0.88rem; text-align: center; }
|
||||
|
||||
.rings-wrap { position: relative; width: 520px; height: 520px; }
|
||||
|
||||
.ring {
|
||||
position: absolute; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* layers from outside in */
|
||||
.r-css-libs { width: 520px; height: 520px; background: #f0fdf4; border: 2px solid #86efac; }
|
||||
.r-frameworks{ width: 400px; height: 400px; background: #eff6ff; border: 2px solid #93c5fd; }
|
||||
.r-bundlers { width: 290px; height: 290px; background: #fdf4ff; border: 2px solid #d8b4fe; }
|
||||
.r-postcss { width: 196px; height: 196px; background: #fff7ed; border: 2px solid #fdba74; }
|
||||
.r-core { width: 116px; height: 116px; background: var(--dark); border: none; color: #fff; flex-direction: column; gap: 2px; text-align: center; }
|
||||
.r-core span { font-size: 0.72rem; font-weight: 700; line-height: 1.3; }
|
||||
|
||||
/* floating labels */
|
||||
.label {
|
||||
position: absolute; font-size: 0.72rem; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: 0.06em;
|
||||
background: rgba(255,255,255,0.85); padding: 2px 7px;
|
||||
border-radius: 99px; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ring label positions — top of each ring */
|
||||
.lbl-css-libs { top: 12px; left: 50%; transform: translateX(-50%); color: #16a34a; }
|
||||
.lbl-frameworks { top: 72px; left: 50%; transform: translateX(-50%); color: #2563eb; }
|
||||
.lbl-bundlers { top: 128px; left: 50%; transform: translateX(-50%); color: #7c3aed; }
|
||||
.lbl-postcss { top: 178px; left: 50%; transform: translateX(-50%); color: #ea580c; }
|
||||
|
||||
/* item pills scattered in each ring band */
|
||||
.pill {
|
||||
position: absolute; font-size: 0.7rem; font-weight: 600;
|
||||
padding: 3px 9px; border-radius: 99px; white-space: nowrap;
|
||||
background: rgba(255,255,255,0.9); border: 1px solid rgba(0,0,0,0.10);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Web Ecosystem – Schichten</h1>
|
||||
<p class="sub">Von innen nach außen: Kern → Standards → Werkzeuge → Frameworks → CSS-Utilities</p>
|
||||
|
||||
<div class="rings-wrap">
|
||||
|
||||
<!-- Rings (painted back-to-front) -->
|
||||
<div class="ring r-css-libs"></div>
|
||||
<div class="ring r-frameworks"></div>
|
||||
<div class="ring r-bundlers"></div>
|
||||
<div class="ring r-postcss"></div>
|
||||
<div class="ring r-core">
|
||||
<span>JS</span>
|
||||
<span>HTML</span>
|
||||
<span>CSS</span>
|
||||
</div>
|
||||
|
||||
<!-- Ring labels -->
|
||||
<div class="label lbl-css-libs">CSS Utilities & Libraries</div>
|
||||
<div class="label lbl-frameworks">Frontend / Backend Frameworks</div>
|
||||
<div class="label lbl-bundlers">Bundler & Build Tools</div>
|
||||
<div class="label lbl-postcss">PostCSS / Preprocessors</div>
|
||||
|
||||
<!-- CSS Libs band (outer ring) -->
|
||||
<div class="pill" style="top:52px; left:30px; color:#16a34a;">Tailwind</div>
|
||||
<div class="pill" style="top:60px; right:24px; color:#16a34a;">Bootstrap</div>
|
||||
<div class="pill" style="bottom:52px;left:38px; color:#16a34a;">shadcn/ui</div>
|
||||
<div class="pill" style="bottom:56px;right:28px;color:#16a34a;">Pico CSS</div>
|
||||
<div class="pill" style="top:50%; left:4px; transform:translateY(-50%); color:#16a34a;">UnoCSS</div>
|
||||
<div class="pill" style="top:50%; right:4px; transform:translateY(-50%); color:#16a34a;">DaisyUI</div>
|
||||
|
||||
<!-- Frameworks band -->
|
||||
<div class="pill" style="top:88px; left:86px; color:#2563eb;">React</div>
|
||||
<div class="pill" style="top:88px; right:72px; color:#2563eb;">Vue</div>
|
||||
<div class="pill" style="bottom:88px;left:72px; color:#2563eb;">Svelte</div>
|
||||
<div class="pill" style="bottom:88px;right:60px;color:#2563eb;">Angular</div>
|
||||
<div class="pill" style="top:42%; left:60px; color:#2563eb;">Next.js</div>
|
||||
<div class="pill" style="top:56%; left:60px; color:#2563eb;">Express</div>
|
||||
<div class="pill" style="top:46%; right:54px; color:#2563eb;">Nuxt</div>
|
||||
<div class="pill" style="top:58%; right:54px; color:#2563eb;">Fastify</div>
|
||||
|
||||
<!-- Bundlers band -->
|
||||
<div class="pill" style="top:148px; left:145px; color:#7c3aed;">Vite</div>
|
||||
<div class="pill" style="top:148px; right:132px;color:#7c3aed;">Webpack</div>
|
||||
<div class="pill" style="bottom:145px;left:134px;color:#7c3aed;">esbuild</div>
|
||||
<div class="pill" style="bottom:145px;right:120px;color:#7c3aed;">Rollup</div>
|
||||
|
||||
<!-- PostCSS band -->
|
||||
<div class="pill" style="top:196px; left:50%; transform:translateX(-50%); color:#ea580c;">PostCSS</div>
|
||||
<div class="pill" style="bottom:196px;left:50%;transform:translateX(-50%); color:#ea580c;">Sass / SCSS</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 474 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 167 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="svg4232" width="541.333" height="226.114"><g id="layer1" transform="translate(-129.334 -393.59)"><path id="path84" d="M271.089 441.054v36.897c0 1.836-1.58 4.865-3.416 6.696l-37.58 37.58c-.92.918-1.86 1.367-2.46 1.367-.602 0-.957-.449-.957-1.367v4.1c0 1.836 1.58 3.416 3.416 3.416h81.857c1.837 0 3.416-1.58 3.416-3.416v-81.994c0-1.836-1.58-3.28-3.416-3.28Zm-52.066 85.273c0 1.836-1.585 3.416-3.416 3.416h-40.86v44.55c0 .918.36 1.367.956 1.367s1.405-.449 2.323-1.367l37.58-37.58c1.832-1.837 3.417-4.86 3.417-6.696Z" style="fill:#1a1a2e;fill-opacity:.5;fill-rule:nonzero;stroke:none"/><path id="path88" d="M270.132 394.59c-.604 0-1.541.449-2.46 1.367l-37.58 37.717c-1.831 1.831-3.416 4.723-3.416 6.56v81.993c0 1.837 1.585 1.837 3.416.00014999999996234692l37.58-37.58c1.837-1.837 3.417-4.86 3.417-6.696v-81.994c0-.918-.353-1.366-.957-1.366M133.75 441.055c-1.837 0-3.416 1.443-3.416 3.28v81.993c0 1.836 1.58 3.416 3.416 3.416h81.857c1.836 0 3.416-1.58 3.416-3.416v-81.994c0-1.836-1.58-3.28-3.416-3.28Z" style="fill:#1a1a2e;fill-opacity:.5;fill-rule:nonzero;stroke:none"/><path id="path100" d="M230.092 441.054c-1.836 0-3.416 1.58-3.416 3.416v77.757c0 .918.355 1.367.956 1.367.602 0 1.542-.449 2.46-1.367l37.58-37.58c1.837-1.837 3.417-4.865 3.417-6.696v-36.897Zm-12.026 5.603c-.606 0-1.541.453-2.46 1.366l-37.58 37.717c-1.83 1.87-3.28 4.728-3.28 6.56v37.443h40.86c1.837 0 3.417-1.58 3.417-3.416v-78.304c0-.913-.355-1.366-.957-1.366" style="fill:#1a1a2e;fill-opacity:.5;fill-rule:nonzero;stroke:none"/><path id="path238" d="M523.082 441.19c-.629 0-1.203.148-1.64.547-.448.404-.683.88-.683 1.503v84.18c0 .618.235 1.192.683 1.64a2.34 2.34 0 0 0 1.64.683h20.225c4.149 0 8.172-.628 11.89-1.776 3.716-1.165 6.914-2.766 9.702-4.92 2.793-2.18 4.968-4.87 6.56-8.063 1.59-3.186 2.459-6.816 2.459-10.795 0-2.75-.476-5.357-1.503-7.79-1.017-2.438-2.378-4.57-4.1-6.423-1.733-1.864-3.755-3.312-6.013-4.51a24.2 24.2 0 0 0-7.106-2.46c1.771-.53 3.553-1.398 5.466-2.595 1.908-1.192 3.734-2.646 5.33-4.373s2.902-3.712 3.963-5.877c1.066-2.17 1.64-4.45 1.64-6.832 0-3.81-.716-7.063-2.187-9.84-1.47-2.793-3.427-5.154-5.876-6.97-2.438-1.808-5.242-3.213-8.473-4.099-3.236-.88-6.646-1.23-10.112-1.23Zm61.358 0c-.617 0-1.098.153-1.503.547-.388.4-.453.885-.273 1.503l16.945 84.18c.18.607.607 1.192 1.093 1.64.487.432 1.028.683 1.64.683h6.696c.624 0 1.154-.251 1.64-.683.476-.448.777-1.033.957-1.64l14.485-74.204 14.623 74.204c.174.607.47 1.192.956 1.64.487.432 1.028.683 1.64.683h6.696c.629 0 1.192-.251 1.64-.683a3.9 3.9 0 0 0 .957-1.64l16.945-84.18c.186-.618.076-1.104-.273-1.503-.367-.394-.755-.547-1.367-.547h-5.33c-1.328 0-2.246.634-2.596 2.05l-13.119 74.34-14.622-74.34c-.356-1.416-1.115-2.05-2.46-2.05h-6.696c-1.41 0-2.252.634-2.596 2.05l-14.486 74.34-13.256-74.34c-.36-1.416-1.322-2.05-2.733-2.05Zm-53.979 7.79h14.486c4.974 0 9.047 1.312 12.162 3.963 3.1 2.65 4.647 6.28 4.647 10.796 0 2.3-.536 4.345-1.504 6.286-.984 1.946-2.224 3.651-3.826 5.056-1.59 1.421-3.433 2.482-5.466 3.28a17.5 17.5 0 0 1-6.423 1.23h-14.076Zm0 38.263h14.486c5.592 0 10.15 1.542 13.666 4.51 3.492 2.963 5.192 6.887 5.192 12.026 0 2.662-.382 5.16-1.366 7.379a17.2 17.2 0 0 1-4.1 5.74c-1.727 1.596-3.706 2.804-6.013 3.69a20.4 20.4 0 0 1-7.38 1.367l-14.485-.001Zm-117.523 62.725v20.225c0 .541.03 1.143.136 1.64.098.498.29.853.547 1.23a3.2 3.2 0 0 0 1.093.957c.426.224.967.273 1.64.273a3.8 3.8 0 0 0 1.093-.136c.404-.11.661-.301.82-.41l-.683-1.913-.547.41-.547.136c-.426 0-.781-.263-.956-.683-.18-.432-.274-.94-.274-1.64v-20.089Zm87.87 0v23.915h2.322v-12.162l.41-.82.82-1.094a6.6 6.6 0 0 1 1.367-.957c.48-.261.967-.409 1.503-.409.54 0 .95.164 1.366.41s.826.563 1.094.956c.273.389.399.886.546 1.367.148.487.273.913.273 1.367v11.342h2.324v-11.616c0-.694-.05-1.366-.274-2.05-.224-.688-.656-1.366-1.093-1.913-.432-.535-.967-.907-1.64-1.23a5.2 5.2 0 0 0-2.323-.547c-.585 0-1.137.176-1.64.41-.503.241-.978.503-1.366.82a5.8 5.8 0 0 0-.957.957l-.41.683v-9.429Zm46.735 0v23.915h2.324v-12.162l.41-.82.82-1.094c.37-.344.743-.705 1.23-.956a3.6 3.6 0 0 1 1.64-.41c.54 0 .945.164 1.366.41.415.246.825.563 1.093.956.267.389.4.886.546 1.367.143.487.274.913.274 1.367v11.342h2.323v-11.616c0-.694-.18-1.366-.41-2.05-.219-.688-.514-1.366-.956-1.913-.438-.535-.968-.907-1.64-1.23a5.15 5.15 0 0 0-2.323-.547c-.58 0-1.137.176-1.64.41-.503.241-.979.503-1.367.82a5.8 5.8 0 0 0-.957.957l-.41.683v-9.429Zm35.258 0v20.225c0 .541.026 1.143.136 1.64.093.498.284.853.547 1.23.256.366.53.738.956.957.427.224.968.273 1.64.273.334 0 .82-.022 1.23-.136.4-.11.667-.301.82-.41l-.683-1.913-.547.41-.683.136c-.42 0-.645-.263-.82-.683-.186-.432-.273-.94-.273-1.64v-20.089Zm-223.16 1.503v22.412h5.33c1.394 0 2.635-.345 3.827-.957 1.191-.6 2.28-1.41 3.143-2.46a11.9 11.9 0 0 0 2.05-3.69c.492-1.426.683-3.005.683-4.645a12.4 12.4 0 0 0-.82-4.51c-.52-1.328-1.132-2.356-2.05-3.28-.918-.918-2.033-1.694-3.28-2.186s-2.662-.684-4.1-.684Zm88.827 0v22.412h2.46v-10.66h9.156v10.66h2.46v-22.412h-2.46v9.703h-9.156v-9.703Zm-86.366 1.914h2.733c.967 0 1.842.278 2.7Line truncated
|
||||
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="svg4232" width="541.333" height="226.114"><g id="layer1" transform="translate(-129.334 -393.59)"><path id="path84" d="M271.089 441.054v36.897c0 1.836-1.58 4.865-3.416 6.696l-37.58 37.58c-.92.918-1.86 1.367-2.46 1.367-.602 0-.957-.449-.957-1.367v4.1c0 1.836 1.58 3.416 3.416 3.416h81.857c1.837 0 3.416-1.58 3.416-3.416v-81.994c0-1.836-1.58-3.28-3.416-3.28Zm-52.066 85.273c0 1.836-1.585 3.416-3.416 3.416h-40.86v44.55c0 .918.36 1.367.956 1.367s1.405-.449 2.323-1.367l37.58-37.58c1.832-1.837 3.417-4.86 3.417-6.696Z" style="fill:#fff;fill-opacity:.5;fill-rule:nonzero;stroke:none"/><path id="path88" d="M270.132 394.59c-.604 0-1.541.449-2.46 1.367l-37.58 37.717c-1.831 1.831-3.416 4.723-3.416 6.56v81.993c0 1.837 1.585 1.837 3.416.00014999999996234692l37.58-37.58c1.837-1.837 3.417-4.86 3.417-6.696v-81.994c0-.918-.353-1.366-.957-1.366M133.75 441.055c-1.837 0-3.416 1.443-3.416 3.28v81.993c0 1.836 1.58 3.416 3.416 3.416h81.857c1.836 0 3.416-1.58 3.416-3.416v-81.994c0-1.836-1.58-3.28-3.416-3.28Z" style="fill:#fff;fill-opacity:.5;fill-rule:nonzero;stroke:none"/><path id="path100" d="M230.092 441.054c-1.836 0-3.416 1.58-3.416 3.416v77.757c0 .918.355 1.367.956 1.367.602 0 1.542-.449 2.46-1.367l37.58-37.58c1.837-1.837 3.417-4.865 3.417-6.696v-36.897Zm-12.026 5.603c-.606 0-1.541.453-2.46 1.366l-37.58 37.717c-1.83 1.87-3.28 4.728-3.28 6.56v37.443h40.86c1.837 0 3.417-1.58 3.417-3.416v-78.304c0-.913-.355-1.366-.957-1.366" style="fill:#fff;fill-opacity:.5;fill-rule:nonzero;stroke:none"/><path id="path238" d="M523.082 441.19c-.629 0-1.203.148-1.64.547-.448.404-.683.88-.683 1.503v84.18c0 .618.235 1.192.683 1.64a2.34 2.34 0 0 0 1.64.683h20.225c4.149 0 8.172-.628 11.89-1.776 3.716-1.165 6.914-2.766 9.702-4.92 2.793-2.18 4.968-4.87 6.56-8.063 1.59-3.186 2.459-6.816 2.459-10.795 0-2.75-.476-5.357-1.503-7.79-1.017-2.438-2.378-4.57-4.1-6.423-1.733-1.864-3.755-3.312-6.013-4.51a24.2 24.2 0 0 0-7.106-2.46c1.771-.53 3.553-1.398 5.466-2.595 1.908-1.192 3.734-2.646 5.33-4.373s2.902-3.712 3.963-5.877c1.066-2.17 1.64-4.45 1.64-6.832 0-3.81-.716-7.063-2.187-9.84-1.47-2.793-3.427-5.154-5.876-6.97-2.438-1.808-5.242-3.213-8.473-4.099-3.236-.88-6.646-1.23-10.112-1.23Zm61.358 0c-.617 0-1.098.153-1.503.547-.388.4-.453.885-.273 1.503l16.945 84.18c.18.607.607 1.192 1.093 1.64.487.432 1.028.683 1.64.683h6.696c.624 0 1.154-.251 1.64-.683.476-.448.777-1.033.957-1.64l14.485-74.204 14.623 74.204c.174.607.47 1.192.956 1.64.487.432 1.028.683 1.64.683h6.696c.629 0 1.192-.251 1.64-.683a3.9 3.9 0 0 0 .957-1.64l16.945-84.18c.186-.618.076-1.104-.273-1.503-.367-.394-.755-.547-1.367-.547h-5.33c-1.328 0-2.246.634-2.596 2.05l-13.119 74.34-14.622-74.34c-.356-1.416-1.115-2.05-2.46-2.05h-6.696c-1.41 0-2.252.634-2.596 2.05l-14.486 74.34-13.256-74.34c-.36-1.416-1.322-2.05-2.733-2.05Zm-53.979 7.79h14.486c4.974 0 9.047 1.312 12.162 3.963 3.1 2.65 4.647 6.28 4.647 10.796 0 2.3-.536 4.345-1.504 6.286-.984 1.946-2.224 3.651-3.826 5.056-1.59 1.421-3.433 2.482-5.466 3.28a17.5 17.5 0 0 1-6.423 1.23h-14.076Zm0 38.263h14.486c5.592 0 10.15 1.542 13.666 4.51 3.492 2.963 5.192 6.887 5.192 12.026 0 2.662-.382 5.16-1.366 7.379a17.2 17.2 0 0 1-4.1 5.74c-1.727 1.596-3.706 2.804-6.013 3.69a20.4 20.4 0 0 1-7.38 1.367l-14.485-.001Zm-117.523 62.725v20.225c0 .541.03 1.143.136 1.64.098.498.29.853.547 1.23a3.2 3.2 0 0 0 1.093.957c.426.224.967.273 1.64.273a3.8 3.8 0 0 0 1.093-.136c.404-.11.661-.301.82-.41l-.683-1.913-.547.41-.547.136c-.426 0-.781-.263-.956-.683-.18-.432-.274-.94-.274-1.64v-20.089Zm87.87 0v23.915h2.322v-12.162l.41-.82.82-1.094a6.6 6.6 0 0 1 1.367-.957c.48-.261.967-.409 1.503-.409.54 0 .95.164 1.366.41s.826.563 1.094.956c.273.389.399.886.546 1.367.148.487.273.913.273 1.367v11.342h2.324v-11.616c0-.694-.05-1.366-.274-2.05-.224-.688-.656-1.366-1.093-1.913-.432-.535-.967-.907-1.64-1.23a5.2 5.2 0 0 0-2.323-.547c-.585 0-1.137.176-1.64.41-.503.241-.978.503-1.366.82a5.8 5.8 0 0 0-.957.957l-.41.683v-9.429Zm46.735 0v23.915h2.324v-12.162l.41-.82.82-1.094c.37-.344.743-.705 1.23-.956a3.6 3.6 0 0 1 1.64-.41c.54 0 .945.164 1.366.41.415.246.825.563 1.093.956.267.389.4.886.546 1.367.143.487.274.913.274 1.367v11.342h2.323v-11.616c0-.694-.18-1.366-.41-2.05-.219-.688-.514-1.366-.956-1.913-.438-.535-.968-.907-1.64-1.23a5.15 5.15 0 0 0-2.323-.547c-.58 0-1.137.176-1.64.41-.503.241-.979.503-1.367.82a5.8 5.8 0 0 0-.957.957l-.41.683v-9.429Zm35.258 0v20.225c0 .541.026 1.143.136 1.64.093.498.284.853.547 1.23.256.366.53.738.956.957.427.224.968.273 1.64.273.334 0 .82-.022 1.23-.136.4-.11.667-.301.82-.41l-.683-1.913-.547.41-.683.136c-.42 0-.645-.263-.82-.683-.186-.432-.273-.94-.273-1.64v-20.089Zm-223.16 1.503v22.412h5.33c1.394 0 2.635-.345 3.827-.957 1.191-.6 2.28-1.41 3.143-2.46a11.9 11.9 0 0 0 2.05-3.69c.492-1.426.683-3.005.683-4.645a12.4 12.4 0 0 0-.82-4.51c-.52-1.328-1.132-2.356-2.05-3.28-.918-.918-2.033-1.694-3.28-2.186s-2.662-.684-4.1-.684Zm88.827 0v22.412h2.46v-10.66h9.156v10.66h2.46v-22.412h-2.46v9.703h-9.156v-9.703Zm-86.366 1.914h2.733c.967 0 1.842.278 2.733.683.88Line truncated
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><defs><linearGradient id="a" x1="882.997" x2="638.955" y1="27.113" y2="866.902" gradientTransform="scale(.1)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000014"/><stop offset="1" stop-color="#150426"/></linearGradient><linearGradient id="b" x1="1001.68" x2="790.326" y1="652.45" y2="1094.91" gradientTransform="scale(.1)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff1639"/><stop offset="1" stop-color="#ff1639" stop-opacity="0"/></linearGradient></defs><path fill="url(#a)" d="M81.504 9.465c.973 1.207 1.469 2.836 2.457 6.09l21.656 71.136a90.079 90.079 0 0 0-25.89-8.765L65.629 30.28a1.833 1.833 0 0 0-3.52.004L48.18 77.902a90.104 90.104 0 0 0-26.003 8.778l21.758-71.14c.996-3.25 1.492-4.876 2.464-6.083a8.023 8.023 0 0 1 3.243-2.398c1.433-.575 3.136-.575 6.535-.575H71.72c3.402 0 5.105 0 6.543.579a7.988 7.988 0 0 1 3.242 2.402Zm0 0"/><path fill="#ff5d01" d="M84.094 90.074c-3.57 3.055-10.696 5.137-18.903 5.137-10.07 0-18.515-3.137-20.754-7.356-.8 2.418-.98 5.184-.98 6.954 0 0-.527 8.675 5.508 14.71a5.671 5.671 0 0 1 5.672-5.671c5.37 0 5.367 4.683 5.363 8.488v.336c0 5.773 3.527 10.719 8.543 12.805a11.62 11.62 0 0 1-1.172-5.098c0-5.508 3.23-7.555 6.988-9.938 2.989-1.894 6.309-4 8.594-8.222a15.513 15.513 0 0 0 1.875-7.41 15.55 15.55 0 0 0-.734-4.735Zm0 0"/><path fill="url(#b)" d="M84.094 90.074c-3.57 3.055-10.696 5.137-18.903 5.137-10.07 0-18.515-3.137-20.754-7.356-.8 2.418-.98 5.184-.98 6.954 0 0-.527 8.675 5.508 14.71a5.671 5.671 0 0 1 5.672-5.671c5.37 0 5.367 4.683 5.363 8.488v.336c0 5.773 3.527 10.719 8.543 12.805a11.62 11.62 0 0 1-1.172-5.098c0-5.508 3.23-7.555 6.988-9.938 2.989-1.894 6.309-4 8.594-8.222a15.513 15.513 0 0 0 1.875-7.41 15.55 15.55 0 0 0-.734-4.735Zm0 0"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" focusable="false">
|
||||
<title>
|
||||
Gatsby
|
||||
</title>
|
||||
<circle cx="14" cy="14" r="14" fill="#639"/>
|
||||
<path fill="#fff" d="M6.2 21.8C4.1 19.7 3 16.9 3 14.2L13.9 25c-2.8-.1-5.6-1.1-7.7-3.2zm10.2 2.9L3.3 11.6C4.4 6.7 8.8 3 14 3c3.7 0 6.9 1.8 8.9 4.5l-1.5 1.3C19.7 6.5 17 5 14 5c-3.9 0-7.2 2.5-8.5 6L17 22.5c2.9-1 5.1-3.5 5.8-6.5H18v-2h7c0 5.2-3.7 9.6-8.6 10.7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="394" height="80" viewBox="0 0 394 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M261.919 0.0330722H330.547V12.7H303.323V79.339H289.71V12.7H261.919V0.0330722Z" fill="black"/>
|
||||
<path d="M149.052 0.0330722V12.7H94.0421V33.0772H138.281V45.7441H94.0421V66.6721H149.052V79.339H80.43V12.7H80.4243V0.0330722H149.052Z" fill="black"/>
|
||||
<path d="M183.32 0.0661486H165.506L229.312 79.3721H247.178L215.271 39.7464L247.127 0.126654L229.312 0.154184L206.352 28.6697L183.32 0.0661486Z" fill="black"/>
|
||||
<path d="M201.6 56.7148L192.679 45.6229L165.455 79.4326H183.32L201.6 56.7148Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.907 79.339L17.0151 0H0V79.3059H13.6121V16.9516L63.8067 79.339H80.907Z" fill="black"/>
|
||||
<path d="M333.607 78.8546C332.61 78.8546 331.762 78.5093 331.052 77.8186C330.342 77.1279 329.991 76.2917 330 75.3011C329.991 74.3377 330.342 73.5106 331.052 72.8199C331.762 72.1292 332.61 71.7838 333.607 71.7838C334.566 71.7838 335.405 72.1292 336.115 72.8199C336.835 73.5106 337.194 74.3377 337.204 75.3011C337.194 75.9554 337.028 76.5552 336.696 77.0914C336.355 77.6368 335.922 78.064 335.377 78.373C334.842 78.6911 334.252 78.8546 333.607 78.8546Z" fill="black"/>
|
||||
<path d="M356.84 45.4453H362.872V68.6846C362.863 70.8204 362.401 72.6472 361.498 74.1832C360.585 75.7191 359.321 76.8914 357.698 77.7185C356.084 78.5364 354.193 78.9546 352.044 78.9546C350.079 78.9546 348.318 78.6001 346.75 77.9094C345.182 77.2187 343.937 76.1826 343.024 74.8193C342.101 73.456 341.649 71.7565 341.649 69.7207H347.691C347.7 70.6114 347.903 71.3838 348.29 72.0291C348.677 72.6744 349.212 73.1651 349.895 73.5105C350.586 73.8559 351.38 74.0286 352.274 74.0286C353.243 74.0286 354.073 73.8286 354.746 73.4196C355.419 73.0197 355.936 72.4199 356.296 71.6201C356.646 70.8295 356.831 69.8479 356.84 68.6846V45.4453Z" fill="black"/>
|
||||
<path d="M387.691 54.5338C387.544 53.1251 386.898 52.0254 385.773 51.2438C384.638 50.4531 383.172 50.0623 381.373 50.0623C380.11 50.0623 379.022 50.2532 378.118 50.6258C377.214 51.0075 376.513 51.5164 376.033 52.1617C375.554 52.807 375.314 53.5432 375.295 54.3703C375.295 55.061 375.461 55.6608 375.784 56.1607C376.107 56.6696 376.54 57.0968 377.103 57.4422C377.656 57.7966 378.274 58.0874 378.948 58.3237C379.63 58.56 380.313 58.76 380.995 58.9236L384.14 59.6961C385.404 59.9869 386.631 60.3778 387.802 60.8776C388.973 61.3684 390.034 61.9955 390.965 62.7498C391.897 63.5042 392.635 64.413 393.179 65.4764C393.723 66.5397 394 67.7848 394 69.2208C394 71.1566 393.502 72.8562 392.496 74.3285C391.491 75.7917 390.043 76.9369 388.143 77.764C386.252 78.582 383.965 79 381.272 79C378.671 79 376.402 78.6002 374.493 77.8004C372.575 77.0097 371.08 75.8463 370.001 74.3194C368.922 72.7926 368.341 70.9294 368.258 68.7391H374.235C374.318 69.8842 374.687 70.8386 375.314 71.6111C375.95 72.3745 376.78 72.938 377.795 73.3197C378.819 73.6923 379.962 73.8832 381.226 73.8832C382.545 73.8832 383.707 73.6832 384.712 73.2924C385.708 72.9016 386.492 72.3564 387.055 71.6475C387.627 70.9476 387.913 70.1206 387.922 69.1754C387.913 68.312 387.654 67.5939 387.156 67.0304C386.649 66.467 385.948 65.9944 385.053 65.6127C384.15 65.231 383.098 64.8856 381.899 64.5857L378.081 63.6223C375.323 62.9225 373.137 61.8592 371.541 60.4323C369.937 59.0054 369.143 57.115 369.143 54.7429C369.143 52.798 369.678 51.0894 370.758 49.6261C371.827 48.1629 373.294 47.0268 375.148 46.2179C377.011 45.4 379.114 45 381.456 45C383.836 45 385.92 45.4 387.719 46.2179C389.517 47.0268 390.929 48.1538 391.952 49.5897C392.976 51.0257 393.511 52.6707 393.539 54.5338H387.691Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><path d="M39.267 108.97l-.284-.567c-.567-1.135-.567-2.27-.283-3.689H8.059L53.454 24.14l19.009 34.33 6.241-4.54L59.695 19.6c-.283-.567-2.553-3.971-6.241-3.971-1.703 0-4.256.567-6.242 4.255L1.25 101.31c-.284.852-2.27 4.54-.568 7.66 1.135 1.703 2.838 3.405 6.81 3.405h38.585c-3.972 0-5.958-1.702-6.81-3.404z" fill="#00c58e"/><path d="M126.65 101.59L89.767 35.201c-.567-.567-2.553-4.256-6.242-4.256-1.702 0-4.255.851-6.241 4.256l-4.823 7.944v15.321l11.065-19.009 36.599 65.254h-13.902a6.525 6.525 0 01-.568 3.972l-.284.284c-1.702 3.12-5.958 3.404-6.525 3.404h21.562c.851 0 4.823-.283 6.809-3.404.851-1.419 1.419-3.972-.567-7.377z" fill="#108775"/><path d="M106.51 108.97v-.284l.284-.567c.283-1.135.567-2.27.283-3.405l-1.134-3.404-28.938-50.501-4.256-7.66h-.284l-4.256 7.66-28.938 50.5-1.134 3.405a6.81 6.81 0 00.567 4.256c1.135 1.702 2.837 3.405 6.809 3.405h53.906c.851 0 5.107-.284 7.093-3.405zM72.464 58.469l26.386 46.245H46.079z" fill="#2f495e"/></svg>
|
||||
|
After Width: | Height: | Size: 1014 B |
|
After Width: | Height: | Size: 48 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.23174 23 20.46348">
|
||||
<title>React Logo</title>
|
||||
<circle cx="0" cy="0" r="2.05" fill="#61dafb"/>
|
||||
<g stroke="#61dafb" stroke-width="1" fill="none">
|
||||
<ellipse rx="11" ry="4.2"/>
|
||||
<ellipse rx="11" ry="4.2" transform="rotate(60)"/>
|
||||
<ellipse rx="11" ry="4.2" transform="rotate(120)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 98.1 118" style="enable-background:new 0 0 98.1 118;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FF3E00;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M91.8,15.6C80.9-0.1,59.2-4.7,43.6,5.2L16.1,22.8C8.6,27.5,3.4,35.2,1.9,43.9c-1.3,7.3-0.2,14.8,3.3,21.3
|
||||
c-2.4,3.6-4,7.6-4.7,11.8c-1.6,8.9,0.5,18.1,5.7,25.4c11,15.7,32.6,20.3,48.2,10.4l27.5-17.5c7.5-4.7,12.7-12.4,14.2-21.1
|
||||
c1.3-7.3,0.2-14.8-3.3-21.3c2.4-3.6,4-7.6,4.7-11.8C99.2,32.1,97.1,22.9,91.8,15.6"/>
|
||||
<path class="st1" d="M40.9,103.9c-8.9,2.3-18.2-1.2-23.4-8.7c-3.2-4.4-4.4-9.9-3.5-15.3c0.2-0.9,0.4-1.7,0.6-2.6l0.5-1.6l1.4,1
|
||||
c3.3,2.4,6.9,4.2,10.8,5.4l1,0.3l-0.1,1c-0.1,1.4,0.3,2.9,1.1,4.1c1.6,2.3,4.4,3.4,7.1,2.7c0.6-0.2,1.2-0.4,1.7-0.7L65.5,72
|
||||
c1.4-0.9,2.3-2.2,2.6-3.8c0.3-1.6-0.1-3.3-1-4.6c-1.6-2.3-4.4-3.3-7.1-2.6c-0.6,0.2-1.2,0.4-1.7,0.7l-10.5,6.7
|
||||
c-1.7,1.1-3.6,1.9-5.6,2.4c-8.9,2.3-18.2-1.2-23.4-8.7c-3.1-4.4-4.4-9.9-3.4-15.3c0.9-5.2,4.1-9.9,8.6-12.7l27.5-17.5
|
||||
c1.7-1.1,3.6-1.9,5.6-2.5c8.9-2.3,18.2,1.2,23.4,8.7c3.2,4.4,4.4,9.9,3.5,15.3c-0.2,0.9-0.4,1.7-0.7,2.6l-0.5,1.6l-1.4-1
|
||||
c-3.3-2.4-6.9-4.2-10.8-5.4l-1-0.3l0.1-1c0.1-1.4-0.3-2.9-1.1-4.1c-1.6-2.3-4.4-3.3-7.1-2.6c-0.6,0.2-1.2,0.4-1.7,0.7L32.4,46.1
|
||||
c-1.4,0.9-2.3,2.2-2.6,3.8s0.1,3.3,1,4.6c1.6,2.3,4.4,3.3,7.1,2.6c0.6-0.2,1.2-0.4,1.7-0.7l10.5-6.7c1.7-1.1,3.6-1.9,5.6-2.5
|
||||
c8.9-2.3,18.2,1.2,23.4,8.7c3.2,4.4,4.4,9.9,3.5,15.3c-0.9,5.2-4.1,9.9-8.6,12.7l-27.5,17.5C44.8,102.5,42.9,103.3,40.9,103.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,132 @@
|
||||
<svg viewBox="0 0 23 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.7482 0H18.8887C21.641 3.93959 21.6571 10.0462 18.8887 14H20.7482C23.516 10.0462 23.4999 3.93959 20.7482 0Z" fill="white"/>
|
||||
<path d="M2.07027 3.05176e-05C-0.682028 3.93963 -0.698142 10.0463 2.07027 14H3.92985C1.16208 10.0463 1.1782 3.93963 3.92985 3.05176e-05H2.07027Z" fill="white"/>
|
||||
<path d="M12.0135 13.6771C11.815 13.9297 11.4089 13.7892 11.4089 13.4682V10.3853C11.4089 10.0114 11.106 9.70846 10.7321 9.70846H7.32818C7.05295 9.70846 6.89245 9.39713 7.05295 9.17347L9.29089 6.04023C9.61124 5.59225 9.29089 4.9696 8.73979 4.9696H4.62036C4.34513 4.9696 4.18463 4.65828 4.34512 4.43461L7.24632 0.372548C7.31013 0.283598 7.41262 0.230743 7.52155 0.230743H16.1671C16.4424 0.230743 16.6029 0.542069 16.4424 0.765734L14.2044 3.89897C13.8841 4.34695 14.2044 4.9696 14.7555 4.9696H18.1595C18.4418 4.9696 18.6004 5.29511 18.4257 5.51748L12.0142 13.6777L12.0135 13.6771Z" fill="#863BFF"/>
|
||||
<mask id="mask0_127_17274" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="4" y="0" width="15" height="14">
|
||||
<path d="M11.9823 13.677C11.7838 13.9296 11.3777 13.7891 11.3777 13.4681V10.3852C11.3777 10.0113 11.0747 9.70837 10.7009 9.70837H7.29693C7.0217 9.70837 6.8612 9.39704 7.0217 9.17338L9.25964 6.04014C9.57999 5.59216 9.25964 4.96951 8.70854 4.96951H4.58911C4.31388 4.96951 4.15338 4.65818 4.31387 4.43452L7.21507 0.372457C7.27888 0.283506 7.38137 0.230652 7.4903 0.230652H16.1359C16.4111 0.230652 16.5716 0.541978 16.4111 0.765643L14.1732 3.89888C13.8528 4.34686 14.1732 4.96951 14.7243 4.96951H18.1282C18.4106 4.96951 18.5691 5.29502 18.3944 5.51739L11.9829 13.6776L11.9823 13.677Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_127_17274)">
|
||||
<g filter="url(#filter0_f_127_17274)">
|
||||
<ellipse cx="1.6481" cy="4.39979" rx="1.6481" ry="4.39979" transform="matrix(0.00324134 0.999995 0.999995 -0.00324134 2.91309 9.66077)" fill="#EDE6FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_f_127_17274)">
|
||||
<ellipse cx="3.11172" cy="8.9321" rx="3.11172" ry="8.9321" transform="matrix(0.00324134 0.999995 0.999995 -0.00324134 -7.51758 2.58936)" fill="#EDE6FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_f_127_17274)">
|
||||
<ellipse cx="1.6481" cy="9.12221" rx="1.6481" ry="9.12221" transform="matrix(0.00324134 0.999995 0.999995 -0.00324134 -7.8584 3.61816)" fill="#7E14FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_f_127_17274)">
|
||||
<ellipse cx="1.6481" cy="9.15566" rx="1.6481" ry="9.15566" transform="matrix(0.00324134 0.999995 0.999995 -0.00324134 -6.4834 9.00391)" fill="#7E14FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_f_127_17274)">
|
||||
<ellipse cx="1.6481" cy="9.15566" rx="1.6481" ry="9.15566" transform="matrix(0.00324134 0.999995 0.999995 -0.00324134 -6.02441 9.34766)" fill="#7E14FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_f_127_17274)">
|
||||
<ellipse cx="4.21045" cy="6.60625" rx="4.21045" ry="6.60625" transform="matrix(0.0584509 -0.99829 -0.99829 -0.0584509 26.4971 8.26871)" fill="#EDE6FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter6_f_127_17274)">
|
||||
<ellipse cx="1.03839" cy="6.43346" rx="1.03839" ry="6.43346" transform="matrix(-0.0172986 -0.99985 -0.99985 0.0172986 26.9297 5.63535)" fill="#7E14FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter7_f_127_17274)">
|
||||
<ellipse cx="1.03839" cy="6.43346" rx="1.03839" ry="6.43346" transform="matrix(-0.0172986 -0.99985 -0.99985 0.0172986 26.9297 5.63535)" fill="#7E14FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter8_f_127_17274)">
|
||||
<ellipse cx="4.36576" cy="2.91514" rx="1.31855" ry="8.70955" transform="rotate(39.5103 4.36576 2.91514)" fill="#7E14FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter9_f_127_17274)">
|
||||
<ellipse cx="18.4697" cy="-1.59207" rx="1.31855" ry="8.70955" transform="rotate(37.8923 18.4697 -1.59207)" fill="#7E14FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter10_f_127_17274)">
|
||||
<ellipse cx="16.6417" cy="2.12577" rx="1.78679" ry="2.89199" transform="rotate(37.8923 16.6417 2.12577)" fill="#47BFFF"/>
|
||||
</g>
|
||||
<g filter="url(#filter11_f_127_17274)">
|
||||
<ellipse cx="3.68841" cy="11.7003" rx="1.31855" ry="8.70955" transform="rotate(37.8923 3.68841 11.7003)" fill="#7E14FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter12_f_127_17274)">
|
||||
<ellipse cx="3.68841" cy="11.7003" rx="1.31855" ry="8.70955" transform="rotate(37.8923 3.68841 11.7003)" fill="#7E14FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter13_f_127_17274)">
|
||||
<ellipse cx="14.9179" cy="9.17936" rx="1.31855" ry="8.70955" transform="rotate(37.8923 14.9179 9.17936)" fill="#7E14FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter14_f_127_17274)">
|
||||
<ellipse cx="15.7453" cy="9.92533" rx="1.78679" ry="4.57726" transform="rotate(37.8923 15.7453 9.92533)" fill="#47BFFF"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_127_17274" x="-1.66562" y="5.06287" width="17.967" height="12.4635" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="2.29179" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_127_17274" x="-12.0914" y="-2.02332" width="27.0314" height="15.3908" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="2.29179" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_127_17274" x="-10.6037" y="0.838165" width="23.7454" height="8.79703" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_127_17274" x="-9.22867" y="6.22382" width="23.8118" height="8.79703" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter4_f_127_17274" x="-8.76968" y="6.56757" width="23.8118" height="8.79703" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter5_f_127_17274" x="8.96524" y="-5.12549" width="22.3664" height="17.6096" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="2.29179" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter6_f_127_17274" x="11.2967" y="0.91397" width="18.3655" height="7.58884" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter7_f_127_17274" x="11.2967" y="0.91397" width="18.3655" height="7.58884" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter8_f_127_17274" x="-4.01871" y="-6.60739" width="16.7698" height="19.045" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter9_f_127_17274" x="10.2694" y="-11.2637" width="16.4007" height="19.3433" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter10_f_127_17274" x="11.6239" y="-3.15738" width="10.0355" height="10.5663" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter11_f_127_17274" x="-4.51187" y="2.02869" width="16.4007" height="19.3433" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter12_f_127_17274" x="-4.51187" y="2.02869" width="16.4007" height="19.3433" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter13_f_127_17274" x="6.71762" y="-0.49228" width="16.4007" height="19.3433" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
<filter id="filter14_f_127_17274" x="9.84946" y="3.39893" width="11.7913" height="13.0528" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1.37508" result="effect1_foregroundBlur_127_17274"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 261.76 226.69" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883"/></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 467 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3916 1524"><title>logo-on-white-bg</title><path fill="#FFF" d="M808 336l387 218.9v437.9l-387 218.9-387-218.9V554.9z"/><path fill="#8ED6FB" d="M1125.9 977.7l-305.1 172.6v-134.4l190.1-104.6 115 66.4zm20.9-18.9V597.9l-111.6 64.5v232l111.6 64.4zm-657.9 18.9L794 1150.3v-134.4L603.8 911.3l-114.9 66.4zM468 958.8V597.9l111.6 64.5v232L468 958.8zm13.1-384.3l312.9-177v129.9L593.5 637.7l-1.6.9-110.8-64.1zm652.6 0l-312.9-177v129.9l200.5 110.2 1.6.9 110.8-64z"/><path fill="#1C78C0" d="M794 985.3L606.4 882.1V677.8L794 786.1v199.2zm26.8 0l187.6-103.1V677.8L820.8 786.1v199.2zm-13.4-207zM619.1 654.2l188.3-103.5 188.3 103.5-188.3 108.7-188.3-108.7z"/><path fill="#1A1C1C" d="M1585.3 912.3h82.5l84.1-280.2h-80.4l-49.8 198.8-53.1-198.8H1499l-53.6 198.8-49.3-198.8h-80.4l83.6 280.2h82.5l52-179.5 51.5 179.5zM1756.2 773c0 84.1 57.3 146.3 147.4 146.3 69.7 0 107.2-41.8 117.9-61.6l-48.8-37c-8 11.8-30 34.3-68.1 34.3-41.3 0-71.3-26.8-72.9-64.3H2029c.5-5.4.5-10.7.5-16.1 0-91.6-49.3-149.5-136.1-149.5-79.9 0-137.2 63.2-137.2 147.9zm77.7-30.6c3.2-32.1 25.7-56.8 60.6-56.8 33.8 0 58.4 22.5 60 56.8h-120.6zm223.5 169.9h69.7v-28.9c7.5 9.1 35.4 35.9 83.1 35.9 80.4 0 137.2-60.5 137.2-146.8 0-86.8-52.5-147.3-132.9-147.3-48.2 0-76.1 26.8-83.1 36.4V524.9h-73.9v387.4zm71.8-139.3c0-52.5 31.1-82.5 71.8-82.5 42.9 0 71.8 33.8 71.8 82.5 0 49.8-30 80.9-71.8 80.9-45 0-71.8-36.5-71.8-80.9zm247 239.5h73.9V883.3c7 9.1 34.8 35.9 83.1 35.9 80.4 0 132.9-60.5 132.9-147.3 0-85.7-56.8-146.8-137.2-146.8-47.7 0-75.6 26.8-83.1 36.4V632h-69.7v380.5zm71.8-241.1c0-44.5 26.8-80.9 71.8-80.9 41.8 0 71.8 31.1 71.8 80.9 0 48.8-28.9 82.5-71.8 82.5-40.7 0-71.8-30-71.8-82.5zm231.5 54.1c0 58.9 48.2 93.8 105 93.8 32.2 0 53.6-9.6 68.1-25.2l4.8 18.2h65.4V734.9c0-62.7-26.8-109.8-116.8-109.8-42.9 0-85.2 16.1-110.4 33.2l27.9 50.4c20.9-10.7 46.6-19.8 74.5-19.8 32.7 0 50.9 16.6 50.9 41.3v18.2c-10.2-7-32.2-15.5-60.6-15.5-65.4-.1-108.8 37.4-108.8 92.6zm73.9-2.2c0-23 19.8-39.1 48.2-39.1s48.8 14.5 48.8 39.1c0 23.6-20.4 38.6-48.2 38.6s-48.8-15.5-48.8-38.6zm348.9 30.6c-46.6 0-79.8-33.8-79.8-81.4 0-45 29.5-82 77.2-82 31.6 0 53.1 15.5 65.4 26.8l20.9-62.2c-18.2-13.9-47.2-30-88.4-30-85.2 0-149 62.7-149 147.9s62.2 146.3 149.5 146.3c40.7 0 71.3-17.1 87.3-30l-19.8-60.5c-12.4 10.1-34.9 25.1-63.3 25.1zm110.9 58.4h73.9V767.6l93.8 144.7h86.8L3361.6 759l98.6-127h-83.1l-90 117.9v-225h-73.9v387.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 675.012 183.973">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #0014eb;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #0f1d49;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="cls-1" d="M83.783,96.187l1.194-2.545c.258-.551.516-1.102.775-1.653-3.815-9.283-6.973-18.834-9.493-28.557-3.775,4.027-6.94,8.648-9.325,13.723,3.334,9.124,7.925,17.758,13.666,25.635,1.092-2.197,2.15-4.405,3.183-6.601h0Z"/>
|
||||
<path class="cls-1" d="M78.4,107.042c-5.629-7.417-10.255-15.511-13.803-24.066-1.771,5.321-2.744,11.014-2.744,16.941v7.112c3.316,3.89,6.348,8.026,9.068,12.389,2.76-3.905,5.214-8.086,7.479-12.375h0Z"/>
|
||||
<path class="cls-1" d="M61.853,113.839v16.037c2.269-2.084,4.361-4.358,6.303-6.778-1.93-3.212-4.035-6.301-6.303-9.258h0Z"/>
|
||||
<path class="cls-1" d="M88.258,86.704c1.642-3.407,3.344-6.8,5.177-10.114-.097-5.355-.501-10.547-.914-15.577-.242-2.949-.486-5.95-.67-9.016-4.391,2.098-8.441,4.803-12.058,8.004,2.251,9.074,5.072,18,8.467,26.705l-.002-.002Z"/>
|
||||
<path class="cls-1" d="M96.776,60.642c.246,3.002.489,6.065.673,9.19,2.892-4.491,6.14-8.733,9.939-12.541,4.04-4.05,8.704-7.543,13.716-10.335h-7.04c-6.335,0-12.406,1.147-18.024,3.245.185,3.551.457,7.037.737,10.443h0Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-1" d="M110.373,60.373c-4.48,4.493-8.183,9.703-11.459,15.246,8.014-2.166,16.174-3.708,24.408-4.619v-20.35c-4.735,2.616-9.141,5.904-12.949,9.723Z"/>
|
||||
<path class="cls-1" d="M95.934,80.957c-1.595,3.005-3.102,6.07-4.565,9.141,8.126-.279,16.163-.578,24.191-.878,2.32-.086,4.641-.173,6.963-.26.517-2.989.8-6.059.8-9.198v-4.395c-9.173,1.037-18.249,2.873-27.124,5.513l-.265.08v-.002Z"/>
|
||||
<path class="cls-1" d="M72.847,124.067c-2.333,3.089-4.882,5.996-7.71,8.639h6c8.762,0,17.013-2.202,24.264-6.074-7.627-1.82-15.281-2.694-22.555-2.567h.001Z"/>
|
||||
<path class="cls-1" d="M106.33,110.057c-8.522,0-16.678-.296-24.308-.671-1.881,3.546-3.895,7.026-6.104,10.357,8.016.16,16.384,1.432,24.63,3.765,5.256-3.645,9.816-8.243,13.452-13.543-2.582.057-5.146.093-7.67.093Z"/>
|
||||
<path class="cls-1" d="M116.693,105.553c2.102-3.813,3.754-7.913,4.88-12.234-1.957.074-3.914.147-5.87.22-8.765.328-17.534.655-26.428.956-.156.333-.313.668-.468,1l-1.197,2.551c-1.11,2.361-2.248,4.736-3.428,7.099,10.083.47,21.057.763,32.513.407h-.001Z"/>
|
||||
</g>
|
||||
<path class="cls-2" d="M177.362,82.924c-.227-.144-.465-.287-.712-.432l-13.313-7.752c-.905-20.777-7.433-38.901-17.945-49.778-3.052-3.169-6.288-5.824-9.618-7.891l-4.12-2.566,9.053-3.31-3.902-11.196-32.089,8.172,17.576,29.751,9.693-6.675-5.823-8.703c3.881,2.557,8.848,6.623,8.871,6.637,11.384,9.112,17.014,25.23,17.717,50.721l.012.472,19.982,11.56-19.982,11.56-.012.471c-.955,34.671-10.778,52.131-32.773,58.363l-2.254.426.053,4.97-.057,5.046.974-.167c4.819-.825,8.19-1.825,8.305-1.861,6.062-1.601,12.595-5.801,18.39-11.82,10.511-10.666,17.042-28.791,17.95-49.781l20.629-11.301v-11.065l-6.603-3.852Z"/>
|
||||
<path class="cls-2" d="M52.834,152.219l-.851.593,7.027,9.689c-4.238-2.748-8.916-6.59-9.888-7.431l-.037-.036-.06-.083-.092-.074c-11.384-9.112-17.013-25.229-17.717-50.721l-.013-.471-19.982-11.56,19.982-11.56.013-.472c.955-34.671,10.778-52.131,32.773-58.363l2.255-.425-.054-4.969.057-4.775-.925.101c-4.826.522-8.255,1.62-8.356,1.654-6.062,1.601-12.595,5.801-18.388,11.82-10.511,10.664-17.042,28.791-17.952,49.782L0,86.218v11.066l6.603,3.852c.227.144.463.288.711.432l13.313,7.752c.905,20.777,7.435,38.901,17.946,49.779,3.024,3.133,6.201,5.749,9.407,7.754l5.13,3.587-8.442,2.336,3.904,11.198,29.159-6.087-16.055-31.75-8.84,6.083h-.001Z"/>
|
||||
<path class="cls-2" d="M662.144,57.197h12.867v109.694h-12.867v-38.28h-.322c-4.825,7.077-12.546,12.867-26.059,12.867-20.266,0-36.672-16.245-36.672-43.267s16.406-43.267,36.672-43.267c13.513,0,21.234,6.112,26.059,12.385h.322v-10.133ZM637.372,130.219c15.283,0,24.772-12.707,24.772-32.008s-9.49-32.008-24.772-32.008c-16.888,0-25.413,15.923-25.413,32.008s8.525,32.008,25.413,32.008Z"/>
|
||||
<path class="cls-2" d="M561.725,54.945c14.958,0,27.504,8.203,27.504,27.021v57.26h-12.867v-54.043c0-10.616-4.986-18.979-17.854-18.979-14.476,0-24.126,9.168-24.126,21.875v51.148h-12.867V57.197h12.867v9.811h.322c4.343-5.79,13.028-12.063,27.021-12.063Z"/>
|
||||
<rect class="cls-2" x="492.568" y="57.199" width="12.867" height="82.03"/>
|
||||
<path class="cls-2" d="M468.997,86.792c-.643-9.007-6.755-20.588-23.161-20.588-19.462,0-26.861,16.567-26.861,32.008s7.399,32.008,26.861,32.008c16.567,0,22.518-11.42,23.161-21.231h12.867c-1.126,18.658-15.763,32.49-36.029,32.49-23.966,0-39.728-17.532-39.728-43.267s15.763-43.267,39.728-43.267c20.266,0,34.099,13.511,36.029,31.847h-12.867Z"/>
|
||||
<path class="cls-2" d="M332.383,130.219c16.406,0,22.518-10.616,23.965-16.245h12.867c-4.182,15.28-15.923,27.504-36.35,27.504-24.931,0-40.05-17.532-40.05-43.267,0-27.343,15.441-43.267,39.406-43.267,25.735,0,38.28,17.532,38.28,46.644h-64.819c0,14.797,8.846,28.63,26.7,28.63ZM332.223,66.204c-15.441,0-26.539,10.937-26.539,24.126h51.952c0-13.189-9.972-24.126-25.413-24.126Z"/>
|
||||
<path class="cls-2" d="M284.574,54.945c3.056,0,5.308.161,7.56.483v12.867h-.322c-17.854-2.895-30.238,9.651-30.238,27.182v43.749h-12.867V57.197h12.867v16.245h.322c4.343-10.776,10.937-18.497,22.679-18.497Z"/>
|
||||
<circle class="cls-2" cx="499.241" cy="39.864" r="8.499"/>
|
||||
<g>
|
||||
<circle class="cls-2" cx="388.676" cy="77.162" r="7.895"/>
|
||||
<circle class="cls-2" cx="388.676" cy="119.262" r="7.895"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 448 KiB |