feat(frontend): bullet-editor fuer Projektaufgaben + Popup-Backdrop deaktiviert

- Modal.tsx + Drawer.tsx: onClick={onClose} vom Backdrop entfernt — alle
  Popups schliessen sich jetzt nur noch ueber Speichern/Abbrechen-Buttons
- ProjectModal.tsx: Textarea "Aufgaben" durch BulletEditor-Komponente
  ersetzt (Toolbar-Button toggled Aufzaehlungspunkt, Enter setzt Bullet
  auf naechster Zeile fort, leere Bullet-Zeile + Enter beendet die Liste)
- ExpertProfileTab.module.css: CSS fuer bulletEditor, bulletToolbar,
  bulletBtn, bulletEditorTextarea hinzugefuegt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-13 20:32:00 +01:00
parent 2078a90fba
commit a4013d4356
5 changed files with 201 additions and 7 deletions

View file

@ -6,6 +6,32 @@
--- ---
### Aenderungen 2026-03-13 (13): Experten-Profil Bullet-Editor fuer Aufgaben + Popup-Backdrop deaktiviert
#### Frontend
- `components/Modal.tsx``onClick={onClose}` vom Overlay-div entfernt: Popups schliessen sich nicht mehr durch Klick neben das Popup (betrifft alle Modal-Instanzen im Projekt)
- `components/Drawer.tsx` — Gleiches: Drawer schliesst sich nicht mehr durch Klick auf den Backdrop
- `profile/sections/ProjectModal.tsx` — Neue lokale `BulletEditor`-Komponente (keine externe Library): Toolbar mit "☰ Liste"-Button (toggled `• ` am Zeilenanfang ein/aus), automatische Aufzaehlungsfortsetzung beim Enter-Druck, leere Bullet-Zeile `• ` + Enter beendet die Liste; ersetzte die einfache `<textarea>` im Feld "Aufgaben"
- `profile/ExpertProfileTab.module.css` — Neue CSS-Klassen: `.bulletEditor`, `.bulletToolbar`, `.bulletBtn`, `.bulletEditorTextarea`, `.bulletBtn:hover`; Editor-Container mit `border + :focus-within`-Ring analog zu Input-Feldern; Textarea innerhalb des Editors hat keinen eigenen Rand (verhindert doppelten Rahmen)
#### TypeScript
- `npx tsc --noEmit` in packages/frontend: 0 Fehler
#### Deployment-Hinweis (Schritt 13)
- Rebuild + Restart: nur frontend (kein Backend-Aenderung)
---
### Aenderungen 2026-03-13 (12): Bug-Fix Sprachen im Experten-Profil
#### Backend (core-service)
- `core/expert-profile/dto/create-language.dto.ts``@IsIn` Validator um fehlende Werte erweitert: `Verhandlungssicher`, `Fliesend`, `Gut` waren nicht enthalten (Frontend sendete diese Werte → 400 Fehler); alle 10 Werte jetzt korrekt: `['Muttersprache', 'Verhandlungssicher', 'Fliesend', 'Gut', 'C2', 'C1', 'B2', 'B1', 'A2', 'A1']`
#### Deployment-Hinweis (Schritt 12)
- Rebuild + Restart: nur core-service
---
### Aenderungen 2026-03-13 (11): Kontakt-Detailseite Breite, Outlook-Daten-Sektion, Felder, Outlook-Push ### Aenderungen 2026-03-13 (11): Kontakt-Detailseite Breite, Outlook-Daten-Sektion, Felder, Outlook-Push
#### Backend (crm-service) #### Backend (crm-service)

View file

