nca-ai-cms-astro-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -0
- package/README.md +87 -0
- package/package.json +53 -0
- package/src/api/_utils.ts +20 -0
- package/src/api/articles/[id]/apply.ts +89 -0
- package/src/api/articles/[id]/regenerate-image.ts +49 -0
- package/src/api/articles/[id]/regenerate-text.ts +57 -0
- package/src/api/articles/[id].ts +53 -0
- package/src/api/auth/check.ts +6 -0
- package/src/api/auth/login.ts +43 -0
- package/src/api/auth/logout.ts +6 -0
- package/src/api/generate-content.ts +43 -0
- package/src/api/generate-image.ts +33 -0
- package/src/api/prompts.ts +45 -0
- package/src/api/save-image.ts +38 -0
- package/src/api/save.ts +49 -0
- package/src/api/scheduler/[id].ts +31 -0
- package/src/api/scheduler/generate.ts +94 -0
- package/src/api/scheduler/publish.ts +96 -0
- package/src/api/scheduler.ts +51 -0
- package/src/components/Editor.tsx +115 -0
- package/src/components/editor/GenerateTab.tsx +384 -0
- package/src/components/editor/PlannerTab.tsx +345 -0
- package/src/components/editor/SettingsTab.tsx +185 -0
- package/src/components/editor/styles.ts +597 -0
- package/src/components/editor/types.ts +49 -0
- package/src/components/editor/useTabNavigation.ts +69 -0
- package/src/config.d.ts +4 -0
- package/src/db/tables.ts +39 -0
- package/src/domain/entities/Article.test.ts +138 -0
- package/src/domain/entities/Article.ts +90 -0
- package/src/domain/entities/ScheduledPost.test.ts +228 -0
- package/src/domain/entities/ScheduledPost.ts +152 -0
- package/src/domain/entities/Source.test.ts +57 -0
- package/src/domain/entities/Source.ts +43 -0
- package/src/domain/entities/index.ts +9 -0
- package/src/domain/index.ts +16 -0
- package/src/domain/value-objects/ArticleFinder.test.ts +104 -0
- package/src/domain/value-objects/ArticleFinder.ts +61 -0
- package/src/domain/value-objects/SEOMetadata.test.ts +48 -0
- package/src/domain/value-objects/SEOMetadata.ts +19 -0
- package/src/domain/value-objects/Slug.test.ts +51 -0
- package/src/domain/value-objects/Slug.ts +33 -0
- package/src/domain/value-objects/index.ts +4 -0
- package/src/index.ts +146 -0
- package/src/middleware.ts +30 -0
- package/src/pages/editor.astro +22 -0
- package/src/pages/login.astro +117 -0
- package/src/services/ArticleService.test.ts +148 -0
- package/src/services/ArticleService.ts +150 -0
- package/src/services/AutoPublisher.ts +122 -0
- package/src/services/ContentFetcher.ts +89 -0
- package/src/services/ContentGenerator.ts +320 -0
- package/src/services/FileWriter.test.ts +80 -0
- package/src/services/FileWriter.ts +59 -0
- package/src/services/ImageConverter.ts +15 -0
- package/src/services/ImageGenerator.ts +108 -0
- package/src/services/PromptService.ts +84 -0
- package/src/services/SchedulerDBAdapter.ts +75 -0
- package/src/services/SchedulerService.test.ts +286 -0
- package/src/services/SchedulerService.ts +149 -0
- package/src/services/index.ts +27 -0
- package/src/utils/authUtils.test.ts +60 -0
- package/src/utils/authUtils.ts +25 -0
- package/src/utils/envUtils.test.ts +40 -0
- package/src/utils/envUtils.ts +26 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/markdown.test.ts +65 -0
- package/src/utils/markdown.ts +13 -0
- package/src/utils/sanitize.test.ts +180 -0
- package/src/utils/sanitize.ts +98 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import type { ScheduledPostData } from './types';
|
|
3
|
+
import { styles } from './styles';
|
|
4
|
+
|
|
5
|
+
function getStatusColor(status: string): string {
|
|
6
|
+
switch (status) {
|
|
7
|
+
case 'published':
|
|
8
|
+
return 'var(--color-success, #4ade80)';
|
|
9
|
+
case 'generated':
|
|
10
|
+
return 'var(--color-primary, #e63946)';
|
|
11
|
+
case 'failed':
|
|
12
|
+
return 'var(--color-error, #f87171)';
|
|
13
|
+
default:
|
|
14
|
+
return 'var(--color-text-muted, #b8b5b0)';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getStatusLabel(status: string): string {
|
|
19
|
+
switch (status) {
|
|
20
|
+
case 'published':
|
|
21
|
+
return 'Veröffentlicht';
|
|
22
|
+
case 'generated':
|
|
23
|
+
return 'Generiert';
|
|
24
|
+
case 'scheduled':
|
|
25
|
+
return 'Geplant';
|
|
26
|
+
case 'failed':
|
|
27
|
+
return 'Fehlgeschlagen';
|
|
28
|
+
default:
|
|
29
|
+
return status;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatDate(dateStr: string): string {
|
|
34
|
+
try {
|
|
35
|
+
return new Date(dateStr).toLocaleDateString('de-DE', {
|
|
36
|
+
day: '2-digit',
|
|
37
|
+
month: '2-digit',
|
|
38
|
+
year: 'numeric',
|
|
39
|
+
hour: '2-digit',
|
|
40
|
+
minute: '2-digit',
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return dateStr;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function PlannerTab() {
|
|
48
|
+
const [posts, setPosts] = useState<ScheduledPostData[]>([]);
|
|
49
|
+
const [loading, setLoading] = useState(true);
|
|
50
|
+
const [error, setError] = useState<string | null>(null);
|
|
51
|
+
const [newInput, setNewInput] = useState('');
|
|
52
|
+
const [newInputType] = useState('keyword');
|
|
53
|
+
const [newDate, setNewDate] = useState('');
|
|
54
|
+
const [adding, setAdding] = useState(false);
|
|
55
|
+
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
56
|
+
|
|
57
|
+
const loadPosts = useCallback(async () => {
|
|
58
|
+
setLoading(true);
|
|
59
|
+
setError(null);
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch('/api/scheduler');
|
|
62
|
+
if (!res.ok) throw new Error('Fehler beim Laden der geplanten Beiträge');
|
|
63
|
+
const data = (await res.json()) as ScheduledPostData[];
|
|
64
|
+
setPosts(data);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
setError(
|
|
67
|
+
err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten',
|
|
68
|
+
);
|
|
69
|
+
} finally {
|
|
70
|
+
setLoading(false);
|
|
71
|
+
}
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const autoPublishDue = useCallback(async () => {
|
|
75
|
+
try {
|
|
76
|
+
await fetch('/api/scheduler', {
|
|
77
|
+
method: 'PATCH',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ action: 'auto-publish' }),
|
|
80
|
+
});
|
|
81
|
+
await loadPosts();
|
|
82
|
+
} catch {
|
|
83
|
+
// silent fail for auto-publish
|
|
84
|
+
}
|
|
85
|
+
}, [loadPosts]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
loadPosts().then(() => autoPublishDue());
|
|
89
|
+
}, [loadPosts, autoPublishDue]);
|
|
90
|
+
|
|
91
|
+
const handleAdd = async () => {
|
|
92
|
+
if (!newInput.trim() || !newDate) return;
|
|
93
|
+
setAdding(true);
|
|
94
|
+
setError(null);
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch('/api/scheduler', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
input: newInput,
|
|
101
|
+
inputType: newInputType,
|
|
102
|
+
scheduledDate: newDate,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) throw new Error('Fehler beim Hinzufügen');
|
|
106
|
+
setNewInput('');
|
|
107
|
+
setNewDate('');
|
|
108
|
+
await loadPosts();
|
|
109
|
+
} catch (err) {
|
|
110
|
+
setError(
|
|
111
|
+
err instanceof Error ? err.message : 'Fehler beim Hinzufügen',
|
|
112
|
+
);
|
|
113
|
+
} finally {
|
|
114
|
+
setAdding(false);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleGenerate = async (id: string) => {
|
|
119
|
+
setActionLoading(id);
|
|
120
|
+
setError(null);
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch('/api/scheduler', {
|
|
123
|
+
method: 'PATCH',
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({ action: 'generate', id }),
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) throw new Error('Fehler beim Generieren');
|
|
128
|
+
await loadPosts();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
setError(
|
|
131
|
+
err instanceof Error ? err.message : 'Fehler beim Generieren',
|
|
132
|
+
);
|
|
133
|
+
} finally {
|
|
134
|
+
setActionLoading(null);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handlePublish = async (id: string) => {
|
|
139
|
+
setActionLoading(id);
|
|
140
|
+
setError(null);
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch('/api/scheduler', {
|
|
143
|
+
method: 'PATCH',
|
|
144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
145
|
+
body: JSON.stringify({ action: 'publish', id }),
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok) throw new Error('Fehler beim Veröffentlichen');
|
|
148
|
+
await loadPosts();
|
|
149
|
+
} catch (err) {
|
|
150
|
+
setError(
|
|
151
|
+
err instanceof Error
|
|
152
|
+
? err.message
|
|
153
|
+
: 'Fehler beim Veröffentlichen',
|
|
154
|
+
);
|
|
155
|
+
} finally {
|
|
156
|
+
setActionLoading(null);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleDelete = async (id: string) => {
|
|
161
|
+
setActionLoading(id);
|
|
162
|
+
setError(null);
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch('/api/scheduler', {
|
|
165
|
+
method: 'DELETE',
|
|
166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
167
|
+
body: JSON.stringify({ id }),
|
|
168
|
+
});
|
|
169
|
+
if (!res.ok) throw new Error('Fehler beim Löschen');
|
|
170
|
+
await loadPosts();
|
|
171
|
+
} catch (err) {
|
|
172
|
+
setError(
|
|
173
|
+
err instanceof Error ? err.message : 'Fehler beim Löschen',
|
|
174
|
+
);
|
|
175
|
+
} finally {
|
|
176
|
+
setActionLoading(null);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div style={styles.plannerContent}>
|
|
182
|
+
<div style={styles.plannerForm}>
|
|
183
|
+
<h3 style={{ ...styles.heading, fontSize: '1rem' }}>
|
|
184
|
+
Neuen Beitrag planen
|
|
185
|
+
</h3>
|
|
186
|
+
<div style={styles.plannerFormRow}>
|
|
187
|
+
<div style={styles.field}>
|
|
188
|
+
<label style={styles.label} htmlFor="planner-input">
|
|
189
|
+
Thema / Keyword
|
|
190
|
+
</label>
|
|
191
|
+
<input
|
|
192
|
+
id="planner-input"
|
|
193
|
+
type="text"
|
|
194
|
+
style={styles.input}
|
|
195
|
+
value={newInput}
|
|
196
|
+
onChange={(e) => setNewInput(e.target.value)}
|
|
197
|
+
placeholder="z.B. Next.js App Router..."
|
|
198
|
+
disabled={adding}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
<div style={styles.field}>
|
|
202
|
+
<label style={styles.label} htmlFor="planner-date">
|
|
203
|
+
Veröffentlichungsdatum
|
|
204
|
+
</label>
|
|
205
|
+
<input
|
|
206
|
+
id="planner-date"
|
|
207
|
+
type="datetime-local"
|
|
208
|
+
style={styles.input}
|
|
209
|
+
value={newDate}
|
|
210
|
+
onChange={(e) => setNewDate(e.target.value)}
|
|
211
|
+
disabled={adding}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
style={{
|
|
217
|
+
...styles.generateButton,
|
|
218
|
+
width: 'auto',
|
|
219
|
+
whiteSpace: 'nowrap' as React.CSSProperties['whiteSpace'],
|
|
220
|
+
opacity: adding || !newInput.trim() || !newDate ? 0.6 : 1,
|
|
221
|
+
}}
|
|
222
|
+
onClick={handleAdd}
|
|
223
|
+
disabled={adding || !newInput.trim() || !newDate}
|
|
224
|
+
>
|
|
225
|
+
{adding ? 'Wird hinzugefügt...' : 'Hinzufügen'}
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
231
|
+
|
|
232
|
+
{loading && (
|
|
233
|
+
<div style={styles.loadingBox}>Geplante Beiträge werden geladen...</div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{!loading && posts.length === 0 && (
|
|
237
|
+
<div style={styles.emptyState}>
|
|
238
|
+
Noch keine geplanten Beiträge. Erstelle deinen ersten oben.
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{!loading && posts.length > 0 && (
|
|
243
|
+
<div style={styles.plannerList}>
|
|
244
|
+
{posts.map((post) => (
|
|
245
|
+
<div key={post.id} style={styles.plannerCard}>
|
|
246
|
+
<div style={styles.plannerCardHeader}>
|
|
247
|
+
<div style={styles.plannerCardMeta}>
|
|
248
|
+
<span style={styles.plannerDate}>
|
|
249
|
+
{formatDate(post.scheduledDate)}
|
|
250
|
+
</span>
|
|
251
|
+
<span
|
|
252
|
+
style={{
|
|
253
|
+
...styles.statusBadge,
|
|
254
|
+
background: getStatusColor(post.status),
|
|
255
|
+
color:
|
|
256
|
+
post.status === 'scheduled'
|
|
257
|
+
? 'var(--color-text, #faf9f7)'
|
|
258
|
+
: '#000',
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
{getStatusLabel(post.status)}
|
|
262
|
+
</span>
|
|
263
|
+
<span style={styles.plannerInputType}>
|
|
264
|
+
{post.inputType}
|
|
265
|
+
</span>
|
|
266
|
+
</div>
|
|
267
|
+
<div style={styles.plannerCardActions}>
|
|
268
|
+
{post.status === 'scheduled' && (
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
style={styles.editButton}
|
|
272
|
+
onClick={() => handleGenerate(post.id)}
|
|
273
|
+
disabled={actionLoading === post.id}
|
|
274
|
+
>
|
|
275
|
+
{actionLoading === post.id
|
|
276
|
+
? 'Generiere...'
|
|
277
|
+
: 'Generieren'}
|
|
278
|
+
</button>
|
|
279
|
+
)}
|
|
280
|
+
{post.status === 'generated' && (
|
|
281
|
+
<button
|
|
282
|
+
type="button"
|
|
283
|
+
style={styles.saveButton}
|
|
284
|
+
onClick={() => handlePublish(post.id)}
|
|
285
|
+
disabled={actionLoading === post.id}
|
|
286
|
+
>
|
|
287
|
+
{actionLoading === post.id
|
|
288
|
+
? 'Veröffentliche...'
|
|
289
|
+
: 'Veröffentlichen'}
|
|
290
|
+
</button>
|
|
291
|
+
)}
|
|
292
|
+
{post.status !== 'published' && (
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
style={styles.cancelButton}
|
|
296
|
+
onClick={() => handleDelete(post.id)}
|
|
297
|
+
disabled={actionLoading === post.id}
|
|
298
|
+
>
|
|
299
|
+
Löschen
|
|
300
|
+
</button>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div style={styles.plannerCardBody}>
|
|
306
|
+
<div style={styles.plannerInput}>
|
|
307
|
+
<strong>Eingabe:</strong> {post.input}
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{post.generatedTitle && (
|
|
311
|
+
<div style={styles.plannerPreview}>
|
|
312
|
+
<strong>Titel:</strong> {post.generatedTitle}
|
|
313
|
+
{post.generatedDescription && (
|
|
314
|
+
<>
|
|
315
|
+
<br />
|
|
316
|
+
<strong>Beschreibung:</strong>{' '}
|
|
317
|
+
{post.generatedDescription}
|
|
318
|
+
</>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
|
|
323
|
+
{post.generatedImageData && (
|
|
324
|
+
<div style={styles.plannerImagePreview}>
|
|
325
|
+
<img
|
|
326
|
+
src={post.generatedImageData}
|
|
327
|
+
alt={post.generatedImageAlt || 'Generiertes Bild'}
|
|
328
|
+
style={styles.plannerImage}
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
|
|
333
|
+
{post.publishedPath && (
|
|
334
|
+
<div style={styles.plannerPublishedPath}>
|
|
335
|
+
{post.publishedPath}
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
))}
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import type { Prompt, SettingsSubTab } from './types';
|
|
3
|
+
import { styles } from './styles';
|
|
4
|
+
|
|
5
|
+
const SUB_TABS: { key: SettingsSubTab; label: string }[] = [
|
|
6
|
+
{ key: 'homepage', label: 'Homepage' },
|
|
7
|
+
{ key: 'content-ai', label: 'Content-KI' },
|
|
8
|
+
{ key: 'analysis-ai', label: 'Analyse-KI' },
|
|
9
|
+
{ key: 'image-ai', label: 'Bild-KI' },
|
|
10
|
+
{ key: 'website', label: 'Website' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
interface EditState {
|
|
14
|
+
id: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SettingsTab() {
|
|
19
|
+
const [activeSubTab, setActiveSubTab] =
|
|
20
|
+
useState<SettingsSubTab>('homepage');
|
|
21
|
+
const [prompts, setPrompts] = useState<Prompt[]>([]);
|
|
22
|
+
const [loading, setLoading] = useState(true);
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
const [editing, setEditing] = useState<EditState | null>(null);
|
|
25
|
+
const [saving, setSaving] = useState(false);
|
|
26
|
+
|
|
27
|
+
const loadPrompts = useCallback(async () => {
|
|
28
|
+
setLoading(true);
|
|
29
|
+
setError(null);
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch('/api/prompts');
|
|
32
|
+
if (!res.ok) throw new Error('Fehler beim Laden der Einstellungen');
|
|
33
|
+
const data = (await res.json()) as Prompt[];
|
|
34
|
+
setPrompts(data);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setError(
|
|
37
|
+
err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten',
|
|
38
|
+
);
|
|
39
|
+
} finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
loadPrompts();
|
|
46
|
+
}, [loadPrompts]);
|
|
47
|
+
|
|
48
|
+
const filteredPrompts = prompts.filter(
|
|
49
|
+
(p) => p.category === activeSubTab,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const handleEdit = (prompt: Prompt) => {
|
|
53
|
+
setEditing({ id: prompt.id, value: prompt.promptText });
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleCancel = () => {
|
|
57
|
+
setEditing(null);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleSave = async () => {
|
|
61
|
+
if (!editing) return;
|
|
62
|
+
setSaving(true);
|
|
63
|
+
setError(null);
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch('/api/prompts', {
|
|
66
|
+
method: 'PUT',
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
id: editing.id,
|
|
70
|
+
promptText: editing.value,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) throw new Error('Fehler beim Speichern');
|
|
74
|
+
setPrompts((prev) =>
|
|
75
|
+
prev.map((p) =>
|
|
76
|
+
p.id === editing.id ? { ...p, promptText: editing.value } : p,
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
setEditing(null);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
setError(
|
|
82
|
+
err instanceof Error ? err.message : 'Fehler beim Speichern',
|
|
83
|
+
);
|
|
84
|
+
} finally {
|
|
85
|
+
setSaving(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div style={styles.settingsContent}>
|
|
91
|
+
<div style={styles.subTabNav} role="tablist">
|
|
92
|
+
{SUB_TABS.map((tab) => (
|
|
93
|
+
<button
|
|
94
|
+
key={tab.key}
|
|
95
|
+
type="button"
|
|
96
|
+
role="tab"
|
|
97
|
+
aria-selected={activeSubTab === tab.key}
|
|
98
|
+
style={
|
|
99
|
+
activeSubTab === tab.key
|
|
100
|
+
? styles.subTabActive
|
|
101
|
+
: styles.subTab
|
|
102
|
+
}
|
|
103
|
+
onClick={() => {
|
|
104
|
+
setActiveSubTab(tab.key);
|
|
105
|
+
setEditing(null);
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{tab.label}
|
|
109
|
+
</button>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{loading && (
|
|
114
|
+
<div style={styles.loadingBox}>Einstellungen werden geladen...</div>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
118
|
+
|
|
119
|
+
{!loading && filteredPrompts.length === 0 && (
|
|
120
|
+
<div style={styles.emptyState}>
|
|
121
|
+
Keine Einstellungen in dieser Kategorie.
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{!loading && filteredPrompts.length > 0 && (
|
|
126
|
+
<div style={styles.cardGrid}>
|
|
127
|
+
{filteredPrompts.map((prompt) => (
|
|
128
|
+
<div key={prompt.id} style={styles.settingsCard}>
|
|
129
|
+
<div style={styles.settingsCardHeader}>
|
|
130
|
+
<div style={styles.settingsItemLabel}>{prompt.name}</div>
|
|
131
|
+
<div style={styles.settingsItemCategory}>
|
|
132
|
+
{prompt.category}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{editing?.id === prompt.id ? (
|
|
137
|
+
<div style={styles.editArea}>
|
|
138
|
+
<textarea
|
|
139
|
+
style={styles.textarea}
|
|
140
|
+
value={editing.value}
|
|
141
|
+
onChange={(e) =>
|
|
142
|
+
setEditing({ ...editing, value: e.target.value })
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
<div style={styles.editButtons}>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
style={styles.saveButton}
|
|
149
|
+
onClick={handleSave}
|
|
150
|
+
disabled={saving}
|
|
151
|
+
>
|
|
152
|
+
{saving ? 'Speichere...' : 'Speichern'}
|
|
153
|
+
</button>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
style={styles.cancelButton}
|
|
157
|
+
onClick={handleCancel}
|
|
158
|
+
>
|
|
159
|
+
Abbrechen
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
) : (
|
|
164
|
+
<>
|
|
165
|
+
<div style={styles.settingsItemValue}>
|
|
166
|
+
{prompt.promptText}
|
|
167
|
+
</div>
|
|
168
|
+
<div style={{ padding: '0 1rem 1rem' }}>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
style={styles.editButton}
|
|
172
|
+
onClick={() => handleEdit(prompt)}
|
|
173
|
+
>
|
|
174
|
+
Bearbeiten
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|