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,384 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import type { GeneratedArticle, GeneratedImage } from './types';
|
|
9
|
+
import { styles } from './styles';
|
|
10
|
+
|
|
11
|
+
interface GenerateTabState {
|
|
12
|
+
input: string;
|
|
13
|
+
setInput: (value: string) => void;
|
|
14
|
+
article: GeneratedArticle | null;
|
|
15
|
+
image: GeneratedImage | null;
|
|
16
|
+
generating: boolean;
|
|
17
|
+
regenerating: 'article' | 'image' | null;
|
|
18
|
+
publishing: boolean;
|
|
19
|
+
published: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
handleGenerate: () => Promise<void>;
|
|
22
|
+
handleRegenerateArticle: () => Promise<void>;
|
|
23
|
+
handleRegenerateImage: () => Promise<void>;
|
|
24
|
+
handlePublish: () => Promise<void>;
|
|
25
|
+
handleReset: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const GenerateTabContext = createContext<GenerateTabState | null>(null);
|
|
29
|
+
|
|
30
|
+
function useGenerateTab(): GenerateTabState {
|
|
31
|
+
const ctx = useContext(GenerateTabContext);
|
|
32
|
+
if (!ctx) {
|
|
33
|
+
throw new Error('useGenerateTab must be used within GenerateTabProvider');
|
|
34
|
+
}
|
|
35
|
+
return ctx;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface GenerateTabProviderProps {
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function GenerateTabProvider({ children }: GenerateTabProviderProps) {
|
|
43
|
+
const [input, setInput] = useState('');
|
|
44
|
+
const [article, setArticle] = useState<GeneratedArticle | null>(null);
|
|
45
|
+
const [image, setImage] = useState<GeneratedImage | null>(null);
|
|
46
|
+
const [generating, setGenerating] = useState(false);
|
|
47
|
+
const [regenerating, setRegenerating] = useState<
|
|
48
|
+
'article' | 'image' | null
|
|
49
|
+
>(null);
|
|
50
|
+
const [publishing, setPublishing] = useState(false);
|
|
51
|
+
const [published, setPublished] = useState(false);
|
|
52
|
+
const [error, setError] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
const generateContent = useCallback(async (topic: string) => {
|
|
55
|
+
const res = await fetch('/api/generate-content', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ input: topic }),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const data = await res.json().catch(() => ({}));
|
|
62
|
+
throw new Error(data.error || 'Fehler beim Generieren des Artikels');
|
|
63
|
+
}
|
|
64
|
+
return res.json() as Promise<GeneratedArticle>;
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const generateImage = useCallback(async (topic: string) => {
|
|
68
|
+
const res = await fetch('/api/generate-image', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({ input: topic }),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const data = await res.json().catch(() => ({}));
|
|
75
|
+
throw new Error(data.error || 'Fehler beim Generieren des Bildes');
|
|
76
|
+
}
|
|
77
|
+
return res.json() as Promise<GeneratedImage>;
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const handleGenerate = useCallback(async () => {
|
|
81
|
+
if (!input.trim()) return;
|
|
82
|
+
setGenerating(true);
|
|
83
|
+
setError(null);
|
|
84
|
+
setArticle(null);
|
|
85
|
+
setImage(null);
|
|
86
|
+
setPublished(false);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const [articleResult, imageResult] = await Promise.all([
|
|
90
|
+
generateContent(input),
|
|
91
|
+
generateImage(input),
|
|
92
|
+
]);
|
|
93
|
+
setArticle(articleResult);
|
|
94
|
+
setImage(imageResult);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
setError(
|
|
97
|
+
err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten',
|
|
98
|
+
);
|
|
99
|
+
} finally {
|
|
100
|
+
setGenerating(false);
|
|
101
|
+
}
|
|
102
|
+
}, [input, generateContent, generateImage]);
|
|
103
|
+
|
|
104
|
+
const handleRegenerateArticle = useCallback(async () => {
|
|
105
|
+
if (!input.trim()) return;
|
|
106
|
+
setRegenerating('article');
|
|
107
|
+
setError(null);
|
|
108
|
+
try {
|
|
109
|
+
const result = await generateContent(input);
|
|
110
|
+
setArticle(result);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
setError(
|
|
113
|
+
err instanceof Error ? err.message : 'Fehler beim Regenerieren',
|
|
114
|
+
);
|
|
115
|
+
} finally {
|
|
116
|
+
setRegenerating(null);
|
|
117
|
+
}
|
|
118
|
+
}, [input, generateContent]);
|
|
119
|
+
|
|
120
|
+
const handleRegenerateImage = useCallback(async () => {
|
|
121
|
+
if (!input.trim()) return;
|
|
122
|
+
setRegenerating('image');
|
|
123
|
+
setError(null);
|
|
124
|
+
try {
|
|
125
|
+
const result = await generateImage(input);
|
|
126
|
+
setImage(result);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
setError(
|
|
129
|
+
err instanceof Error ? err.message : 'Fehler beim Regenerieren',
|
|
130
|
+
);
|
|
131
|
+
} finally {
|
|
132
|
+
setRegenerating(null);
|
|
133
|
+
}
|
|
134
|
+
}, [input, generateImage]);
|
|
135
|
+
|
|
136
|
+
const handlePublish = useCallback(async () => {
|
|
137
|
+
if (!article || !image) return;
|
|
138
|
+
setPublishing(true);
|
|
139
|
+
setError(null);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const saveRes = await fetch('/api/save', {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
145
|
+
body: JSON.stringify(article),
|
|
146
|
+
});
|
|
147
|
+
if (!saveRes.ok) {
|
|
148
|
+
const data = await saveRes.json().catch(() => ({}));
|
|
149
|
+
throw new Error(data.error || 'Fehler beim Speichern des Artikels');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const imgRes = await fetch('/api/save-image', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
155
|
+
body: JSON.stringify(image),
|
|
156
|
+
});
|
|
157
|
+
if (!imgRes.ok) {
|
|
158
|
+
const data = await imgRes.json().catch(() => ({}));
|
|
159
|
+
throw new Error(data.error || 'Fehler beim Speichern des Bildes');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setPublished(true);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
setError(
|
|
165
|
+
err instanceof Error
|
|
166
|
+
? err.message
|
|
167
|
+
: 'Fehler beim Veröffentlichen',
|
|
168
|
+
);
|
|
169
|
+
} finally {
|
|
170
|
+
setPublishing(false);
|
|
171
|
+
}
|
|
172
|
+
}, [article, image]);
|
|
173
|
+
|
|
174
|
+
const handleReset = useCallback(() => {
|
|
175
|
+
setInput('');
|
|
176
|
+
setArticle(null);
|
|
177
|
+
setImage(null);
|
|
178
|
+
setPublished(false);
|
|
179
|
+
setError(null);
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
const value: GenerateTabState = {
|
|
183
|
+
input,
|
|
184
|
+
setInput,
|
|
185
|
+
article,
|
|
186
|
+
image,
|
|
187
|
+
generating,
|
|
188
|
+
regenerating,
|
|
189
|
+
publishing,
|
|
190
|
+
published,
|
|
191
|
+
error,
|
|
192
|
+
handleGenerate,
|
|
193
|
+
handleRegenerateArticle,
|
|
194
|
+
handleRegenerateImage,
|
|
195
|
+
handlePublish,
|
|
196
|
+
handleReset,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<GenerateTabContext.Provider value={value}>
|
|
201
|
+
{children}
|
|
202
|
+
</GenerateTabContext.Provider>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function GenerateTabControls() {
|
|
207
|
+
const {
|
|
208
|
+
input,
|
|
209
|
+
setInput,
|
|
210
|
+
article,
|
|
211
|
+
image,
|
|
212
|
+
generating,
|
|
213
|
+
regenerating,
|
|
214
|
+
publishing,
|
|
215
|
+
published,
|
|
216
|
+
error,
|
|
217
|
+
handleGenerate,
|
|
218
|
+
handleRegenerateArticle,
|
|
219
|
+
handleRegenerateImage,
|
|
220
|
+
handlePublish,
|
|
221
|
+
handleReset,
|
|
222
|
+
} = useGenerateTab();
|
|
223
|
+
|
|
224
|
+
if (published && article) {
|
|
225
|
+
return (
|
|
226
|
+
<div style={styles.panel}>
|
|
227
|
+
<div style={styles.successBox}>
|
|
228
|
+
<div style={styles.successIcon}>✓</div>
|
|
229
|
+
<div style={styles.successTitle}>Erfolgreich veröffentlicht!</div>
|
|
230
|
+
<div style={styles.successPath}>{article.filepath}</div>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
style={styles.newButton}
|
|
234
|
+
onClick={handleReset}
|
|
235
|
+
>
|
|
236
|
+
Neuen Artikel erstellen
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div style={styles.panel}>
|
|
245
|
+
<div style={styles.field}>
|
|
246
|
+
<label style={styles.label} htmlFor="topic-input">
|
|
247
|
+
Thema oder Keyword
|
|
248
|
+
</label>
|
|
249
|
+
<input
|
|
250
|
+
id="topic-input"
|
|
251
|
+
type="text"
|
|
252
|
+
style={styles.input}
|
|
253
|
+
value={input}
|
|
254
|
+
onChange={(e) => setInput(e.target.value)}
|
|
255
|
+
placeholder="z.B. React Server Components, TypeScript Generics..."
|
|
256
|
+
disabled={generating}
|
|
257
|
+
onKeyDown={(e) => {
|
|
258
|
+
if (e.key === 'Enter') handleGenerate();
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
<span style={styles.hint}>
|
|
262
|
+
Gib ein Thema ein, um einen Artikel zu generieren
|
|
263
|
+
</span>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
style={{
|
|
269
|
+
...styles.generateButton,
|
|
270
|
+
opacity: generating || !input.trim() ? 0.6 : 1,
|
|
271
|
+
cursor:
|
|
272
|
+
generating || !input.trim() ? 'not-allowed' : 'pointer',
|
|
273
|
+
}}
|
|
274
|
+
onClick={handleGenerate}
|
|
275
|
+
disabled={generating || !input.trim()}
|
|
276
|
+
>
|
|
277
|
+
<span style={styles.buttonContent}>
|
|
278
|
+
{generating && <span style={styles.spinner} />}
|
|
279
|
+
{generating ? 'Generiere...' : 'Artikel generieren'}
|
|
280
|
+
</span>
|
|
281
|
+
</button>
|
|
282
|
+
|
|
283
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
284
|
+
|
|
285
|
+
{article && !published && (
|
|
286
|
+
<div style={styles.actionSection}>
|
|
287
|
+
<div style={styles.regenerateRow}>
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
style={{
|
|
291
|
+
...styles.secondaryButton,
|
|
292
|
+
opacity: regenerating ? 0.6 : 1,
|
|
293
|
+
}}
|
|
294
|
+
onClick={handleRegenerateArticle}
|
|
295
|
+
disabled={regenerating !== null}
|
|
296
|
+
>
|
|
297
|
+
{regenerating === 'article'
|
|
298
|
+
? 'Regeneriere...'
|
|
299
|
+
: 'Artikel regenerieren'}
|
|
300
|
+
</button>
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
style={{
|
|
304
|
+
...styles.secondaryButton,
|
|
305
|
+
opacity: regenerating ? 0.6 : 1,
|
|
306
|
+
}}
|
|
307
|
+
onClick={handleRegenerateImage}
|
|
308
|
+
disabled={regenerating !== null}
|
|
309
|
+
>
|
|
310
|
+
{regenerating === 'image'
|
|
311
|
+
? 'Regeneriere...'
|
|
312
|
+
: 'Bild regenerieren'}
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<button
|
|
317
|
+
type="button"
|
|
318
|
+
style={{
|
|
319
|
+
...styles.publishButton,
|
|
320
|
+
opacity: publishing || !image ? 0.6 : 1,
|
|
321
|
+
cursor:
|
|
322
|
+
publishing || !image ? 'not-allowed' : 'pointer',
|
|
323
|
+
}}
|
|
324
|
+
onClick={handlePublish}
|
|
325
|
+
disabled={publishing || !image}
|
|
326
|
+
>
|
|
327
|
+
<span style={styles.buttonContent}>
|
|
328
|
+
{publishing && <span style={styles.spinner} />}
|
|
329
|
+
{publishing
|
|
330
|
+
? 'Veröffentliche...'
|
|
331
|
+
: 'Artikel veröffentlichen'}
|
|
332
|
+
</span>
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function GenerateTabPreview() {
|
|
341
|
+
const { article, image, published } = useGenerateTab();
|
|
342
|
+
|
|
343
|
+
if (!article && !image) return null;
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div style={styles.previewArea}>
|
|
347
|
+
{image && (
|
|
348
|
+
<div style={styles.imagePreview}>
|
|
349
|
+
<img src={image.url} alt={image.alt} style={styles.image} />
|
|
350
|
+
<div style={styles.imageAlt}>{image.alt}</div>
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{article && (
|
|
355
|
+
<div style={published ? styles.publishedPreview : styles.preview}>
|
|
356
|
+
<div style={styles.previewHeader}>
|
|
357
|
+
<span style={styles.previewTitle}>Vorschau</span>
|
|
358
|
+
{published && (
|
|
359
|
+
<span style={styles.publishedBadge}>Veröffentlicht</span>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
<div style={styles.filepath}>{article.filepath}</div>
|
|
363
|
+
<div style={styles.frontmatter}>
|
|
364
|
+
<div style={styles.frontmatterRow}>
|
|
365
|
+
<span style={styles.frontmatterLabel}>Titel:</span>
|
|
366
|
+
<span style={styles.frontmatterValue}>
|
|
367
|
+
{article.title}
|
|
368
|
+
</span>
|
|
369
|
+
</div>
|
|
370
|
+
<div style={styles.frontmatterRow}>
|
|
371
|
+
<span style={styles.frontmatterLabel}>Beschreibung:</span>
|
|
372
|
+
<span style={styles.frontmatterValue}>
|
|
373
|
+
{article.description}
|
|
374
|
+
</span>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
<div style={styles.content}>
|
|
378
|
+
<div style={styles.markdown}>{article.content}</div>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|