Menüs, Dialoge, Schaltflächen, Platzhalter, Hinweise und die
Fehlermeldungen der API standen in Ersatzschreibweise (oe/ae/ue). Sie
erscheinen als Toast oder Beschriftung direkt in der Oberfläche und
gehören damit richtig geschrieben; sechs sichtbare Texte mit ss statt ß
sind gleich mit erledigt ("Schließen", "Straße", "zu groß").
Unangetastet bleibt bewusst:
- Kommentare im Quelltext - die sind durchgängig ASCII, und der Diff wäre
dreimal so groß ohne Nutzen für den Benutzer
- Protokollzeilen (console.*) - die landen im journal, nicht in der App
- Datenbankwerte und interne Kennungen: 'uebernommen', 'empfaenger',
'vorschlaege', 'laeuft-bereits' stecken in Enums, SQL und
Zustandsvergleichen. Ein Umlaut darin hätte still falsche Ergebnisse
geliefert
- server/src/mail/extract.js - die Muster decken absichtlich beide
Schreibweisen ab: /gr(ü|ue)(ß|ss)e/
- scanner/utils/crypto.js - der Ersatzschlüssel darf sich nicht ändern,
sonst sind gespeicherte Portal-Passwörter nicht mehr lesbar
Geprüft mit einem Rundgang durch die Ansichten und Dialoge: sichtbarer
Text plus title/aria-label/placeholder enthalten keine
Ersatzschreibweisen mehr.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Avatar } from '../leads/leadUi';
|
|
import { useToast } from '../Toast';
|
|
import { compressImage, formatBytes } from '../../lib/imageCompress';
|
|
|
|
interface AvatarPickerProps {
|
|
/** Data-URL oder externe URL; '' = kein Bild. */
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
/** Fallback, solange kein Bild gesetzt ist. */
|
|
initials: string;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
removeLabel?: string;
|
|
}
|
|
|
|
/**
|
|
* Foto-Upload mit Vorschau - genutzt fuer Kontakte und Benutzer. Das Bild wird
|
|
* im Browser auf ein Quadrat verkleinert, bevor es hochgeladen wird; damit
|
|
* bleiben die Data-URLs in der Datenbank klein.
|
|
*/
|
|
export const AvatarPicker: React.FC<AvatarPickerProps> = ({
|
|
value,
|
|
onChange,
|
|
initials,
|
|
size = 'lg',
|
|
removeLabel = 'Foto entfernen',
|
|
}) => {
|
|
const { addToast } = useToast();
|
|
const [isBusy, setIsBusy] = useState(false);
|
|
|
|
const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
// Zuruecksetzen, damit dieselbe Datei nach einem Entfernen erneut gewaehlt
|
|
// werden kann.
|
|
event.target.value = '';
|
|
if (!file) return;
|
|
|
|
setIsBusy(true);
|
|
try {
|
|
const result = await compressImage(file);
|
|
onChange(result.dataUrl);
|
|
addToast(
|
|
`Foto übernommen (${formatBytes(result.sourceBytes)} → ${formatBytes(result.resultBytes)}).`,
|
|
'success'
|
|
);
|
|
} catch (err) {
|
|
addToast(err instanceof Error ? err.message : 'Das Bild konnte nicht verarbeitet werden.', 'error');
|
|
} finally {
|
|
setIsBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="group relative">
|
|
<Avatar src={value || null} initials={initials} size={size} />
|
|
<label
|
|
className={`absolute inset-0 flex cursor-pointer items-center justify-center rounded-xl bg-ink-950/45 text-[10px] font-extrabold uppercase tracking-wider text-white transition ${
|
|
isBusy ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
|
}`}
|
|
>
|
|
{isBusy ? (
|
|
<span className="h-5 w-5 animate-spin rounded-full border-2 border-white/40 border-t-white" />
|
|
) : (
|
|
'Ändern'
|
|
)}
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
disabled={isBusy}
|
|
className="hidden"
|
|
onChange={handleUpload}
|
|
/>
|
|
</label>
|
|
</div>
|
|
{value && !isBusy && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onChange('')}
|
|
className="text-[10px] font-bold uppercase tracking-wider text-ink-400 transition hover:text-red-600"
|
|
>
|
|
{removeLabel}
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|