Files
perinco-CRM/src/components/ui/AvatarPicker.tsx
T
Sascha WolfandClaude Opus 5 8b7ce448a6 fix(ui): echte Umlaute in allen sichtbaren Texten
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>
2026-08-11 09:37:24 +02:00

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>
);
};