mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 22:36:38 +02:00
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:
parent
2078a90fba
commit
a4013d4356
5 changed files with 201 additions and 7 deletions
26
Summarize.md
26
Summarize.md
|
|
@ -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
|
||||
|
||||
#### Backend (crm-service)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function Drawer({
|
|||
if (!isOpen) return null;
|
||||
|
||||
return createPortal(
|
||||
<div className={styles.overlay} onClick={onClose}>
|
||||
<div className={styles.overlay}>
|
||||
<div
|
||||
className={styles.panel}
|
||||
style={{ width }}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export function Modal({ isOpen, onClose, title, children, maxWidth = '600px' }:
|
|||
if (!isOpen) return null;
|
||||
|
||||
return createPortal(
|
||||
<div className={styles.overlay} onClick={onClose}>
|
||||
<div className={styles.overlay}>
|
||||
<div
|
||||
className={styles.container}
|
||||
style={{ maxWidth }}
|
||||
|
|
|
|||
|
|
@ -418,6 +418,67 @@
|
|||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -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 api from '../../api/client';
|
||||
import type { ExpertProject } from '../ExpertProfileTab';
|
||||
|
|
@ -34,6 +34,113 @@ const INDUSTRIES = [
|
|||
const currentYear = new Date().getFullYear();
|
||||
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) {
|
||||
const [fromMonth, setFromMonth] = useState(1);
|
||||
const [fromYear, setFromYear] = useState(currentYear);
|
||||
|
|
@ -185,12 +292,12 @@ export function ProjectModal({ isOpen, onClose, onSave, project }: ProjectModalP
|
|||
|
||||
<div className={styles.modalField}>
|
||||
<label>Aufgaben</label>
|
||||
<textarea
|
||||
<BulletEditor
|
||||
value={tasks}
|
||||
onChange={(e) => setTasks(e.target.value)}
|
||||
placeholder="Beschreiben Sie Ihre Aufgaben..."
|
||||
onChange={setTasks}
|
||||
placeholder="Beschreiben Sie Ihre Aufgaben... (☰ Liste für Aufzählungspunkte)"
|
||||
maxLength={1500}
|
||||
rows={4}
|
||||
rows={6}
|
||||
/>
|
||||
<div className={`${styles.charCount} ${tasks.length > 1400 ? styles.charCountWarn : ''}`}>
|
||||
{tasks.length}/1.500
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue