mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 22:46:39 +02:00
Input fields now appear inline next to the section title, matching the layout pattern used by Projects, Certifications and Attachments sections. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
107 lines
2.8 KiB
TypeScript
107 lines
2.8 KiB
TypeScript
import { useState, type KeyboardEvent } from 'react';
|
||
import api from '../../api/client';
|
||
import styles from '../ExpertProfileTab.module.css';
|
||
|
||
interface SkillsSectionProps {
|
||
skills: string[];
|
||
onUpdate: () => Promise<void>;
|
||
}
|
||
|
||
export function SkillsSection({ skills, onUpdate }: SkillsSectionProps) {
|
||
const [newSkill, setNewSkill] = useState('');
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState('');
|
||
|
||
const handleAdd = async () => {
|
||
const skill = newSkill.trim();
|
||
if (!skill) return;
|
||
if (skills.includes(skill)) {
|
||
setError('Skill bereits vorhanden');
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
setError('');
|
||
try {
|
||
await api.patch('/expert-profile/me/skills', { skills: [...skills, skill] });
|
||
setNewSkill('');
|
||
await onUpdate();
|
||
} catch {
|
||
setError('Fehler beim Hinzufügen');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleRemove = async (skillToRemove: string) => {
|
||
setLoading(true);
|
||
setError('');
|
||
try {
|
||
await api.patch('/expert-profile/me/skills', {
|
||
skills: skills.filter((s) => s !== skillToRemove),
|
||
});
|
||
await onUpdate();
|
||
} catch {
|
||
setError('Fehler beim Entfernen');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
handleAdd();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className={styles.section}>
|
||
<div className={styles.sectionHeader}>
|
||
<h3 className={styles.sectionTitle}>Skills</h3>
|
||
<div className={styles.chipInput}>
|
||
<input
|
||
type="text"
|
||
value={newSkill}
|
||
onChange={(e) => setNewSkill(e.target.value)}
|
||
onKeyDown={handleKeyDown}
|
||
placeholder="Neuer Skill..."
|
||
maxLength={100}
|
||
disabled={loading}
|
||
/>
|
||
<button
|
||
type="button"
|
||
className={styles.btnPrimary}
|
||
onClick={handleAdd}
|
||
disabled={loading || !newSkill.trim()}
|
||
>
|
||
Hinzufügen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{error && <div className={styles.error}>{error}</div>}
|
||
|
||
{skills.length > 0 ? (
|
||
<div className={styles.chipContainer}>
|
||
{skills.map((skill) => (
|
||
<span key={skill} className={styles.chip}>
|
||
{skill}
|
||
<button
|
||
type="button"
|
||
className={styles.chipRemove}
|
||
onClick={() => handleRemove(skill)}
|
||
disabled={loading}
|
||
aria-label={`${skill} entfernen`}
|
||
>
|
||
×
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p className={styles.emptyState}>Noch keine Skills hinzugefügt</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|