SEction 9-10

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-01 08:10:24 +00:00
parent 0340016932
commit 41e124d8e8
11 changed files with 2180 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
/**
* Step 4: Add Keywords
* Initial keyword input for content pipeline
*/
import React, { useState } from 'react';
import Button from '../../ui/button/Button';
import { Card } from '../../ui/card';
import Badge from '../../ui/badge/Badge';
import Alert from '../../ui/alert/Alert';
import {
ArrowRightIcon,
ArrowLeftIcon,
ListIcon,
PlusIcon,
CloseIcon,
} from '../../../icons';
import { createKeyword } from '../../../services/api';
import { useToast } from '../../ui/toast/ToastContainer';
import type { WizardData } from '../OnboardingWizard';
interface Step4AddKeywordsProps {
data: WizardData;
updateData: (updates: Partial<WizardData>) => void;
onNext: () => void;
onBack: () => void;
onSkip: () => void;
}
export default function Step4AddKeywords({
data,
updateData,
onNext,
onBack,
onSkip
}: Step4AddKeywordsProps) {
const toast = useToast();
const [keywords, setKeywords] = useState<string[]>([]);
const [inputValue, setInputValue] = useState('');
const [isAdding, setIsAdding] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleAddKeyword = () => {
const keyword = inputValue.trim();
if (!keyword) return;
if (keywords.includes(keyword)) {
toast.warning('Keyword already added');
return;
}
if (keywords.length >= 50) {
toast.warning('Maximum 50 keywords allowed in onboarding');
return;
}
setKeywords([...keywords, keyword]);
setInputValue('');
};
const handleRemoveKeyword = (keyword: string) => {
setKeywords(keywords.filter(k => k !== keyword));
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddKeyword();
}
};
const handlePaste = (e: React.ClipboardEvent) => {
e.preventDefault();
const text = e.clipboardData.getData('text');
const newKeywords = text
.split(/[\n,;]+/)
.map(k => k.trim())
.filter(k => k && !keywords.includes(k));
const totalKeywords = [...keywords, ...newKeywords].slice(0, 50);
setKeywords(totalKeywords);
if (newKeywords.length > 0) {
toast.success(`Added ${Math.min(newKeywords.length, 50 - keywords.length)} keywords`);
}
};
const handleSubmitKeywords = async () => {
if (!data.createdSiteId || keywords.length === 0) return;
setError(null);
setIsAdding(true);
try {
// Create keywords one by one (could be optimized with bulk endpoint later)
let successCount = 0;
for (const keyword of keywords) {
try {
await createKeyword({
keyword,
status: 'unprocessed',
});
successCount++;
} catch (err) {
console.warn(`Failed to add keyword "${keyword}":`, err);
}
}
if (successCount > 0) {
updateData({
keywordsAdded: true,
keywordsCount: successCount
});
toast.success(`Added ${successCount} keywords to your pipeline!`);
onNext();
} else {
setError('Failed to add keywords. Please try again.');
}
} catch (err: any) {
setError(err.message || 'Failed to add keywords');
} finally {
setIsAdding(false);
}
};
const SUGGESTIONS = [
'best [product] for [use case]',
'how to [action] with [tool]',
'[topic] guide for beginners',
'[industry] trends 2025',
'[problem] solutions for [audience]',
];
return (
<div>
<div className="mb-6">
<div className="flex items-center gap-2 mb-2">
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
Add Target Keywords
</h2>
<Badge tone="neutral" variant="soft">Optional</Badge>
</div>
<p className="text-gray-600 dark:text-gray-400">
Add keywords to start your content pipeline. These will be used to generate content ideas and articles.
</p>
</div>
{error && (
<Alert variant="error" title="Error" message={error} />
)}
{/* Keyword Input */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Enter keywords (press Enter or paste multiple)
</label>
<div className="flex gap-2">
<div className="relative flex-1">
<ListIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder="Enter a keyword..."
className="w-full pl-10 pr-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent"
/>
</div>
<Button
variant="outline"
onClick={handleAddKeyword}
disabled={!inputValue.trim()}
>
<PlusIcon className="w-4 h-4" />
</Button>
</div>
<p className="text-xs text-gray-500 mt-1">
Tip: Paste a comma-separated list or one keyword per line
</p>
</div>
{/* Keywords List */}
<Card className="p-4 mb-4 min-h-[120px] bg-gray-50 dark:bg-gray-800">
{keywords.length === 0 ? (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
<ListIcon className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No keywords added yet</p>
<p className="text-xs">Start typing or paste keywords above</p>
</div>
) : (
<div className="flex flex-wrap gap-2">
{keywords.map((keyword) => (
<Badge
key={keyword}
tone="neutral"
variant="soft"
className="gap-1 pr-1"
>
{keyword}
<button
onClick={() => handleRemoveKeyword(keyword)}
className="ml-1 p-0.5 hover:bg-gray-300 dark:hover:bg-gray-600 rounded"
>
<CloseIcon className="w-3 h-3" />
</button>
</Badge>
))}
</div>
)}
</Card>
<div className="flex items-center justify-between text-sm text-gray-500 mb-4">
<span>{keywords.length} keyword{keywords.length !== 1 ? 's' : ''} added</span>
{keywords.length > 0 && (
<button
onClick={() => setKeywords([])}
className="text-red-500 hover:text-red-600"
>
Clear all
</button>
)}
</div>
{/* Keyword Suggestions */}
<Card className="p-4 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 mb-6">
<h4 className="font-medium text-blue-900 dark:text-blue-100 text-sm mb-2">
💡 Keyword Ideas
</h4>
<div className="flex flex-wrap gap-2">
{SUGGESTIONS.map((suggestion, index) => (
<span
key={index}
className="text-xs text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-800/50 px-2 py-1 rounded"
>
{suggestion}
</span>
))}
</div>
</Card>
{/* Info Alert */}
<Alert
variant="info"
title="Add keywords later"
message="You can add more keywords later from the Planner page. The automation will process these keywords and generate content automatically."
/>
{/* Actions */}
<div className="flex items-center justify-between">
<Button
variant="ghost"
onClick={onBack}
className="gap-2"
>
<ArrowLeftIcon className="w-4 h-4" />
Back
</Button>
<div className="flex gap-2">
<Button
variant="ghost"
onClick={onSkip}
>
Skip for now
</Button>
<Button
variant="primary"
onClick={handleSubmitKeywords}
disabled={isAdding || keywords.length === 0}
className="gap-2"
>
{isAdding ? 'Adding...' : `Add ${keywords.length} Keyword${keywords.length !== 1 ? 's' : ''}`}
<ArrowRightIcon className="w-4 h-4" />
</Button>
</div>
</div>
</div>
);
}