This commit is contained in:
Philippe Torrel
2026-11-03 14:52:15 +01:00
parent ed2c0b9906
commit f475f40350
104 changed files with 12459 additions and 4509 deletions
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
@@ -0,0 +1,10 @@
# Frontend Class: Debugging Product Sales Table Exercise
Install the dependencies and devDependencies and start the server.
```bash
npm install
npm run dev
```
This will start the server on http://localhost:5173
@@ -0,0 +1,44 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" />
<title>Product Sales Table</title>
</head>
<body>
<div id="root"></div>
<div class="container py-5">
<div class="alert alert-primary" role="alert">
<h2>Übung 15: Produkt-Verkaufstabelle</h2>
<p>
Es handelt sich um eine einfache React-App, die eine Liste der Produktverkäufe in einer Tabelle anzeigt, die
von einer API aufgerufen wird. Um die Übung zu vereinfachen, verwenden wir im Projekt lokale Daten, die aber
eine externe API anregen.
</p>
<p>
Du wirst jedoch feststellen, dass beim Starten der App sofort eine leere Seite mit Fehlern in der Konsole
angezeigt wird. Du kannst die Fehler überprüfen, indem du die Konsole öffnest. Die Aufgabe lautet wie folgt:
</p>
<ol class="list">
<li>
Untersuche die Fehler und debugge die Anwendung nur mit dem VSCode-Debugger. Das heißt, du darfst vorher
keine Dateien bearbeiten. Verwende das Variablenfeld im Debugger, um die Variablen zu bearbeiten.
</li>
<li>
Nachdem du die Anwendung erfolgreich debuggt hast, behebe den Fehler, indem du die Funktion fetchSalesData
änderst. Ändere nicht die Datendatei selbst. Das ist eine gute Übung, wenn du eine API aufrufst, die
manchmal fehlerhafte Daten zurückgibt.
</li>
</ol>
</div>
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
@@ -0,0 +1,28 @@
{
"name": "product-sales-table",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"@vitejs/plugin-react": "^6.1.0",
"eslint": "^10.8.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.4",
"typescript": "^6.0.3",
"vite": "^8.2.2"
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,42 @@
import { useState, useEffect } from 'react';
import Table from './components/Table';
function App() {
const [salesData, setSalesData] = useState(null);
const fetchSalesData = async () => {
const response = await fetch('./src/lib/db/sales.json');
const data = await response.json();
setSalesData(data);
};
useEffect(() => {
fetchSalesData();
}, []);
return (
<main>
<div className="wrapper">
<div className="header-container">
<div className="header-title-wrapper">
<h1 className="title">Product Sales</h1>
<p className="subtitle">
A table of product sales data for the current month.
</p>
</div>
</div>
<div className="table-wrapper">
<div className="table-container">
<div className="table-box">
{salesData && <Table salesData={salesData} />}
</div>
</div>
</div>
</div>
</main>
);
}
export default App;
@@ -0,0 +1,27 @@
import TableRow from './TableRow';
import { Product } from '../lib/datatypes';
const Table = ({ salesData }: { salesData: Product[] }) => {
return (
<table className="divide-y divide-gray-300">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Product</th>
<th scope="col">Code</th>
<th scope="col">Quantity</th>
<th scope="col">Price</th>
<th scope="col">Profit</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{salesData.map((product) => (
<TableRow key={product.id} product={product} />
))}
</tbody>
</table>
);
};
export default Table;
@@ -0,0 +1,25 @@
import { Product } from '../lib/datatypes';
const TableRow = ({ product }: { product: Product }) => {
const {
id,
product: productName,
productCode,
quantity,
price,
profit,
} = product;
return (
<tr>
<td className="id-column">{id}</td>
<td className="product-info-column">{productName}</td>
<td className="product-info-column">{productCode}</td>
<td className="product-num-column">{quantity}</td>
<td className="product-num-column">${price.toFixed(2)}</td>
<td className="product-num-column">${profit.toFixed(2)}</td>
</tr>
);
};
export default TableRow;
@@ -0,0 +1,187 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
*,
html,
body {
margin: 0;
padding: 0;
color: #171717;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
button {
border: none;
}
.screen-reader-text {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* ===== App Component ===== */
main {
max-width: 80rem; /* 1280px */
margin: 0 auto;
padding: 3rem 0.75rem; /* 48px 12px */
}
.wrapper {
padding: 0 1rem; /* 0 16px */
}
.header-container {
display: block;
}
.header-title-wrapper {
display: block;
}
.title {
font-size: 1rem; /* 16px */
line-height: 1.5rem; /* 24px */
line-height: 1.5rem; /* 24px */
font-weight: 600;
color: #111827;
}
.subtitle {
margin-top: 0.5rem; /* 8px */
font-size: 0.875rem; /* 14px */
line-height: 1.25rem; /* 20px */
color: #374151;
}
.table-wrapper {
display: flow-root;
margin-top: 2rem; /* 32px */
}
.table-container {
margin: -0.5rem -1rem; /* -8px -16px */
overflow-x: auto;
}
.table-box {
display: inline-block;
min-width: 100%;
padding: 0.5rem 0 /* 8px 0 */;
vertical-align: middle;
}
/* ===== Table Component ===== */
table {
min-width: 100%;
border-collapse: collapse;
}
th {
white-space: nowrap;
padding-top: 0.875rem /* 14px */;
padding-bottom: 0.875rem /* 14px */;
padding-left: 1rem /* 16px */;
padding-right: 0.75rem /* 12px */;
text-align: left;
font-weight: 600;
font-size: 0.875rem /* 14px */;
line-height: 1.25rem /* 20px */;
color: #111827;
}
tbody {
background-color: white;
}
thead tr {
border-bottom: 1px solid #d1d5db;
}
tbody tr {
border-bottom: 1px solid #e5e7eb;
}
.id-column {
white-space: nowrap;
padding-top: 0.5rem /* 8px */;
padding-bottom: 0.5rem /* 8px */;
padding-left: 1rem /* 16px */;
padding-right: 0.75rem /* 12px */;
font-size: 0.875rem /* 14px */;
line-height: 1.25rem /* 20px */;
color: #6b7280;
}
.product-info-column {
white-space: nowrap;
padding: 0.5rem /* 8px */;
font-size: 0.875rem /* 14px */;
line-height: 1.25rem /* 20px */;
font-weight: 500;
color: #111827;
}
.product-num-column {
white-space: nowrap;
padding: 0.5rem /* 8px */;
font-size: 0.875rem /* 14px */;
line-height: 1.25rem /* 20px */;
color: #6b7280;
}
/* ===== Breakpoints ===== */
@media (min-width: 640px) {
.wrapper {
padding: 0 1.5rem; /* 0 24px */
}
.header-container {
display: flex;
align-items: center;
}
.header-title-wrapper {
flex: 1 1 auto;
}
.table-container {
margin: -0.5rem -1.5rem; /* -8px -24px */
}
.table-box {
padding: 0.5rem 1.5rem /* 8px 24px */;
}
th {
padding-left: 0px;
}
.id-column {
padding-left: 0px;
}
}
@media (min-width: 1024px) {
.wrapper {
padding: 0 2rem; /* 0 32px */
}
.table-container {
margin: -0.5rem -2rem; /* -8px -32px */
}
.table-box {
padding: 0.5rem 2rem /* 8px 32px */;
}
}
@@ -0,0 +1,8 @@
export interface Product {
id: number;
product: string;
productCode: string;
quantity: number;
price: number;
profit: number;
}
@@ -0,0 +1,115 @@
[
{
"id": 1,
"product": "Laptop",
"productCode": "LPT",
"quantity": 352,
"price": 1200.0,
"profit": 200.0
},
{
"id": 2,
"product": "Smartphone",
"productCode": "SPH",
"quantity": 500,
"price": 800.0,
"profit": 123.0
},
{
"id": 3,
"product": "Headphones",
"productCode": "HDP",
"quantity": 432,
"price": 150.0,
"profit": 50.0
},
{
"id": 4,
"product": "Keyboard",
"productCode": "KBD",
"quantity": 431,
"price": 75.0,
"profit": 25.0
},
null,
{
"id": 6,
"product": "Monitor",
"productCode": "MNT",
"quantity": 234,
"price": 300.0,
"profit": 50.0
},
{
"id": 7,
"product": "Tablet",
"productCode": "TBL",
"quantity": 68,
"price": 350.0,
"profit": 100.0
},
{
"id": 8,
"product": "Printer",
"productCode": "PRT",
"quantity": 642,
"price": 200.0,
"profit": 50.0
},
{
"id": 9,
"product": "Webcam",
"productCode": "WCM",
"quantity": 123,
"price": 90.0,
"profit": 20.0
},
{
"id": 10,
"product": "Desk Lamp",
"productCode": "DLP",
"quantity": 1921,
"price": 30.0,
"profit": 5.0
},
{
"id": 11,
"product": "Chair",
"productCode": "CHR",
"quantity": 4854,
"price": 120.0,
"profit": 20.0
},
{
"id": 12,
"product": "External Hard Drive",
"productCode": "EHD",
"quantity": 483,
"price": 100.0,
"profit": 20.0
},
{
"id": 13,
"product": "USB-C Adapter",
"productCode": "USC",
"quantity": 10239,
"price": 19.99,
"profit": 5.0
},
{
"id": 14,
"product": "Smart Watch",
"productCode": "SWT",
"quantity": 483,
"price": 199.99,
"profit": 50.0
},
{
"id": 15,
"product": "Speakers",
"productCode": "SPK",
"quantity": 394,
"price": 85.0,
"profit": 20.0
}
]
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})