first commit
This commit is contained in:
639
README.md
Normal file
639
README.md
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
# DHBW 2025: Web Engineering Prüfungsleistung
|
||||||
|
**Maximale Punktzahl: 100 Punkte**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Grundanforderungen (25 Punkte)
|
||||||
|
|
||||||
|
### Projektgrundlagen (15 Punkte)
|
||||||
|
- **Funktionsfähige Anwendung** (8 Punkte)
|
||||||
|
- Webapp oder Backend-Anwendung läuft fehlerfrei
|
||||||
|
- Kernfunktionalitäten sind in der Projektdokumentation beschrieben und testbar
|
||||||
|
- **Repository** (4 Punkte)
|
||||||
|
- GitHub/GitLab/CodeBerg Repository ist (öffentlich) zugänglich
|
||||||
|
- Commit-Historie zeigt kontinuierliche Entwicklung
|
||||||
|
- **Projektdokumentation** (3 Punkte)
|
||||||
|
- Aussagekräftiger Projekttitel und Beschreibung
|
||||||
|
- README.md mit Installation, Verwendung und Technologie-Stack
|
||||||
|
- Kurze Erklärung der Hauptfunktionen am Anfang relevanter Dateien
|
||||||
|
|
||||||
|
### Präsentation (20 Punkte)
|
||||||
|
- **Elevator Pitch** (5 Punkte)
|
||||||
|
- Maximal 1 Minute
|
||||||
|
- Welches Problem wird gelöst und warum wurde es vorher noch nicht gelöst?
|
||||||
|
- Zielgruppe definiert
|
||||||
|
- **Technische Präsentation** (5 Punkte)
|
||||||
|
- PowerPoint/Folien mit Technologie-Stack
|
||||||
|
- Live-Demo der Anwendung
|
||||||
|
- **Projektevolution:** (5 Punkte)
|
||||||
|
Jedes Projekt verläuft unterschiedlich.
|
||||||
|
Sofern es zutrifft, beschreibt die unterschiedlichen Phasen des Projekts.
|
||||||
|
*Wie weit seid ihr gekommen? Wo waren die Stolpersteine? Wie könnte sich eure Anwendung bei fortgesetzter Arbeit entwickeln?*
|
||||||
|
|
||||||
|
0. (Paper) Prototype
|
||||||
|
1. Make it work (PoC)
|
||||||
|
2. Make it right (MvP)
|
||||||
|
3. Make it fast (Production)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Clean Code Prinzipien (35 Punkte)
|
||||||
|
|
||||||
|
### Projektstruktur (10 Punkte)
|
||||||
|
- **Ordnerstruktur** (5 Punkte)
|
||||||
|
```
|
||||||
|
projekt/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ # React Components / UI
|
||||||
|
│ ├── config/ # Konfigurationen, Hard-coded Strings etc.
|
||||||
|
│ ├── services/ # Separitiere für-sich-stehende Funktionen Klassen, Clients etc.
|
||||||
|
│ ├── utils/ # Hilfsfunktionen
|
||||||
|
│ ├── main.css # Haupteinstieg in CSS Klassen, Variablen, Imports etc.
|
||||||
|
│ ├── main.js # Die eigentliche Appliktion
|
||||||
|
│ └── index.js # Haupteinstieg in die gesamte Applikation
|
||||||
|
├── public/ # Statische Assets, Ressourcen, Bilder etc.
|
||||||
|
├── tests/ # Test-Dateien
|
||||||
|
├── .gitignore
|
||||||
|
├── package.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
- Logische Trennung von Funktionalitäten
|
||||||
|
- Konsistente Benennung
|
||||||
|
|
||||||
|
- **Datei-Organisation** (5 Punkte)
|
||||||
|
- Ein Konzept pro Datei (Single Responsibility)
|
||||||
|
- Konsistente Namenskonventionen: `kebab-case` für Dateien, `camelCase` für Variablen, `PascalCase` für Komponenten/Klassen
|
||||||
|
|
||||||
|
### Code-Qualität (15 Punkte)
|
||||||
|
|
||||||
|
#### KISS Prinzip (5 Punkte)
|
||||||
|
|
||||||
|
**❌ Nicht so einfach:**
|
||||||
|
```javascript
|
||||||
|
const processData = (d) => d.filter(x => x.status === 'active').map(x => ({...x, processed: true})).reduce((acc, curr) => acc + curr.value, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Einfach:**
|
||||||
|
```javascript
|
||||||
|
function calculateActiveTotal(data) {
|
||||||
|
const activeItems = data.filter(item => item.status === 'active');
|
||||||
|
const processedItems = activeItems.map(item => ({
|
||||||
|
...item,
|
||||||
|
processed: true
|
||||||
|
}));
|
||||||
|
return processedItems.reduce((total, item) => total + item.value, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Nicht so einfach:**
|
||||||
|
```javascript
|
||||||
|
const u = users.find(u => u.id === id && u.active && !u.deleted) || null;
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Einfach:**
|
||||||
|
```javascript
|
||||||
|
function findActiveUser(users, userId) {
|
||||||
|
return users.find(user =>
|
||||||
|
user.id === userId &&
|
||||||
|
user.active &&
|
||||||
|
!user.deleted
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Beschreibende Namen (5 Punkte)
|
||||||
|
|
||||||
|
**❌ Schlecht:**
|
||||||
|
```javascript
|
||||||
|
let isActive = true;
|
||||||
|
let data = [];
|
||||||
|
function calc(x, y) { return x * y; }
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Gut:**
|
||||||
|
```javascript
|
||||||
|
let isMenuVisible = true;
|
||||||
|
let userProfiles = [];
|
||||||
|
function calculateTotalPrice(price, taxRate) { return price * taxRate; }
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Unklar und uneindeutig:**
|
||||||
|
```javascript
|
||||||
|
const btn = document.getElementById('btn');
|
||||||
|
let flag = false;
|
||||||
|
function process(items) { /* ... */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Sprechende Variablennamen:**
|
||||||
|
```javascript
|
||||||
|
const submitButton = document.getElementById('submit-btn');
|
||||||
|
let isFormValid = false;
|
||||||
|
function validateUserInput(formData) { /* ... */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Funktionsverantwortlichkeiten (5 Punkte)
|
||||||
|
|
||||||
|
**❌ Zu viele Verantwortlichkeiten:**
|
||||||
|
```javascript
|
||||||
|
function handleUser(userData) {
|
||||||
|
// Validiert Daten
|
||||||
|
if (!userData.email) return false;
|
||||||
|
// Speichert in DB
|
||||||
|
database.save(userData);
|
||||||
|
// Sendet Email
|
||||||
|
sendWelcomeEmail(userData.email);
|
||||||
|
// Updated UI
|
||||||
|
updateUserList();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Eine klare Verantwortlichkeit pro Funktion:**
|
||||||
|
```javascript
|
||||||
|
function validateUserData(userData) {
|
||||||
|
return userData.email && userData.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUser(userData) {
|
||||||
|
return database.save(userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWelcomeEmail(email) {
|
||||||
|
return emailService.send(email, 'welcome');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kommentierung & Dokumentation (5 Punkte)
|
||||||
|
**JSDoc ist optional und der beste Code muss nicht kommentiert werden.**
|
||||||
|
|
||||||
|
**❌ Über-/Unterkommentiert:**
|
||||||
|
```javascript
|
||||||
|
// Erhöht i um 1
|
||||||
|
i++;
|
||||||
|
// Checked ob user existiert
|
||||||
|
if (user) {
|
||||||
|
// Macht irgendwas
|
||||||
|
doSomething();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Sinnvoll kommentiert:**
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Handles user authentication and session management
|
||||||
|
*
|
||||||
|
* OR
|
||||||
|
*
|
||||||
|
* This file manages JWT tokens and user permissions
|
||||||
|
*/
|
||||||
|
|
||||||
|
function calculateCompoundInterest(principal, rate, time) {
|
||||||
|
// Using compound interest formula: A = P(1 + r/n)^(nt)
|
||||||
|
return principal * Math.pow((1 + rate), time);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Unterkommentiert:**
|
||||||
|
```javascript
|
||||||
|
function x(a,b,c,d,e) {
|
||||||
|
return a*b+c-d/e;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Mit JSDoc dokumentiert:**
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Calculates shipping cost based on weight, distance and priority
|
||||||
|
* @param {number} weight - Package weight in kg
|
||||||
|
* @param {number} distance - Delivery distance in km
|
||||||
|
* @param {boolean} isPriority - Priority delivery flag
|
||||||
|
* @returns {number} Total shipping cost
|
||||||
|
*/
|
||||||
|
function calculateShippingCost(weight, distance, priority) {
|
||||||
|
const baseCost = weight * 2.5;
|
||||||
|
const distanceCost = distance * 0.1;
|
||||||
|
const priorityMultiplier = priority ? 1.5 : 1;
|
||||||
|
return (baseCost + distanceCost) * priorityMultiplier;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Live-Demo (10 Punkte)
|
||||||
|
- **Fehlerfreie Demonstration** (10 Punkte)
|
||||||
|
- Anwendung läuft während der Präsentation stabil
|
||||||
|
- Alle Kernfunktionen werden erfolgreich vorgeführt
|
||||||
|
- Ungefähr 5 Minuten Demo-Zeit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Accessibility & UX (25 Punkte)
|
||||||
|
|
||||||
|
### Web Accessibility (15 Punkte)
|
||||||
|
|
||||||
|
#### Semantisches HTML (5 Punkte)
|
||||||
|
|
||||||
|
**❌ Nicht semantisch:**
|
||||||
|
```html
|
||||||
|
<div class="header">
|
||||||
|
<div class="nav">
|
||||||
|
<div class="nav-item">Home</div>
|
||||||
|
<div class="nav-item">About</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="title">Welcome</div>
|
||||||
|
<div class="text">Description...</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Semantisch korrekt:**
|
||||||
|
```html
|
||||||
|
<header>
|
||||||
|
<nav aria-label="Hauptnavigation">
|
||||||
|
<ul>
|
||||||
|
<li><a href="#home">Home</a></li>
|
||||||
|
<li><a href="#about">About</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<section>
|
||||||
|
<h1>Welcome</h1>
|
||||||
|
<p>Description...</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ARIA Labels & Alt-Texte (5 Punkte)
|
||||||
|
|
||||||
|
**❌ Schlecht:**
|
||||||
|
```html
|
||||||
|
<img src="chart.png" alt="chart">
|
||||||
|
<button onclick="closeMenu()">×</button>
|
||||||
|
<input type="email" required>
|
||||||
|
<div>We don't send spam</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Gut:**
|
||||||
|
```html
|
||||||
|
<img src="chart.png" alt="Verkaufszahlen 2024: 40% Steigerung gegenüber Vorjahr">
|
||||||
|
<button aria-label="Menü schließen" onclick="closeMenu()">×</button>
|
||||||
|
<input type="email" aria-describedby="email-help" required>
|
||||||
|
<div id="email-help">Wir versenden keine Spam-Mails</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tastaturnavigation (5 Punkte)
|
||||||
|
- Alle interaktiven Elemente per Tab erreichbar
|
||||||
|
- Sichtbarer Focus-Indikator
|
||||||
|
- Logische Tab-Reihenfolge
|
||||||
|
|
||||||
|
**❌ Nicht tastaturzugänglich:**
|
||||||
|
```html
|
||||||
|
<div class="clickable">🍔</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
div.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Tastaturzugänglich:**
|
||||||
|
```css
|
||||||
|
div.clickable:focus {
|
||||||
|
outline: 2px solid #007acc;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="clickable" tabindex="0" aria-label="Menü öffnen">🍔</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responsive Design (10 Punkte)
|
||||||
|
|
||||||
|
#### Mobile First Approach (5 Punkte - mindestens 1 Breakpoint)
|
||||||
|
```css
|
||||||
|
/* Mobile First */
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Flexbox/Grid Layout (5 Punkte)
|
||||||
|
|
||||||
|
**❌ Veraltete Layouts:**
|
||||||
|
```css
|
||||||
|
.container {
|
||||||
|
float: left;
|
||||||
|
width: 33.33%;
|
||||||
|
}
|
||||||
|
.clearfix::after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Moderne Layouts:**
|
||||||
|
```css
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Erweiterte Technologien & Innovation (15 Punkte)
|
||||||
|
|
||||||
|
### Plattformunabhängigkeit (5 Punkte)
|
||||||
|
- Funktioniert auf macOS, Windows, GNU/Linux
|
||||||
|
- Browser-Kompatibilität (Chrome, Firefox, Safari, Edge)
|
||||||
|
|
||||||
|
**❌ Plattformspezifisch:**
|
||||||
|
```javascript
|
||||||
|
// Nur Chrome
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Plattformunabhängig:**
|
||||||
|
|
||||||
|
In aller Regel sind solche sog. Polyfills kaum noch eigenhändig zu implementieren.
|
||||||
|
Nichtsdestotrotz gilt es die Plattformunabhängig zu prüfen und ggf. Alternativen für die Implementierung zu finden wie im folgenden Beispiel für die Zwischenablage:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Fallback für ältere Browser
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
return navigator.clipboard.writeText(text);
|
||||||
|
} else {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entwicklungs-Tools (10 Punkte)
|
||||||
|
|
||||||
|
#### Package Management (4 Punkte)
|
||||||
|
|
||||||
|
Sämtliche nodeJS/JavaScript Projekte sollten eine `package.json` beinhalten.
|
||||||
|
`npm init` und ein Dialog öffnet sich.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-project",
|
||||||
|
"scripts": {
|
||||||
|
"start": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"test": "jest",
|
||||||
|
"lint": "eslint src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^4.0.0",
|
||||||
|
"jest": "^29.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Build Tools & Optimierung (6 Punkte)
|
||||||
|
- Vite, Webpack, oder ähnliche Tools
|
||||||
|
- Optimierte Produktions-Builds
|
||||||
|
- Debugger und unnötige console.log()'s entfernen
|
||||||
|
- Code-Splitting
|
||||||
|
- Nur Teile statt gesamte Bibliotheken importieren
|
||||||
|
- Bspw. PostCSS für Entfernung von überschüssigem CSS
|
||||||
|
|
||||||
|
**❌ Entwicklungs-Build:**
|
||||||
|
```javascript
|
||||||
|
// Unminified, alle Dependencies geladen
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
// Debugging
|
||||||
|
console.log('Debug info:', data);
|
||||||
|
debugger;
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Produktions-Build:**
|
||||||
|
```javascript
|
||||||
|
// Minified, nur benötigte Funktionen
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
// Debug-Code entfernt in Production
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zusätzliche Features (Bonus: bis zu 10 Punkte)
|
||||||
|
**Wähle mindestens 2 Bereiche:**
|
||||||
|
|
||||||
|
#### API Integration (5 Punkte)
|
||||||
|
|
||||||
|
**❌ Ohne Error Handling:**
|
||||||
|
```javascript
|
||||||
|
async function getUser(id) {
|
||||||
|
const response = await fetch(`/api/users/${id}`);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Mit Error Handling:**
|
||||||
|
```javascript
|
||||||
|
async function fetchUserData(userId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/users/${userId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Benutzerdaten:', error);
|
||||||
|
throw new Error('Benutzerdaten konnten nicht geladen werden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Local Storage / State Management (5 Punkte)
|
||||||
|
|
||||||
|
**❌ Ohne Error Handling:**
|
||||||
|
```javascript
|
||||||
|
function saveData(data) {
|
||||||
|
localStorage.setItem('data', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
function loadData() {
|
||||||
|
return JSON.parse(localStorage.getItem('data'));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Mit Error Handling:**
|
||||||
|
```javascript
|
||||||
|
function saveUserPreferences(prefs) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('userPrefs', JSON.stringify(prefs));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Local Storage nicht verfügbar:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadUserPreferences() {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem('userPrefs');
|
||||||
|
return data ? JSON.parse(data) : getDefaultPreferences();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Fehler beim Laden der Einstellungen:', error);
|
||||||
|
return getDefaultPreferences();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Progressive Web App Features (5 Punkte)
|
||||||
|
- Service Worker für Offline-Funktionalität
|
||||||
|
- Manifest.json für App-Installation
|
||||||
|
- Caching-Strategien
|
||||||
|
|
||||||
|
**❌ Ohne PWA:**
|
||||||
|
```html
|
||||||
|
<!-- Nur normale Website -->
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Mit PWA Features:**
|
||||||
|
```html
|
||||||
|
<!-- manifest.json -->
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<!-- Service Worker -->
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/sw.js');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Testing (10 Punkte)
|
||||||
|
|
||||||
|
Von Vornherein mitgedachte Tests helfen beim Code-Splitting.
|
||||||
|
Um beim Testen jene Funktionen eindeutig referenzieren zu können, hilft es sie in einzelne Dateien zu verteilen und Ordnern zu gruppieren.
|
||||||
|
|
||||||
|
**❌ Ohne Tests:**
|
||||||
|
```javascript
|
||||||
|
// Keine Tests vorhanden
|
||||||
|
function calculateTotal(price, tax) {
|
||||||
|
return price + (price * tax);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Mit Tests:**
|
||||||
|
```javascript
|
||||||
|
// calculate.js
|
||||||
|
export function calculateTotal(price, tax) {
|
||||||
|
if (typeof price !== 'number' || typeof tax !== 'number') {
|
||||||
|
throw new Error('Price and tax must be numbers');
|
||||||
|
}
|
||||||
|
return price + (price * tax);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate.test.js
|
||||||
|
import { calculateTotal } from './calculate.js';
|
||||||
|
|
||||||
|
test('calculateTotal should return correct sum', () => {
|
||||||
|
expect(calculateTotal(100, 0.19)).toBeCloseTo(119);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calculateTotal should throw error for invalid input', () => {
|
||||||
|
expect(() => calculateTotal('100', 0.19)).toThrow();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Responsive Design Erweitert (5 Punkte - 2 oder mehr Breakpoints)
|
||||||
|
```css
|
||||||
|
/* Mobile */
|
||||||
|
.grid { display: block; }
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large Desktop */
|
||||||
|
@media (min-width: 1440px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Security Features (5 Punkte)
|
||||||
|
|
||||||
|
**❌ Unsicher:**
|
||||||
|
```javascript
|
||||||
|
// XSS anfällig
|
||||||
|
element.innerHTML = userInput;
|
||||||
|
// Unvalidierte Eingabe
|
||||||
|
function search(query) {
|
||||||
|
return database.query(`SELECT * FROM users WHERE name = '${query}'`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Sicher:**
|
||||||
|
```javascript
|
||||||
|
// XSS-sicher durch HTML-Escaping
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
element.innerHTML = escapeHtml(userInput);
|
||||||
|
|
||||||
|
// SQL-Injection sicher
|
||||||
|
function search(query) {
|
||||||
|
const sanitizedQuery = query.replace(/[^\w\s]/gi, '');
|
||||||
|
return database.query('SELECT * FROM users WHERE name = ?', [sanitizedQuery]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Abgabe
|
||||||
|
|
||||||
|
- **Deadline:** 20.07.2025
|
||||||
|
- **Format:** GitHub/GitLab Repository Link + Live-Demo URL (falls gehostet)
|
||||||
|
- **Präsentation:** 11.07.2025 - 10-15 Minuten pro Gruppe + ~5 Minuten Demo
|
||||||
|
|
||||||
|
## Tipps
|
||||||
|
|
||||||
|
1. **Beginnt früh** mit der Eingrenzung des Funktionsumfangs
|
||||||
|
2. **Testet eure Anwendung** auf verschiedenen Geräten, Betriebssystemen und Browsern
|
||||||
|
3. **Fragt bei Unklarheiten** rechtzeitig nach
|
||||||
|
4. **Fokussiert euch** auf Qualität statt Quantität
|
||||||
|
5. **Behaltet Barrieren im Blick** und vermeidet sie rechtzeitig
|
||||||
|
5. **Verwendet gute Struktur** um eure Code-Beispiele zu organisieren
|
||||||
Reference in New Issue
Block a user