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
package/src/api/save.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { Article } from '../domain/entities/Article';
|
|
4
|
+
import { FileWriter } from '../services/FileWriter';
|
|
5
|
+
import { contentPath } from 'virtual:nca-ai-cms/config';
|
|
6
|
+
import { jsonResponse, jsonError } from './_utils';
|
|
7
|
+
|
|
8
|
+
const SaveArticleSchema = z.object({
|
|
9
|
+
title: z.string().min(1),
|
|
10
|
+
description: z.string().min(1),
|
|
11
|
+
content: z.string().min(1),
|
|
12
|
+
date: z.string().optional(),
|
|
13
|
+
tags: z.array(z.string()).optional(),
|
|
14
|
+
imageAlt: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
18
|
+
try {
|
|
19
|
+
const body = await request.json();
|
|
20
|
+
const parsed = SaveArticleSchema.safeParse(body);
|
|
21
|
+
if (!parsed.success) {
|
|
22
|
+
return jsonError(parsed.error.errors[0]?.message ?? 'Invalid request', 400);
|
|
23
|
+
}
|
|
24
|
+
const data = parsed.data;
|
|
25
|
+
|
|
26
|
+
const article = new Article({
|
|
27
|
+
title: data.title,
|
|
28
|
+
description: data.description,
|
|
29
|
+
content: data.content,
|
|
30
|
+
date: new Date(data.date || Date.now()),
|
|
31
|
+
tags: data.tags || [],
|
|
32
|
+
image: './hero.webp',
|
|
33
|
+
imageAlt: data.imageAlt,
|
|
34
|
+
contentPath,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const writer = new FileWriter();
|
|
38
|
+
const result = await writer.write(article);
|
|
39
|
+
|
|
40
|
+
return jsonResponse({
|
|
41
|
+
success: true,
|
|
42
|
+
filepath: result.filepath,
|
|
43
|
+
folderPath: article.folderPath,
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Save error:', error);
|
|
47
|
+
return jsonError(error);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { SchedulerService } from '../../services/SchedulerService';
|
|
3
|
+
import { AstroSchedulerDBAdapter } from '../../services/SchedulerDBAdapter';
|
|
4
|
+
import { jsonResponse, jsonError } from '../_utils';
|
|
5
|
+
|
|
6
|
+
function getService(): SchedulerService {
|
|
7
|
+
return new SchedulerService(new AstroSchedulerDBAdapter());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const DELETE: APIRoute = async ({ params }) => {
|
|
11
|
+
try {
|
|
12
|
+
const id = params.id;
|
|
13
|
+
if (!id) {
|
|
14
|
+
return jsonError('id is required', 400);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const service = getService();
|
|
18
|
+
await service.delete(id);
|
|
19
|
+
|
|
20
|
+
return jsonResponse({ success: true });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Scheduler delete error:', error);
|
|
23
|
+
const status =
|
|
24
|
+
error instanceof Error && error.message.includes('not found')
|
|
25
|
+
? 404
|
|
26
|
+
: error instanceof Error && error.message.includes('Cannot delete')
|
|
27
|
+
? 403
|
|
28
|
+
: 500;
|
|
29
|
+
return jsonError(error, status);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { SchedulerService } from '../../services/SchedulerService';
|
|
3
|
+
import { AstroSchedulerDBAdapter } from '../../services/SchedulerDBAdapter';
|
|
4
|
+
import { ContentGenerator } from '../../services/ContentGenerator';
|
|
5
|
+
import { ImageGenerator } from '../../services/ImageGenerator';
|
|
6
|
+
import { PromptService } from '../../services/PromptService';
|
|
7
|
+
import { getEnvVariable } from '../../utils/envUtils';
|
|
8
|
+
import { jsonResponse, jsonError } from '../_utils';
|
|
9
|
+
|
|
10
|
+
function getService(): SchedulerService {
|
|
11
|
+
return new SchedulerService(new AstroSchedulerDBAdapter());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
15
|
+
try {
|
|
16
|
+
const { id, mode = 'all' } = await request.json();
|
|
17
|
+
|
|
18
|
+
if (!id) {
|
|
19
|
+
return jsonError('id is required', 400);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const service = getService();
|
|
23
|
+
const post = await service.getById(id);
|
|
24
|
+
|
|
25
|
+
if (!post.canGenerate()) {
|
|
26
|
+
return jsonError(
|
|
27
|
+
`Cannot generate: post is in status "${post.status}"`,
|
|
28
|
+
400
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const apiKey = getEnvVariable('GOOGLE_GEMINI_API_KEY');
|
|
33
|
+
|
|
34
|
+
if (mode === 'text' || mode === 'all') {
|
|
35
|
+
const promptService = new PromptService();
|
|
36
|
+
const contentGenerator = new ContentGenerator({ apiKey, promptService });
|
|
37
|
+
|
|
38
|
+
const article =
|
|
39
|
+
post.inputType === 'url'
|
|
40
|
+
? await contentGenerator.generateFromUrl(post.input)
|
|
41
|
+
: await contentGenerator.generateFromKeywords(post.input);
|
|
42
|
+
|
|
43
|
+
if (mode === 'text') {
|
|
44
|
+
await service.updateText(id, {
|
|
45
|
+
title: article.title,
|
|
46
|
+
description: article.description,
|
|
47
|
+
content: article.content,
|
|
48
|
+
tags: article.tags,
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
// mode === 'all': generate image too
|
|
52
|
+
let imageData: string | undefined;
|
|
53
|
+
let imageAlt: string | undefined;
|
|
54
|
+
try {
|
|
55
|
+
const imageGenerator = new ImageGenerator({ apiKey });
|
|
56
|
+
const image = await imageGenerator.generate(article.title);
|
|
57
|
+
imageData = image.base64;
|
|
58
|
+
imageAlt = image.alt;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.warn(
|
|
61
|
+
'Image generation failed, continuing without image:',
|
|
62
|
+
error
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await service.markGenerated(id, {
|
|
67
|
+
title: article.title,
|
|
68
|
+
description: article.description,
|
|
69
|
+
content: article.content,
|
|
70
|
+
tags: article.tags,
|
|
71
|
+
imageData,
|
|
72
|
+
imageAlt,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} else if (mode === 'image') {
|
|
76
|
+
// Use existing title or fallback to input
|
|
77
|
+
const title = post.generatedTitle || post.input;
|
|
78
|
+
const imageGenerator = new ImageGenerator({ apiKey });
|
|
79
|
+
const image = await imageGenerator.generate(title);
|
|
80
|
+
|
|
81
|
+
await service.updateImage(id, {
|
|
82
|
+
imageData: image.base64 || '',
|
|
83
|
+
imageAlt: image.alt,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const updated = await service.getById(id);
|
|
88
|
+
|
|
89
|
+
return jsonResponse({ post: updated });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Scheduler generate error:', error);
|
|
92
|
+
return jsonError(error);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { SchedulerService } from '../../services/SchedulerService';
|
|
3
|
+
import { AstroSchedulerDBAdapter } from '../../services/SchedulerDBAdapter';
|
|
4
|
+
import { Article } from '../../domain/entities/Article';
|
|
5
|
+
import { FileWriter } from '../../services/FileWriter';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { convertToWebP } from '../../services/ImageConverter';
|
|
8
|
+
import { contentPath } from 'virtual:nca-ai-cms/config';
|
|
9
|
+
import { jsonResponse, jsonError } from '../_utils';
|
|
10
|
+
|
|
11
|
+
function getService(): SchedulerService {
|
|
12
|
+
return new SchedulerService(new AstroSchedulerDBAdapter());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function publishPost(
|
|
16
|
+
service: SchedulerService,
|
|
17
|
+
postId: string
|
|
18
|
+
): Promise<{ id: string; publishedPath: string }> {
|
|
19
|
+
const post = await service.getById(postId);
|
|
20
|
+
|
|
21
|
+
if (!post.canPublish()) {
|
|
22
|
+
throw new Error(`Cannot publish: post is in status "${post.status}"`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!post.generatedTitle || !post.generatedContent) {
|
|
26
|
+
throw new Error('Cannot publish: missing generated content');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create article with the scheduled date (not today)
|
|
30
|
+
const articleProps = {
|
|
31
|
+
title: post.generatedTitle,
|
|
32
|
+
description: post.generatedDescription || '',
|
|
33
|
+
content: post.generatedContent,
|
|
34
|
+
date: post.scheduledDate,
|
|
35
|
+
tags: post.parsedTags,
|
|
36
|
+
image: './hero.webp',
|
|
37
|
+
...(post.generatedImageAlt ? { imageAlt: post.generatedImageAlt } : {}),
|
|
38
|
+
contentPath,
|
|
39
|
+
};
|
|
40
|
+
const article = new Article(articleProps);
|
|
41
|
+
|
|
42
|
+
// Write article to filesystem
|
|
43
|
+
const writer = new FileWriter();
|
|
44
|
+
await writer.write(article);
|
|
45
|
+
|
|
46
|
+
// Write image if available
|
|
47
|
+
if (post.generatedImageData) {
|
|
48
|
+
const imagePath = path.join(process.cwd(), article.folderPath, 'hero.webp');
|
|
49
|
+
await convertToWebP(post.generatedImageData, imagePath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Mark as published in DB
|
|
53
|
+
await service.markPublished(post.id, article.folderPath);
|
|
54
|
+
|
|
55
|
+
return { id: post.id, publishedPath: article.folderPath };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
59
|
+
try {
|
|
60
|
+
const data = await request.json();
|
|
61
|
+
const service = getService();
|
|
62
|
+
|
|
63
|
+
// Auto-publish all due posts
|
|
64
|
+
if (data.mode === 'auto') {
|
|
65
|
+
const duePosts = await service.getDuePosts();
|
|
66
|
+
const results: { id: string; publishedPath: string }[] = [];
|
|
67
|
+
const failed: { id: string; error: string }[] = [];
|
|
68
|
+
|
|
69
|
+
for (const post of duePosts) {
|
|
70
|
+
try {
|
|
71
|
+
const result = await publishPost(service, post.id);
|
|
72
|
+
results.push(result);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const message =
|
|
75
|
+
error instanceof Error ? error.message : String(error);
|
|
76
|
+
console.error(`Failed to auto-publish ${post.id}:`, error);
|
|
77
|
+
failed.push({ id: post.id, error: message });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return jsonResponse({ published: results, failed });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Publish single post
|
|
85
|
+
if (!data.id) {
|
|
86
|
+
return jsonError('id or mode:"auto" is required', 400);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = await publishPost(service, data.id);
|
|
90
|
+
|
|
91
|
+
return jsonResponse({ success: true, ...result });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Scheduler publish error:', error);
|
|
94
|
+
return jsonError(error);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { SchedulerService } from '../services/SchedulerService';
|
|
4
|
+
import { AstroSchedulerDBAdapter } from '../services/SchedulerDBAdapter';
|
|
5
|
+
import { jsonResponse, jsonError } from './_utils';
|
|
6
|
+
|
|
7
|
+
const CreateScheduledPostSchema = z.object({
|
|
8
|
+
input: z.string().min(1, 'input is required'),
|
|
9
|
+
scheduledDate: z.string().min(1, 'scheduledDate is required'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function getService(): SchedulerService {
|
|
13
|
+
return new SchedulerService(new AstroSchedulerDBAdapter());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const GET: APIRoute = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const service = getService();
|
|
19
|
+
const posts = await service.list();
|
|
20
|
+
return jsonResponse({ posts });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Scheduler list error:', error);
|
|
23
|
+
return jsonError(error);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
28
|
+
try {
|
|
29
|
+
const body = await request.json();
|
|
30
|
+
const parsed = CreateScheduledPostSchema.safeParse(body);
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return jsonError(parsed.error.errors[0]?.message ?? 'Invalid request', 400);
|
|
33
|
+
}
|
|
34
|
+
const data = parsed.data;
|
|
35
|
+
|
|
36
|
+
const service = getService();
|
|
37
|
+
const post = await service.create({
|
|
38
|
+
input: data.input,
|
|
39
|
+
scheduledDate: new Date(data.scheduledDate),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return jsonResponse({ post }, 201);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Scheduler create error:', error);
|
|
45
|
+
const status =
|
|
46
|
+
error instanceof Error && error.message.includes('already scheduled')
|
|
47
|
+
? 409
|
|
48
|
+
: 500;
|
|
49
|
+
return jsonError(error, status);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { TabType } from './editor/types';
|
|
3
|
+
import { styles } from './editor/styles';
|
|
4
|
+
import { useTabNavigation } from './editor/useTabNavigation';
|
|
5
|
+
import {
|
|
6
|
+
GenerateTabProvider,
|
|
7
|
+
GenerateTabControls,
|
|
8
|
+
GenerateTabPreview,
|
|
9
|
+
} from './editor/GenerateTab';
|
|
10
|
+
import { SettingsTab } from './editor/SettingsTab';
|
|
11
|
+
import { PlannerTab } from './editor/PlannerTab';
|
|
12
|
+
|
|
13
|
+
const TABS: { key: TabType; label: string }[] = [
|
|
14
|
+
{ key: 'generate', label: 'Generieren' },
|
|
15
|
+
{ key: 'planner', label: 'Planer' },
|
|
16
|
+
{ key: 'settings', label: 'Einstellungen' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export default function Editor() {
|
|
20
|
+
const [activeTab, setActiveTab] = useState<TabType>('generate');
|
|
21
|
+
|
|
22
|
+
const tabIndex = TABS.findIndex((t) => t.key === activeTab);
|
|
23
|
+
const { handleTabKeyDown, tabListRef } = useTabNavigation({
|
|
24
|
+
tabCount: TABS.length,
|
|
25
|
+
activeIndex: tabIndex,
|
|
26
|
+
onActivate: (index) => { const tab = TABS[index]; if (tab) setActiveTab(tab.key); },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const isFullWidth = activeTab !== 'generate';
|
|
30
|
+
|
|
31
|
+
const handleLogout = async () => {
|
|
32
|
+
await fetch('/api/auth/logout', { method: 'POST' });
|
|
33
|
+
window.location.href = '/login';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div style={styles.container}>
|
|
38
|
+
<div style={styles.headerRow}>
|
|
39
|
+
<div
|
|
40
|
+
ref={tabListRef}
|
|
41
|
+
role="tablist"
|
|
42
|
+
aria-label="Editor-Tabs"
|
|
43
|
+
style={styles.tabNav}
|
|
44
|
+
>
|
|
45
|
+
{TABS.map((tab) => (
|
|
46
|
+
<button
|
|
47
|
+
key={tab.key}
|
|
48
|
+
type="button"
|
|
49
|
+
role="tab"
|
|
50
|
+
aria-selected={activeTab === tab.key}
|
|
51
|
+
aria-controls={`panel-${tab.key}`}
|
|
52
|
+
id={`tab-${tab.key}`}
|
|
53
|
+
tabIndex={activeTab === tab.key ? 0 : -1}
|
|
54
|
+
style={
|
|
55
|
+
activeTab === tab.key ? styles.tabActive : styles.tab
|
|
56
|
+
}
|
|
57
|
+
onClick={() => setActiveTab(tab.key)}
|
|
58
|
+
onKeyDown={handleTabKeyDown}
|
|
59
|
+
>
|
|
60
|
+
{tab.label}
|
|
61
|
+
</button>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
style={styles.logoutButton}
|
|
67
|
+
onClick={handleLogout}
|
|
68
|
+
>
|
|
69
|
+
Abmelden
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{activeTab === 'generate' && (
|
|
74
|
+
<GenerateTabProvider>
|
|
75
|
+
<div
|
|
76
|
+
id="panel-generate"
|
|
77
|
+
role="tabpanel"
|
|
78
|
+
aria-labelledby="tab-generate"
|
|
79
|
+
style={{
|
|
80
|
+
display: 'grid',
|
|
81
|
+
gridTemplateColumns: '380px 1fr',
|
|
82
|
+
gap: '1.5rem',
|
|
83
|
+
alignItems: 'start',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<GenerateTabControls />
|
|
87
|
+
<GenerateTabPreview />
|
|
88
|
+
</div>
|
|
89
|
+
</GenerateTabProvider>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{activeTab === 'planner' && (
|
|
93
|
+
<div
|
|
94
|
+
id="panel-planner"
|
|
95
|
+
role="tabpanel"
|
|
96
|
+
aria-labelledby="tab-planner"
|
|
97
|
+
style={isFullWidth ? styles.panelFullWidth : undefined}
|
|
98
|
+
>
|
|
99
|
+
<PlannerTab />
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{activeTab === 'settings' && (
|
|
104
|
+
<div
|
|
105
|
+
id="panel-settings"
|
|
106
|
+
role="tabpanel"
|
|
107
|
+
aria-labelledby="tab-settings"
|
|
108
|
+
style={isFullWidth ? styles.panelFullWidth : undefined}
|
|
109
|
+
>
|
|
110
|
+
<SettingsTab />
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|