CRUD

Typen definieren service.ts
export type Service = { id: string, customer: string, kind: "small" | "big", brand: string, numberplate: string, expense: number, important: boolean,}
export type NewService = Omit<Service, "id">Page beschreiben pages/ServicePage.tsx
import { useState } from "react"import { NewService, Service } from "../entities/Service";import { ServiceDisplay } from "../containers/ServiceDisplay";import { ServiceForm } from "../containers/ServiceForm";import { ThemeSwitch } from "../containers/ThemeSwitch";
export function ServicePage() { const [services, setServices] = useState<Service[]>([ { id: crypto.randomUUID(), customer: "Lucy Fernandez", brand: "Lada", expense: 3, kind: "small", numberplate: "ZH30294", important: false }, { id: crypto.randomUUID(), customer: "John Smith", brand: "Toyota", expense: 5, kind: "big", numberplate: "ABC123", important: true }, { id: crypto.randomUUID(), customer: "Emily Johnson", brand: "Honda", expense: 4, kind: "small", numberplate: "XYZ987", important: false }, { id: crypto.randomUUID(), customer: "Michael Williams", brand: "Ford", expense: 6, kind: "small", numberplate: "DEF456", important: true }, { id: crypto.randomUUID(), customer: "Sophia Brown", brand: "Chevrolet", expense: 4, kind: "small", numberplate: "GHI789", important: false }, { id: crypto.randomUUID(), customer: "Daniel Miller", brand: "Volkswagen", expense: 7, kind: "small", numberplate: "JKL321", important: true }, { id: crypto.randomUUID(), customer: "Olivia Martinez", brand: "Nissan", expense: 5, kind: "small", numberplate: "MNO654", important: false }, { id: crypto.randomUUID(), customer: "James Taylor", brand: "BMW", expense: 8, kind: "big", numberplate: "PQR987", important: true }, { id: crypto.randomUUID(), customer: "Ava Garcia", brand: "Mercedes-Benz", expense: 7, kind: "small", numberplate: "STU246", important: false }, { id: crypto.randomUUID(), customer: "Ethan Anderson", brand: "Audi", expense: 6, kind: "small", numberplate: "VWX135", important: true } ]);
const [editService, setEditService] = useState<Service>();
const onSave = (service: Service | NewService) => { if ("id" in service) { setServices((prev) => prev.map((s) => s.id === service.id ? service : s)) setEditService(undefined) } else { setServices((prev) => [ ...prev, { ...service, id: crypto.randomUUID() } ]); } }
const onDelete = (id: string) => { setServices(services.filter((service) => service.id !== id)) }
const onUpdate = (id: string) => { setEditService(services.find((service) => service.id === id)) }
const onCancel = () => { setEditService(undefined) }
return <div className={editService ? "edit" : ""}> <ThemeSwitch /> <h2>Services</h2> <ServiceForm onSave={onSave} onCancel={onCancel} edit={editService} /> <ServiceDisplay data={services} onUpdate={onUpdate} onDelete={onDelete} /> </div>}Formular erstellen containers/ServiceForm.tsx
import { ChangeEvent, FormEvent, MouseEvent, useEffect, useState } from "react"import { NewService, Service } from "../entities/Service";
type ServiceFormProps = { onSave: (service: Service | NewService) => void, onCancel: () => void, edit?: Service}
const defaultService: Service | NewService = {brand: "", expense: 0, important: false, customer: "", numberplate: "", kind: "small"};
export function ServiceForm(props: ServiceFormProps) { const [service, setService] = useState<Service | NewService>( {brand: "", expense: 0, important: false, customer: "", numberplate: "", kind: "small"} );
const onCancel = (e: MouseEvent<HTMLButtonElement>) => { setService(defaultService); e.currentTarget.closest("form")?.reset() props.onCancel(); }
const onSubmit = (event: FormEvent<HTMLFormElement>) => { if(service) { event.preventDefault(); props.onSave(service); setService(defaultService); event.currentTarget.reset(); } }
const handleChange = (event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { setService((prev) => { let value: string | number | boolean = event.target.value; if(event.target.name === "expense") { value = parseInt(value); } if(event.target.name === "important") { value = !service.important; } // Gefährlich, weil ...prev die Typisierung ausschaltet. // Leider gibt es in TypeScript keine exact Types. // Siehe https://github.com/microsoft/TypeScript/issues/12936 return {...prev, [event.target.name]: value} }); }
useEffect(() => { if(props.edit) { setService(props.edit); } }, [props.edit]);
return <form onSubmit={onSubmit}> <label>Name<input type="text" name="customer" required maxLength={50} onChange={handleChange} value={service?.customer} /></label> <label>Marke<input type="text" name="brand" required onChange={handleChange} value={service?.brand} /></label> <label>Aufwand in Stunden<input type="number" name="expense" required min={1} max={20} onChange={handleChange} value={service?.expense || ""} /></label> <label>Serviceart<select name="kind" required onChange={handleChange} value={service?.kind} > <option value="small">Klein</option> <option value="big">Gross</option> </select></label> <label>Autonummer<input type="text" name="numberplate" required onChange={handleChange} value={service?.numberplate} pattern="^(AG|AR|AI|BL|BE|FR|GE|GL|GR|JU|LU|NE|NW|OW|SG|SH|SZ|SO|TG|TI|UR|VD|VS|ZG|ZH)[1-9]{1}[0-9]{0,6}$" /></label> <label>Dringlichkeit<input type="checkbox" name="important" onChange={handleChange} checked={service.important} /></label> <button type="submit">{props.edit ? "Ändern" : "Erstellen"}</button> {props.edit && <button type="reset" onClick={onCancel}>Abbrechen</button>} </form>}Anzeige ermöglichen containers/ServiceDisplay.tsx
import { useContext } from "react";import { Service } from "../entities/Service";import { ThemeContext } from "../contexts/ThemeContext";
type ServiceDisplayProp = { data: Service[]; onDelete: (id: string) => void; onUpdate: (id: string) => void;}
export function ServiceDisplay(props: ServiceDisplayProp) { const theme = useContext(ThemeContext); return <table className={theme.darkMode ? "dark" : ""}> <thead> <tr> <th>Name</th> <th>Marke</th> <th>Aufwand in Stunden</th> <th>Serviceart</th> <th>Autonummer</th> <th>Dringlichkeit</th> <th>Aktionen</th> </tr> </thead> <tbody> {props.data.map((service) => <tr key={service.numberplate}> <td>{service.customer}</td> <td>{service.brand}</td> <td>{service.expense}</td> <td>{service.kind}</td> <td>{service.numberplate}</td> <td>{service.important ? "✅" : "❌"}</td> <td> <button onClick={() => props.onUpdate(service.id)}>Edit</button> <button onClick={() => props.onDelete(service.id)}>Delete</button> </td> </tr>)} </tbody> </table>}Styles index.css
html { height: 100%;}
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; min-height: 100%; display: flex; flex-direction: column; align-items: center;}
h2 { text-align: center;}
input, select { box-sizing: border-box; display: block; width: 100%; margin-bottom: 0.5rem;
&:user-invalid { border: 2px solid #F00; }}
form { margin-bottom: 2rem;}
table { border-collapse: collapse;}
table.dark { background-color: #000; color: #FFF;}
table th { background-color: #EEE;}
td, th { padding: 0.3rem;}
.edit table { visibility: hidden;}contexts/ThemeContext.ts
import { createContext } from 'react';
type Theme = { darkMode: boolean; setDarkMode?: (value: boolean) => void;};
export const initialTheme: Theme = { darkMode: false };
export const ThemeContext = createContext<Theme>(initialTheme);