@ -40,7 +40,7 @@ export function Drawer({
if (!isOpen) return null; if (!isOpen) return null;
return createPortal( return createPortal(
<div className={styles.overlay} onClick={onClose}> <div className={styles.overlay}>
<div <div
className={styles.panel} className={styles.panel}
style={{ width }} style={{ width }}

View file

@ -32,7 +32,7 @@ export function Modal({ isOpen, onClose, title, children, maxWidth = '600px' }:
if (!isOpen) return null; if (!isOpen) return null;
return createPortal( return createPortal(
<div className={styles.overlay} onClick={onClose}> <div className={styles.overlay}>
<div <div
className={styles.container} className={styles.container}
style={{ maxWidth }} style={{ maxWidth }}

View file

@ -418,6 +418,67 @@
color: var(--color-error); color: var(--color-error);
} }
/* === Bullet Editor === */
.bulletEditor {
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
overflow: hidden;
transition: border-color 0.15s, box-shadow 0.15s;
}
.bulletEditor:focus-within {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-light);
}
.bulletToolbar {
display: flex;
gap: 0.25rem;
padding: 0.375rem 0.5rem;
background: var(--color-bg);
border-bottom: 1px solid var(--color-border);
}
.bulletBtn {
padding: 0.25rem 0.625rem;
font-size: 0.8125rem;
font-weight: 500;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--color-text-muted);
transition: background 0.1s, border-color 0.1s, color 0.1s;
line-height: 1.4;
font-family: inherit;
}
.bulletBtn:hover {
background: var(--color-primary-light);
border-color: var(--color-primary);
color: var(--color-primary);
}
.bulletEditor .bulletEditorTextarea {
display: block;
width: 100%;
border: none;
border-radius: 0;
box-shadow: none;
padding: 0.625rem 0.75rem;
font-size: 0.9375rem;
font-family: inherit;
resize: vertical;
background: transparent;
box-sizing: border-box;
}
.bulletEditor .bulletEditorTextarea:focus {
outline: none;
border: none;
box-shadow: none;
}
.checkboxRow { .checkboxRow {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -1,4 +1,4 @@
import { useState, useEffect, type FormEvent } from 'react'; import { useState, useEffect, useRef, useCallback, type FormEvent } from 'react';
import { Modal } from '../../components/Modal'; import { Modal } from '../../components/Modal';
import api from '../../api/client'; import api from '../../api/client';
import type { ExpertProject } from '../ExpertProfileTab'; import type { ExpertProject } from '../ExpertProfileTab';
@ -34,6 +34,113 @@ const INDUSTRIES = [
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const YEARS = Array.from({ length: 40 }, (_, i) => currentYear - i); const YEARS = Array.from({ length: 40 }, (_, i) => currentYear - i);
// Lightweight bullet-point editor — no external dependencies
function BulletEditor({
value,
onChange,
placeholder,
maxLength,
rows = 6,
}: {
value: string;
onChange: (val: string) => void;
placeholder?: string;
maxLength?: number;
rows?: number;
}) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const toggleBullet = useCallback(() => {
const ta = textareaRef.current;
if (!ta) return;
const start = ta.selectionStart;
const val = ta.value;
const lineStart = val.lastIndexOf('\n', start - 1) + 1;
const lineEnd = val.indexOf('\n', start);
const line = val.substring(lineStart, lineEnd === -1 ? val.length : lineEnd);
let newVal: string;
let newCursor: number;
if (line.startsWith('• ')) {
// Remove bullet from current line
newVal =
val.substring(0, lineStart) +
line.substring(2) +
val.substring(lineEnd === -1 ? val.length : lineEnd);
newCursor = Math.max(lineStart, start - 2);
} else {
// Add bullet to current line
newVal = val.substring(0, lineStart) + '• ' + val.substring(lineStart);
newCursor = start + 2;
}
onChange(newVal);
requestAnimationFrame(() => {
ta.selectionStart = ta.selectionEnd = newCursor;
ta.focus();
});
}, [onChange]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key !== 'Enter') return;
const ta = textareaRef.current;
if (!ta) return;
const start = ta.selectionStart;
const val = ta.value;
const lineStart = val.lastIndexOf('\n', start - 1) + 1;
const lineContent = val.substring(lineStart, start);
if (lineContent === '• ') {
// Empty bullet: remove it and exit list
e.preventDefault();
const newVal = val.substring(0, lineStart) + val.substring(start);
onChange(newVal);
requestAnimationFrame(() => {
ta.selectionStart = ta.selectionEnd = lineStart;
});
} else if (lineContent.startsWith('• ')) {
// Continue bullet list on new line
e.preventDefault();
const insert = '\n• ';
const newVal =
val.substring(0, start) + insert + val.substring(ta.selectionEnd);
onChange(newVal);
requestAnimationFrame(() => {
ta.selectionStart = ta.selectionEnd = start + insert.length;
});
}
},
[onChange],
);
return (
<div className={styles.bulletEditor}>
<div className={styles.bulletToolbar}>
<button
type="button"
className={styles.bulletBtn}
onClick={toggleBullet}
title="Aufzählungspunkt ein/aus (☰)"
>
Liste
</button>
</div>
<textarea
ref={textareaRef}
className={styles.bulletEditorTextarea}
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
maxLength={maxLength}
rows={rows}
/>
</div>
);
}
export function ProjectModal({ isOpen, onClose, onSave, project }: ProjectModalProps) { export function ProjectModal({ isOpen, onClose, onSave, project }: ProjectModalProps) {
const [fromMonth, setFromMonth] = useState(1); const [fromMonth, setFromMonth] = useState(1);
const [fromYear, setFromYear] = useState(currentYear); const [fromYear, setFromYear] = useState(currentYear);
@ -185,12 +292,12 @@ export function ProjectModal({ isOpen, onClose, onSave, project }: ProjectModalP
<div className={styles.modalField}> <div className={styles.modalField}>
<label>Aufgaben</label> <label>Aufgaben</label>
<textarea <BulletEditor
value={tasks} value={tasks}
onChange={(e) => setTasks(e.target.value)} onChange={setTasks}
placeholder="Beschreiben Sie Ihre Aufgaben..." placeholder="Beschreiben Sie Ihre Aufgaben... (☰ Liste für Aufzählungspunkte)"
maxLength={1500} maxLength={1500}
rows={4} rows={6}
/> />
<div className={`${styles.charCount} ${tasks.length > 1400 ? styles.charCountWarn : ''}`}> <div className={`${styles.charCount} ${tasks.length > 1400 ? styles.charCountWarn : ''}`}>
{tasks.length}/1.500 {tasks.length}/1.500