Refactor keyword handling: Replace 'intent' with 'country' across backend and frontend
- Updated AutomationService to include estimated_word_count. - Increased stage_1_batch_size from 20 to 50 in AutomationViewSet. - Changed Keywords model to replace 'intent' property with 'country'. - Adjusted ClusteringService to allow a maximum of 50 keywords for clustering. - Modified admin and management commands to remove 'intent' and use 'country' instead. - Updated serializers to reflect the change from 'intent' to 'country'. - Adjusted views and filters to use 'country' instead of 'intent'. - Updated frontend forms, filters, and pages to replace 'intent' with 'country'. - Added migration to remove 'intent' field and add 'country' field to SeedKeyword model.
This commit is contained in:
@@ -97,7 +97,6 @@ class AutoClusterFunction(BaseAIFunction):
|
|||||||
'keyword': kw.keyword,
|
'keyword': kw.keyword,
|
||||||
'volume': kw.volume,
|
'volume': kw.volume,
|
||||||
'difficulty': kw.difficulty,
|
'difficulty': kw.difficulty,
|
||||||
'intent': kw.intent,
|
|
||||||
}
|
}
|
||||||
for kw in keywords
|
for kw in keywords
|
||||||
],
|
],
|
||||||
@@ -111,7 +110,7 @@ class AutoClusterFunction(BaseAIFunction):
|
|||||||
|
|
||||||
# Format keywords
|
# Format keywords
|
||||||
keywords_text = '\n'.join([
|
keywords_text = '\n'.join([
|
||||||
f"- {kw['keyword']} (Volume: {kw['volume']}, Difficulty: {kw['difficulty']}, Intent: {kw['intent']})"
|
f"- {kw['keyword']} (Volume: {kw['volume']}, Difficulty: {kw['difficulty']})"
|
||||||
for kw in keyword_data
|
for kw in keyword_data
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class IntegrationTestBase(TestCase):
|
|||||||
sector=self.industry_sector,
|
sector=self.industry_sector,
|
||||||
volume=1000,
|
volume=1000,
|
||||||
difficulty=50,
|
difficulty=50,
|
||||||
intent="informational"
|
country="US"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Authenticate client
|
# Authenticate client
|
||||||
|
|||||||
@@ -548,18 +548,18 @@ class IndustrySectorAdmin(Igny8ModelAdmin):
|
|||||||
@admin.register(SeedKeyword)
|
@admin.register(SeedKeyword)
|
||||||
class SeedKeywordAdmin(Igny8ModelAdmin):
|
class SeedKeywordAdmin(Igny8ModelAdmin):
|
||||||
"""SeedKeyword admin - Global reference data, no account filtering"""
|
"""SeedKeyword admin - Global reference data, no account filtering"""
|
||||||
list_display = ['keyword', 'industry', 'sector', 'volume', 'difficulty', 'intent', 'is_active', 'created_at']
|
list_display = ['keyword', 'industry', 'sector', 'volume', 'difficulty', 'country', 'is_active', 'created_at']
|
||||||
list_filter = ['is_active', 'industry', 'sector', 'intent']
|
list_filter = ['is_active', 'industry', 'sector', 'country']
|
||||||
search_fields = ['keyword']
|
search_fields = ['keyword']
|
||||||
readonly_fields = ['created_at', 'updated_at']
|
readonly_fields = ['created_at', 'updated_at']
|
||||||
actions = ['delete_selected'] # Enable bulk delete
|
actions = ['delete_selected'] # Enable bulk delete
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Keyword Info', {
|
('Keyword Info', {
|
||||||
'fields': ('keyword', 'industry', 'sector', 'is_active')
|
'fields': ('keyword', 'industry', 'sector', 'country', 'is_active')
|
||||||
}),
|
}),
|
||||||
('SEO Metrics', {
|
('SEO Metrics', {
|
||||||
'fields': ('volume', 'difficulty', 'intent')
|
'fields': ('volume', 'difficulty')
|
||||||
}),
|
}),
|
||||||
('Timestamps', {
|
('Timestamps', {
|
||||||
'fields': ('created_at', 'updated_at')
|
'fields': ('created_at', 'updated_at')
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.2.9 on 2025-12-17 06:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('igny8_core_auth', '0017_add_history_tracking'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name='seedkeyword',
|
||||||
|
name='igny8_seed__intent_15020d_idx',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='seedkeyword',
|
||||||
|
name='intent',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='seedkeyword',
|
||||||
|
name='country',
|
||||||
|
field=models.CharField(choices=[('US', 'United States'), ('CA', 'Canada'), ('GB', 'United Kingdom'), ('AE', 'United Arab Emirates'), ('AU', 'Australia'), ('IN', 'India'), ('PK', 'Pakistan')], default='US', help_text='Target country for this keyword', max_length=2),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='seedkeyword',
|
||||||
|
index=models.Index(fields=['country'], name='igny8_seed__country_4127a5_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -517,11 +517,14 @@ class SeedKeyword(models.Model):
|
|||||||
These are canonical keywords that can be imported into account-specific Keywords.
|
These are canonical keywords that can be imported into account-specific Keywords.
|
||||||
Non-deletable global reference data.
|
Non-deletable global reference data.
|
||||||
"""
|
"""
|
||||||
INTENT_CHOICES = [
|
COUNTRY_CHOICES = [
|
||||||
('informational', 'Informational'),
|
('US', 'United States'),
|
||||||
('navigational', 'Navigational'),
|
('CA', 'Canada'),
|
||||||
('commercial', 'Commercial'),
|
('GB', 'United Kingdom'),
|
||||||
('transactional', 'Transactional'),
|
('AE', 'United Arab Emirates'),
|
||||||
|
('AU', 'Australia'),
|
||||||
|
('IN', 'India'),
|
||||||
|
('PK', 'Pakistan'),
|
||||||
]
|
]
|
||||||
|
|
||||||
keyword = models.CharField(max_length=255, db_index=True)
|
keyword = models.CharField(max_length=255, db_index=True)
|
||||||
@@ -533,7 +536,7 @@ class SeedKeyword(models.Model):
|
|||||||
validators=[MinValueValidator(0), MaxValueValidator(100)],
|
validators=[MinValueValidator(0), MaxValueValidator(100)],
|
||||||
help_text='Keyword difficulty (0-100)'
|
help_text='Keyword difficulty (0-100)'
|
||||||
)
|
)
|
||||||
intent = models.CharField(max_length=50, choices=INTENT_CHOICES, default='informational')
|
country = models.CharField(max_length=2, choices=COUNTRY_CHOICES, default='US', help_text='Target country for this keyword')
|
||||||
is_active = models.BooleanField(default=True, db_index=True)
|
is_active = models.BooleanField(default=True, db_index=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@@ -547,7 +550,7 @@ class SeedKeyword(models.Model):
|
|||||||
models.Index(fields=['keyword']),
|
models.Index(fields=['keyword']),
|
||||||
models.Index(fields=['industry', 'sector']),
|
models.Index(fields=['industry', 'sector']),
|
||||||
models.Index(fields=['industry', 'sector', 'is_active']),
|
models.Index(fields=['industry', 'sector', 'is_active']),
|
||||||
models.Index(fields=['intent']),
|
models.Index(fields=['country']),
|
||||||
]
|
]
|
||||||
ordering = ['keyword']
|
ordering = ['keyword']
|
||||||
|
|
||||||
|
|||||||
@@ -524,14 +524,14 @@ class SeedKeywordSerializer(serializers.ModelSerializer):
|
|||||||
industry_slug = serializers.CharField(source='industry.slug', read_only=True)
|
industry_slug = serializers.CharField(source='industry.slug', read_only=True)
|
||||||
sector_name = serializers.CharField(source='sector.name', read_only=True)
|
sector_name = serializers.CharField(source='sector.name', read_only=True)
|
||||||
sector_slug = serializers.CharField(source='sector.slug', read_only=True)
|
sector_slug = serializers.CharField(source='sector.slug', read_only=True)
|
||||||
intent_display = serializers.CharField(source='get_intent_display', read_only=True)
|
country_display = serializers.CharField(source='get_country_display', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SeedKeyword
|
model = SeedKeyword
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'keyword', 'industry', 'industry_name', 'industry_slug',
|
'id', 'keyword', 'industry', 'industry_name', 'industry_slug',
|
||||||
'sector', 'sector_name', 'sector_slug',
|
'sector', 'sector_name', 'sector_slug',
|
||||||
'volume', 'difficulty', 'intent', 'intent_display',
|
'volume', 'difficulty', 'country', 'country_display',
|
||||||
'is_active', 'created_at', 'updated_at'
|
'is_active', 'created_at', 'updated_at'
|
||||||
]
|
]
|
||||||
read_only_fields = ['created_at', 'updated_at']
|
read_only_fields = ['created_at', 'updated_at']
|
||||||
|
|||||||
@@ -839,7 +839,7 @@ class SeedKeywordViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
search_fields = ['keyword']
|
search_fields = ['keyword']
|
||||||
ordering_fields = ['keyword', 'volume', 'difficulty', 'created_at']
|
ordering_fields = ['keyword', 'volume', 'difficulty', 'created_at']
|
||||||
ordering = ['keyword']
|
ordering = ['keyword']
|
||||||
filterset_fields = ['industry', 'sector', 'intent', 'is_active']
|
filterset_fields = ['industry', 'sector', 'country', 'is_active']
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
"""Override retrieve to return unified format"""
|
"""Override retrieve to return unified format"""
|
||||||
@@ -877,7 +877,7 @@ class SeedKeywordViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
def import_seed_keywords(self, request):
|
def import_seed_keywords(self, request):
|
||||||
"""
|
"""
|
||||||
Import seed keywords from CSV (Admin/Superuser only).
|
Import seed keywords from CSV (Admin/Superuser only).
|
||||||
Expected columns: keyword, industry_name, sector_name, volume, difficulty, intent
|
Expected columns: keyword, industry_name, sector_name, volume, difficulty, country
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@@ -960,7 +960,7 @@ class SeedKeywordViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
sector=sector,
|
sector=sector,
|
||||||
volume=int(row.get('volume', 0) or 0),
|
volume=int(row.get('volume', 0) or 0),
|
||||||
difficulty=int(row.get('difficulty', 0) or 0),
|
difficulty=int(row.get('difficulty', 0) or 0),
|
||||||
intent=row.get('intent', 'informational') or 'informational',
|
country=row.get('country', 'US') or 'US',
|
||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
imported_count += 1
|
imported_count += 1
|
||||||
@@ -1487,9 +1487,9 @@ def seedkeyword_csv_template(request):
|
|||||||
response['Content-Disposition'] = 'attachment; filename="seedkeyword_template.csv"'
|
response['Content-Disposition'] = 'attachment; filename="seedkeyword_template.csv"'
|
||||||
|
|
||||||
writer = csv.writer(response)
|
writer = csv.writer(response)
|
||||||
writer.writerow(['keyword', 'industry', 'sector', 'volume', 'difficulty', 'intent', 'is_active'])
|
writer.writerow(['keyword', 'industry', 'sector', 'volume', 'difficulty', 'country', 'is_active'])
|
||||||
writer.writerow(['python programming', 'Technology', 'Software Development', '10000', '45', 'Informational', 'true'])
|
writer.writerow(['python programming', 'Technology', 'Software Development', '10000', '45', 'US', 'true'])
|
||||||
writer.writerow(['medical software', 'Healthcare', 'Healthcare IT', '5000', '60', 'Commercial', 'true'])
|
writer.writerow(['medical software', 'Healthcare', 'Healthcare IT', '5000', '60', 'CA', 'true'])
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -1534,7 +1534,7 @@ def seedkeyword_csv_import(request):
|
|||||||
defaults={
|
defaults={
|
||||||
'volume': int(row.get('volume', 0)),
|
'volume': int(row.get('volume', 0)),
|
||||||
'difficulty': int(row.get('difficulty', 0)),
|
'difficulty': int(row.get('difficulty', 0)),
|
||||||
'intent': row.get('intent', 'Informational'),
|
'country': row.get('country', 'US'),
|
||||||
'is_active': is_active
|
'is_active': is_active
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class AutomationConfig(models.Model):
|
|||||||
scheduled_time = models.TimeField(default='02:00', help_text="Time to run (e.g., 02:00)")
|
scheduled_time = models.TimeField(default='02:00', help_text="Time to run (e.g., 02:00)")
|
||||||
|
|
||||||
# Batch sizes per stage
|
# Batch sizes per stage
|
||||||
stage_1_batch_size = models.IntegerField(default=20, help_text="Keywords per batch")
|
stage_1_batch_size = models.IntegerField(default=50, help_text="Keywords per batch")
|
||||||
stage_2_batch_size = models.IntegerField(default=1, help_text="Clusters at a time")
|
stage_2_batch_size = models.IntegerField(default=1, help_text="Clusters at a time")
|
||||||
stage_3_batch_size = models.IntegerField(default=20, help_text="Ideas per batch")
|
stage_3_batch_size = models.IntegerField(default=20, help_text="Ideas per batch")
|
||||||
stage_4_batch_size = models.IntegerField(default=1, help_text="Tasks - sequential")
|
stage_4_batch_size = models.IntegerField(default=1, help_text="Tasks - sequential")
|
||||||
|
|||||||
@@ -637,6 +637,7 @@ class AutomationService:
|
|||||||
content_type=idea.content_type or 'post',
|
content_type=idea.content_type or 'post',
|
||||||
content_structure=idea.content_structure or 'article',
|
content_structure=idea.content_structure or 'article',
|
||||||
keywords=keywords_str,
|
keywords=keywords_str,
|
||||||
|
word_count=idea.estimated_word_count,
|
||||||
status='queued',
|
status='queued',
|
||||||
account=idea.account,
|
account=idea.account,
|
||||||
site=idea.site,
|
site=idea.site,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class AutomationViewSet(viewsets.ViewSet):
|
|||||||
"is_enabled": true,
|
"is_enabled": true,
|
||||||
"frequency": "daily",
|
"frequency": "daily",
|
||||||
"scheduled_time": "02:00",
|
"scheduled_time": "02:00",
|
||||||
"stage_1_batch_size": 20,
|
"stage_1_batch_size": 50,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ class Keywords(SoftDeletableModel, SiteSectorBaseModel):
|
|||||||
return self.difficulty_override if self.difficulty_override is not None else (self.seed_keyword.difficulty if self.seed_keyword else 0)
|
return self.difficulty_override if self.difficulty_override is not None else (self.seed_keyword.difficulty if self.seed_keyword else 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def intent(self):
|
def country(self):
|
||||||
"""Get intent from seed_keyword"""
|
"""Get country from seed_keyword"""
|
||||||
return self.seed_keyword.intent if self.seed_keyword else 'informational'
|
return self.seed_keyword.country if self.seed_keyword else 'US'
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Validate that seed_keyword's industry/sector matches site's industry/sector"""
|
"""Validate that seed_keyword's industry/sector matches site's industry/sector"""
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ class ClusteringService:
|
|||||||
'error': 'No keyword IDs provided'
|
'error': 'No keyword IDs provided'
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(keyword_ids) > 20:
|
if len(keyword_ids) > 50:
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': 'Maximum 20 keywords allowed for clustering'
|
'error': 'Maximum 50 keywords allowed for clustering'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check credits (fixed cost per clustering operation)
|
# Check credits (fixed cost per clustering operation)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class KeywordsResource(resources.ModelResource):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Keywords
|
model = Keywords
|
||||||
fields = ('id', 'keyword', 'seed_keyword__keyword', 'site__name', 'sector__name',
|
fields = ('id', 'keyword', 'seed_keyword__keyword', 'site__name', 'sector__name',
|
||||||
'cluster__name', 'volume', 'difficulty', 'intent', 'status', 'created_at')
|
'cluster__name', 'volume', 'difficulty', 'country', 'status', 'created_at')
|
||||||
export_order = fields
|
export_order = fields
|
||||||
|
|
||||||
|
|
||||||
@@ -55,11 +55,11 @@ class ClustersAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
|||||||
@admin.register(Keywords)
|
@admin.register(Keywords)
|
||||||
class KeywordsAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
class KeywordsAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||||
resource_class = KeywordsResource
|
resource_class = KeywordsResource
|
||||||
list_display = ['keyword', 'seed_keyword', 'site', 'sector', 'cluster', 'volume', 'difficulty', 'intent', 'status', 'created_at']
|
list_display = ['keyword', 'seed_keyword', 'site', 'sector', 'cluster', 'volume', 'difficulty', 'country', 'status', 'created_at']
|
||||||
list_editable = ['status'] # Enable inline editing for status
|
list_editable = ['status'] # Enable inline editing for status
|
||||||
list_filter = [
|
list_filter = [
|
||||||
('status', ChoicesDropdownFilter),
|
('status', ChoicesDropdownFilter),
|
||||||
('intent', ChoicesDropdownFilter),
|
('country', ChoicesDropdownFilter),
|
||||||
('site', RelatedDropdownFilter),
|
('site', RelatedDropdownFilter),
|
||||||
('sector', RelatedDropdownFilter),
|
('sector', RelatedDropdownFilter),
|
||||||
('cluster', RelatedDropdownFilter),
|
('cluster', RelatedDropdownFilter),
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ class Command(BaseCommand):
|
|||||||
'account': site.account,
|
'account': site.account,
|
||||||
'volume': 1000 + (created_count * 100), # Varying volumes
|
'volume': 1000 + (created_count * 100), # Varying volumes
|
||||||
'difficulty': 30 + (created_count * 10), # Varying difficulty (0-100 scale)
|
'difficulty': 30 + (created_count * 10), # Varying difficulty (0-100 scale)
|
||||||
'intent': 'informational' if created_count % 2 == 0 else 'commercial',
|
|
||||||
'status': 'active',
|
'status': 'active',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -243,7 +243,6 @@ class Command(BaseCommand):
|
|||||||
'account': site.account,
|
'account': site.account,
|
||||||
'volume': 500 + (created_count * 50), # Varying volumes
|
'volume': 500 + (created_count * 50), # Varying volumes
|
||||||
'difficulty': 20 + (created_count * 8), # Varying difficulty (0-100 scale)
|
'difficulty': 20 + (created_count * 8), # Varying difficulty (0-100 scale)
|
||||||
'intent': 'informational' if created_count % 2 == 0 else 'commercial',
|
|
||||||
'status': 'active',
|
'status': 'active',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class Command(BaseCommand):
|
|||||||
sector=industry_sector,
|
sector=industry_sector,
|
||||||
volume=keyword.volume or 0,
|
volume=keyword.volume or 0,
|
||||||
difficulty=keyword.difficulty or 0,
|
difficulty=keyword.difficulty or 0,
|
||||||
intent=keyword.intent or 'informational',
|
country='US', # Default country for migration
|
||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
created_count += 1
|
created_count += 1
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class KeywordSerializer(serializers.ModelSerializer):
|
|||||||
keyword = serializers.CharField(read_only=True) # From seed_keyword.keyword
|
keyword = serializers.CharField(read_only=True) # From seed_keyword.keyword
|
||||||
volume = serializers.IntegerField(read_only=True) # From seed_keyword.volume or volume_override
|
volume = serializers.IntegerField(read_only=True) # From seed_keyword.volume or volume_override
|
||||||
difficulty = serializers.IntegerField(read_only=True) # From seed_keyword.difficulty or difficulty_override
|
difficulty = serializers.IntegerField(read_only=True) # From seed_keyword.difficulty or difficulty_override
|
||||||
intent = serializers.CharField(read_only=True) # From seed_keyword.intent
|
country = serializers.CharField(read_only=True) # From seed_keyword.country
|
||||||
|
|
||||||
# SeedKeyword relationship
|
# SeedKeyword relationship
|
||||||
# Optional for create - can either provide seed_keyword_id OR custom keyword fields
|
# Optional for create - can either provide seed_keyword_id OR custom keyword fields
|
||||||
@@ -24,11 +24,11 @@ class KeywordSerializer(serializers.ModelSerializer):
|
|||||||
custom_keyword = serializers.CharField(write_only=True, required=False, allow_blank=False)
|
custom_keyword = serializers.CharField(write_only=True, required=False, allow_blank=False)
|
||||||
custom_volume = serializers.IntegerField(write_only=True, required=False, allow_null=True)
|
custom_volume = serializers.IntegerField(write_only=True, required=False, allow_null=True)
|
||||||
custom_difficulty = serializers.IntegerField(write_only=True, required=False, allow_null=True)
|
custom_difficulty = serializers.IntegerField(write_only=True, required=False, allow_null=True)
|
||||||
custom_intent = serializers.ChoiceField(
|
custom_country = serializers.ChoiceField(
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
choices=['informational', 'navigational', 'transactional', 'commercial'],
|
choices=['US', 'CA', 'GB', 'AE', 'AU', 'IN', 'PK'],
|
||||||
default='informational'
|
default='US'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Overrides
|
# Overrides
|
||||||
@@ -50,11 +50,11 @@ class KeywordSerializer(serializers.ModelSerializer):
|
|||||||
'keyword',
|
'keyword',
|
||||||
'volume',
|
'volume',
|
||||||
'difficulty',
|
'difficulty',
|
||||||
'intent',
|
'country',
|
||||||
'custom_keyword', # Write-only field for creating custom keywords
|
'custom_keyword', # Write-only field for creating custom keywords
|
||||||
'custom_volume', # Write-only
|
'custom_volume', # Write-only
|
||||||
'custom_difficulty', # Write-only
|
'custom_difficulty', # Write-only
|
||||||
'custom_intent', # Write-only
|
'custom_country', # Write-only
|
||||||
'volume_override',
|
'volume_override',
|
||||||
'difficulty_override',
|
'difficulty_override',
|
||||||
'cluster_id',
|
'cluster_id',
|
||||||
@@ -67,7 +67,7 @@ class KeywordSerializer(serializers.ModelSerializer):
|
|||||||
'sector_id',
|
'sector_id',
|
||||||
'account_id',
|
'account_id',
|
||||||
]
|
]
|
||||||
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id', 'keyword', 'volume', 'difficulty', 'intent']
|
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id', 'keyword', 'volume', 'difficulty', 'country']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -106,7 +106,7 @@ class KeywordSerializer(serializers.ModelSerializer):
|
|||||||
custom_keyword = validated_data.pop('custom_keyword', None)
|
custom_keyword = validated_data.pop('custom_keyword', None)
|
||||||
custom_volume = validated_data.pop('custom_volume', None)
|
custom_volume = validated_data.pop('custom_volume', None)
|
||||||
custom_difficulty = validated_data.pop('custom_difficulty', None)
|
custom_difficulty = validated_data.pop('custom_difficulty', None)
|
||||||
custom_intent = validated_data.pop('custom_intent', None) or 'informational'
|
custom_country = validated_data.pop('custom_country', None) or 'US'
|
||||||
|
|
||||||
# Get site and sector - they're passed as objects via save() in the view
|
# Get site and sector - they're passed as objects via save() in the view
|
||||||
site = validated_data.get('site')
|
site = validated_data.get('site')
|
||||||
@@ -132,7 +132,7 @@ class KeywordSerializer(serializers.ModelSerializer):
|
|||||||
defaults={
|
defaults={
|
||||||
'volume': custom_volume or 0,
|
'volume': custom_volume or 0,
|
||||||
'difficulty': custom_difficulty or 0,
|
'difficulty': custom_difficulty or 0,
|
||||||
'intent': custom_intent or 'informational',
|
'country': custom_country or 'US',
|
||||||
'is_active': True,
|
'is_active': True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -162,7 +162,7 @@ class KeywordSerializer(serializers.ModelSerializer):
|
|||||||
validated_data.pop('custom_keyword', None)
|
validated_data.pop('custom_keyword', None)
|
||||||
validated_data.pop('custom_volume', None)
|
validated_data.pop('custom_volume', None)
|
||||||
validated_data.pop('custom_difficulty', None)
|
validated_data.pop('custom_difficulty', None)
|
||||||
validated_data.pop('custom_intent', None)
|
validated_data.pop('custom_country', None)
|
||||||
|
|
||||||
# seed_keyword_id is optional for updates - only update if provided
|
# seed_keyword_id is optional for updates - only update if provided
|
||||||
if 'seed_keyword_id' in validated_data:
|
if 'seed_keyword_id' in validated_data:
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
ordering = ['-created_at'] # Default ordering (newest first)
|
ordering = ['-created_at'] # Default ordering (newest first)
|
||||||
|
|
||||||
# Filter configuration - filter by status, cluster_id, and seed_keyword fields
|
# Filter configuration - filter by status, cluster_id, and seed_keyword fields
|
||||||
filterset_fields = ['status', 'cluster_id', 'seed_keyword__intent', 'seed_keyword_id']
|
filterset_fields = ['status', 'cluster_id', 'seed_keyword__country', 'seed_keyword_id']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
@@ -475,7 +475,7 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
|
|
||||||
writer = csv.writer(response)
|
writer = csv.writer(response)
|
||||||
# Header row
|
# Header row
|
||||||
writer.writerow(['ID', 'Keyword', 'Volume', 'Difficulty', 'Intent', 'Status', 'Cluster ID', 'Created At'])
|
writer.writerow(['ID', 'Keyword', 'Volume', 'Difficulty', 'Country', 'Status', 'Cluster ID', 'Created At'])
|
||||||
|
|
||||||
# Data rows
|
# Data rows
|
||||||
for keyword in keywords:
|
for keyword in keywords:
|
||||||
@@ -484,7 +484,7 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
keyword.keyword,
|
keyword.keyword,
|
||||||
keyword.volume,
|
keyword.volume,
|
||||||
keyword.difficulty,
|
keyword.difficulty,
|
||||||
keyword.intent,
|
keyword.country,
|
||||||
keyword.status,
|
keyword.status,
|
||||||
keyword.cluster_id or '',
|
keyword.cluster_id or '',
|
||||||
keyword.created_at.isoformat() if keyword.created_at else '',
|
keyword.created_at.isoformat() if keyword.created_at else '',
|
||||||
@@ -631,12 +631,13 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Create keyword
|
# Note: This direct creation bypasses seed_keyword linkage
|
||||||
|
# Keywords should ideally be created through seed_keyword FK
|
||||||
|
# Country comes from seed_keyword.country property
|
||||||
Keywords.objects.create(
|
Keywords.objects.create(
|
||||||
keyword=keyword_text,
|
keyword=keyword_text,
|
||||||
volume=int(row.get('volume', 0) or 0),
|
volume=int(row.get('volume', 0) or 0),
|
||||||
difficulty=int(row.get('difficulty', 0) or 0),
|
difficulty=int(row.get('difficulty', 0) or 0),
|
||||||
intent=row.get('intent', 'informational') or 'informational',
|
|
||||||
status=row.get('status', 'new') or 'new',
|
status=row.get('status', 'new') or 'new',
|
||||||
site=site,
|
site=site,
|
||||||
sector=sector,
|
sector=sector,
|
||||||
@@ -1193,6 +1194,7 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
|||||||
content_structure=idea.content_structure or 'article',
|
content_structure=idea.content_structure or 'article',
|
||||||
taxonomy_term=None, # Can be set later if taxonomy is available
|
taxonomy_term=None, # Can be set later if taxonomy is available
|
||||||
keywords=keywords_str, # Comma-separated keywords string
|
keywords=keywords_str, # Comma-separated keywords string
|
||||||
|
word_count=idea.estimated_word_count, # Copy word count from idea
|
||||||
status='queued',
|
status='queued',
|
||||||
account=idea.account,
|
account=idea.account,
|
||||||
site=idea.site,
|
site=idea.site,
|
||||||
|
|||||||
@@ -1050,7 +1050,7 @@ Make sure each prompt is detailed enough for image generation, describing the vi
|
|||||||
|
|
||||||
def cluster_keywords(
|
def cluster_keywords(
|
||||||
self,
|
self,
|
||||||
keywords: List[Dict[str, Any]], # List of keyword dicts with 'keyword', 'volume', 'difficulty', 'intent'
|
keywords: List[Dict[str, Any]], # List of keyword dicts with 'keyword', 'volume', 'difficulty'
|
||||||
sector_name: Optional[str] = None,
|
sector_name: Optional[str] = None,
|
||||||
account=None,
|
account=None,
|
||||||
response_steps=None,
|
response_steps=None,
|
||||||
@@ -1069,7 +1069,7 @@ Make sure each prompt is detailed enough for image generation, describing the vi
|
|||||||
Based on reference plugin's clustering prompt.
|
Based on reference plugin's clustering prompt.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
keywords: List of keyword dicts with keyword, volume, difficulty, intent
|
keywords: List of keyword dicts with keyword, volume, difficulty
|
||||||
sector_name: Optional sector name for context
|
sector_name: Optional sector name for context
|
||||||
account: Optional account for getting custom prompts
|
account: Optional account for getting custom prompts
|
||||||
|
|
||||||
@@ -1085,7 +1085,7 @@ Make sure each prompt is detailed enough for image generation, describing the vi
|
|||||||
|
|
||||||
# Format keywords for prompt
|
# Format keywords for prompt
|
||||||
keywords_text = '\n'.join([
|
keywords_text = '\n'.join([
|
||||||
f"- {kw.get('keyword', '')} (Volume: {kw.get('volume', 0)}, Difficulty: {kw.get('difficulty', 0)}, Intent: {kw.get('intent', 'unknown')})"
|
f"- {kw.get('keyword', '')} (Volume: {kw.get('volume', 0)}, Difficulty: {kw.get('difficulty', 0)})"
|
||||||
for kw in keywords
|
for kw in keywords
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -55,16 +55,19 @@ export const getKeywordFormConfig = (options?: {
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'intent',
|
key: 'country',
|
||||||
label: 'Intent',
|
label: 'Country',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
value: 'informational',
|
value: 'US',
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
options: [
|
options: [
|
||||||
{ value: 'informational', label: 'Informational' },
|
{ value: 'US', label: 'United States' },
|
||||||
{ value: 'transactional', label: 'Transactional' },
|
{ value: 'CA', label: 'Canada' },
|
||||||
{ value: 'navigational', label: 'Navigational' },
|
{ value: 'GB', label: 'United Kingdom' },
|
||||||
{ value: 'commercial', label: 'Commercial' },
|
{ value: 'AE', label: 'United Arab Emirates' },
|
||||||
|
{ value: 'AU', label: 'Australia' },
|
||||||
|
{ value: 'IN', label: 'India' },
|
||||||
|
{ value: 'PK', label: 'Pakistan' },
|
||||||
],
|
],
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export function useImportExport(
|
|||||||
</p>
|
</p>
|
||||||
{filename === 'keywords' && (
|
{filename === 'keywords' && (
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-300 mt-2 p-2 bg-blue-50 dark:bg-blue-900/20 rounded border border-blue-200 dark:border-blue-800">
|
<p className="text-xs text-gray-600 dark:text-gray-300 mt-2 p-2 bg-blue-50 dark:bg-blue-900/20 rounded border border-blue-200 dark:border-blue-800">
|
||||||
<strong>Expected columns:</strong> keyword, volume, difficulty, intent, status
|
<strong>Expected columns:</strong> keyword, volume, difficulty, country, status
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
keywordColumn,
|
keywordColumn,
|
||||||
volumeColumn,
|
volumeColumn,
|
||||||
difficultyColumn,
|
difficultyColumn,
|
||||||
intentColumn,
|
countryColumn,
|
||||||
clusterColumn,
|
clusterColumn,
|
||||||
sectorColumn,
|
sectorColumn,
|
||||||
statusColumn,
|
statusColumn,
|
||||||
@@ -106,7 +106,7 @@ export const createKeywordsPageConfig = (
|
|||||||
keyword?: string;
|
keyword?: string;
|
||||||
volume?: number | null;
|
volume?: number | null;
|
||||||
difficulty?: number | null;
|
difficulty?: number | null;
|
||||||
intent?: string;
|
country?: string;
|
||||||
cluster_id?: number | null;
|
cluster_id?: number | null;
|
||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
@@ -116,8 +116,8 @@ export const createKeywordsPageConfig = (
|
|||||||
setSearchTerm: (value: string) => void;
|
setSearchTerm: (value: string) => void;
|
||||||
statusFilter: string;
|
statusFilter: string;
|
||||||
setStatusFilter: (value: string) => void;
|
setStatusFilter: (value: string) => void;
|
||||||
intentFilter: string;
|
countryFilter: string;
|
||||||
setIntentFilter: (value: string) => void;
|
setCountryFilter: (value: string) => void;
|
||||||
difficultyFilter: string;
|
difficultyFilter: string;
|
||||||
setDifficultyFilter: (value: string) => void;
|
setDifficultyFilter: (value: string) => void;
|
||||||
clusterFilter: string;
|
clusterFilter: string;
|
||||||
@@ -197,28 +197,23 @@ export const createKeywordsPageConfig = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...intentColumn,
|
...countryColumn,
|
||||||
sortable: false, // Backend doesn't support sorting by intent
|
sortable: false, // Backend doesn't support sorting by country
|
||||||
sortField: 'seed_keyword__intent',
|
sortField: 'seed_keyword__country',
|
||||||
render: (value: string) => {
|
render: (value: string) => {
|
||||||
// Map intent values to badge colors
|
const countryNames: Record<string, string> = {
|
||||||
// Transactional and Commercial → success (green, like active)
|
'US': 'United States',
|
||||||
// Navigational → warning (amber/yellow, like pending)
|
'CA': 'Canada',
|
||||||
// Informational → info (blue)
|
'GB': 'United Kingdom',
|
||||||
const getIntentColor = (intent: string) => {
|
'AE': 'United Arab Emirates',
|
||||||
const lowerIntent = intent?.toLowerCase() || '';
|
'AU': 'Australia',
|
||||||
if (lowerIntent === 'transactional' || lowerIntent === 'commercial') {
|
'IN': 'India',
|
||||||
return 'success'; // Green, like active status
|
'PK': 'Pakistan',
|
||||||
} else if (lowerIntent === 'navigational') {
|
|
||||||
return 'warning'; // Amber/yellow, like pending status
|
|
||||||
}
|
|
||||||
return 'info'; // Blue for informational or default
|
|
||||||
};
|
};
|
||||||
|
const displayName = countryNames[value] || value || '-';
|
||||||
const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-';
|
|
||||||
return (
|
return (
|
||||||
<Badge color={getIntentColor(value)} size="xs" variant="soft">
|
<Badge color="info" size="xs" variant="soft">
|
||||||
<span className="text-[11px] font-normal">{properCase}</span>
|
<span className="text-[11px] font-normal">{value || '-'}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -317,15 +312,18 @@ export const createKeywordsPageConfig = (
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'intent',
|
key: 'country',
|
||||||
label: 'Intent',
|
label: 'Country',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Intent' },
|
{ value: '', label: 'All Countries' },
|
||||||
{ value: 'informational', label: 'Informational' },
|
{ value: 'US', label: 'United States' },
|
||||||
{ value: 'navigational', label: 'Navigational' },
|
{ value: 'CA', label: 'Canada' },
|
||||||
{ value: 'transactional', label: 'Transactional' },
|
{ value: 'GB', label: 'United Kingdom' },
|
||||||
{ value: 'commercial', label: 'Commercial' },
|
{ value: 'AE', label: 'United Arab Emirates' },
|
||||||
|
{ value: 'AU', label: 'Australia' },
|
||||||
|
{ value: 'IN', label: 'India' },
|
||||||
|
{ value: 'PK', label: 'Pakistan' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -541,18 +539,21 @@ export const createKeywordsPageConfig = (
|
|||||||
max: 100,
|
max: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'intent',
|
key: 'country',
|
||||||
label: 'Search Intent',
|
label: 'Country',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
value: handlers.formData.intent || 'informational',
|
value: handlers.formData.country || 'US',
|
||||||
onChange: (value: any) =>
|
onChange: (value: any) =>
|
||||||
handlers.setFormData({ ...handlers.formData, intent: value }),
|
handlers.setFormData({ ...handlers.formData, country: value }),
|
||||||
required: true,
|
required: true,
|
||||||
options: [
|
options: [
|
||||||
{ value: 'informational', label: 'Informational' },
|
{ value: 'US', label: 'United States' },
|
||||||
{ value: 'navigational', label: 'Navigational' },
|
{ value: 'CA', label: 'Canada' },
|
||||||
{ value: 'transactional', label: 'Transactional' },
|
{ value: 'GB', label: 'United Kingdom' },
|
||||||
{ value: 'commercial', label: 'Commercial' },
|
{ value: 'AE', label: 'United Arab Emirates' },
|
||||||
|
{ value: 'AU', label: 'Australia' },
|
||||||
|
{ value: 'IN', label: 'India' },
|
||||||
|
{ value: 'PK', label: 'Pakistan' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ export const difficultyColumn = {
|
|||||||
width: '120px',
|
width: '120px',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const intentColumn = {
|
export const countryColumn = {
|
||||||
key: 'intent',
|
key: 'country',
|
||||||
label: 'Intent',
|
label: 'Country',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
badge: true,
|
badge: true,
|
||||||
width: '120px',
|
width: '120px',
|
||||||
|
|||||||
@@ -15,16 +15,19 @@ export const statusFilter = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const intentFilter = {
|
export const countryFilter = {
|
||||||
key: 'intent',
|
key: 'country',
|
||||||
label: 'Intent',
|
label: 'Country',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Intent' },
|
{ value: '', label: 'All Countries' },
|
||||||
{ value: 'informational', label: 'Informational' },
|
{ value: 'US', label: 'United States' },
|
||||||
{ value: 'transactional', label: 'Transactional' },
|
{ value: 'CA', label: 'Canada' },
|
||||||
{ value: 'navigational', label: 'Navigational' },
|
{ value: 'GB', label: 'United Kingdom' },
|
||||||
{ value: 'commercial', label: 'Commercial' },
|
{ value: 'AE', label: 'United Arab Emirates' },
|
||||||
|
{ value: 'AU', label: 'Australia' },
|
||||||
|
{ value: 'IN', label: 'India' },
|
||||||
|
{ value: 'PK', label: 'Pakistan' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ interface DashboardStats {
|
|||||||
mapped: number;
|
mapped: number;
|
||||||
unmapped: number;
|
unmapped: number;
|
||||||
byStatus: Record<string, number>;
|
byStatus: Record<string, number>;
|
||||||
byIntent: Record<string, number>;
|
byCountry: Record<string, number>;
|
||||||
};
|
};
|
||||||
clusters: {
|
clusters: {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -90,11 +90,11 @@ export default function PlannerDashboard() {
|
|||||||
const unmappedKeywords = keywords.filter(k => !k.cluster || k.cluster.length === 0);
|
const unmappedKeywords = keywords.filter(k => !k.cluster || k.cluster.length === 0);
|
||||||
|
|
||||||
const keywordsByStatus: Record<string, number> = {};
|
const keywordsByStatus: Record<string, number> = {};
|
||||||
const keywordsByIntent: Record<string, number> = {};
|
const keywordsByCountry: Record<string, number> = {};
|
||||||
keywords.forEach(k => {
|
keywords.forEach(k => {
|
||||||
keywordsByStatus[k.status || 'unknown'] = (keywordsByStatus[k.status || 'unknown'] || 0) + 1;
|
keywordsByStatus[k.status || 'unknown'] = (keywordsByStatus[k.status || 'unknown'] || 0) + 1;
|
||||||
if (k.intent) {
|
if (k.country) {
|
||||||
keywordsByIntent[k.intent] = (keywordsByIntent[k.intent] || 0) + 1;
|
keywordsByCountry[k.country] = (keywordsByCountry[k.country] || 0) + 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ export default function PlannerDashboard() {
|
|||||||
mapped: mappedKeywords.length,
|
mapped: mappedKeywords.length,
|
||||||
unmapped: unmappedKeywords.length,
|
unmapped: unmappedKeywords.length,
|
||||||
byStatus: keywordsByStatus,
|
byStatus: keywordsByStatus,
|
||||||
byIntent: keywordsByIntent
|
byCountry: keywordsByCountry
|
||||||
},
|
},
|
||||||
clusters: {
|
clusters: {
|
||||||
total: clusters.length,
|
total: clusters.length,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function KeywordOpportunities() {
|
|||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [intentFilter, setIntentFilter] = useState('');
|
const [countryFilter, setCountryFilter] = useState('');
|
||||||
const [difficultyFilter, setDifficultyFilter] = useState('');
|
const [difficultyFilter, setDifficultyFilter] = useState('');
|
||||||
const [volumeMin, setVolumeMin] = useState<number | ''>('');
|
const [volumeMin, setVolumeMin] = useState<number | ''>('');
|
||||||
const [volumeMax, setVolumeMax] = useState<number | ''>('');
|
const [volumeMax, setVolumeMax] = useState<number | ''>('');
|
||||||
@@ -119,7 +119,7 @@ export default function KeywordOpportunities() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchTerm) baseFilters.search = searchTerm;
|
if (searchTerm) baseFilters.search = searchTerm;
|
||||||
if (intentFilter) baseFilters.intent = intentFilter;
|
if (countryFilter) baseFilters.country = countryFilter;
|
||||||
|
|
||||||
// Fetch ALL pages to get complete dataset
|
// Fetch ALL pages to get complete dataset
|
||||||
let allResults: SeedKeyword[] = [];
|
let allResults: SeedKeyword[] = [];
|
||||||
@@ -227,7 +227,7 @@ export default function KeywordOpportunities() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, intentFilter, difficultyFilter, volumeMin, volumeMax, sortBy, sortDirection]);
|
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, volumeMin, volumeMax, sortBy, sortDirection]);
|
||||||
|
|
||||||
// Load data on mount and when filters change (excluding search - handled separately)
|
// Load data on mount and when filters change (excluding search - handled separately)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -504,28 +504,27 @@ export default function KeywordOpportunities() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'intent',
|
key: 'country',
|
||||||
label: 'Intent',
|
label: 'Country',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
sortField: 'intent',
|
sortField: 'country',
|
||||||
render: (value: string) => {
|
render: (value: string) => {
|
||||||
const getIntentColor = (intent: string) => {
|
const countryNames: Record<string, string> = {
|
||||||
const lowerIntent = intent?.toLowerCase() || '';
|
'US': 'United States',
|
||||||
if (lowerIntent === 'transactional' || lowerIntent === 'commercial') {
|
'CA': 'Canada',
|
||||||
return 'success';
|
'GB': 'United Kingdom',
|
||||||
} else if (lowerIntent === 'navigational') {
|
'AE': 'United Arab Emirates',
|
||||||
return 'warning';
|
'AU': 'Australia',
|
||||||
}
|
'IN': 'India',
|
||||||
return 'info';
|
'PK': 'Pakistan',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
color={getIntentColor(value)}
|
color="info"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={value?.toLowerCase() === 'informational' ? 'light' : undefined}
|
variant="light"
|
||||||
>
|
>
|
||||||
{value}
|
{value || '-'}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -539,15 +538,18 @@ export default function KeywordOpportunities() {
|
|||||||
placeholder: 'Search keywords...',
|
placeholder: 'Search keywords...',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'intent',
|
key: 'country',
|
||||||
label: 'Intent',
|
label: 'Country',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Intent' },
|
{ value: '', label: 'All Countries' },
|
||||||
{ value: 'informational', label: 'Informational' },
|
{ value: 'US', label: 'United States' },
|
||||||
{ value: 'navigational', label: 'Navigational' },
|
{ value: 'CA', label: 'Canada' },
|
||||||
{ value: 'transactional', label: 'Transactional' },
|
{ value: 'GB', label: 'United Kingdom' },
|
||||||
{ value: 'commercial', label: 'Commercial' },
|
{ value: 'AE', label: 'United Arab Emirates' },
|
||||||
|
{ value: 'AU', label: 'Australia' },
|
||||||
|
{ value: 'IN', label: 'India' },
|
||||||
|
{ value: 'PK', label: 'Pakistan' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -612,7 +614,7 @@ export default function KeywordOpportunities() {
|
|||||||
filters={pageConfig.filters}
|
filters={pageConfig.filters}
|
||||||
filterValues={{
|
filterValues={{
|
||||||
search: searchTerm,
|
search: searchTerm,
|
||||||
intent: intentFilter,
|
country: countryFilter,
|
||||||
difficulty: difficultyFilter,
|
difficulty: difficultyFilter,
|
||||||
}}
|
}}
|
||||||
onFilterChange={(key, value) => {
|
onFilterChange={(key, value) => {
|
||||||
@@ -620,8 +622,8 @@ export default function KeywordOpportunities() {
|
|||||||
|
|
||||||
if (key === 'search') {
|
if (key === 'search') {
|
||||||
setSearchTerm(stringValue);
|
setSearchTerm(stringValue);
|
||||||
} else if (key === 'intent') {
|
} else if (key === 'country') {
|
||||||
setIntentFilter(stringValue);
|
setCountryFilter(stringValue);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
} else if (key === 'difficulty') {
|
} else if (key === 'difficulty') {
|
||||||
setDifficultyFilter(stringValue);
|
setDifficultyFilter(stringValue);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function Keywords() {
|
|||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('');
|
const [statusFilter, setStatusFilter] = useState('');
|
||||||
const [clusterFilter, setClusterFilter] = useState('');
|
const [clusterFilter, setClusterFilter] = useState('');
|
||||||
const [intentFilter, setIntentFilter] = useState('');
|
const [countryFilter, setCountryFilter] = useState('');
|
||||||
const [difficultyFilter, setDifficultyFilter] = useState('');
|
const [difficultyFilter, setDifficultyFilter] = useState('');
|
||||||
const [volumeMin, setVolumeMin] = useState<number | ''>('');
|
const [volumeMin, setVolumeMin] = useState<number | ''>('');
|
||||||
const [volumeMax, setVolumeMax] = useState<number | ''>('');
|
const [volumeMax, setVolumeMax] = useState<number | ''>('');
|
||||||
@@ -81,7 +81,7 @@ export default function Keywords() {
|
|||||||
keyword: '',
|
keyword: '',
|
||||||
volume: null,
|
volume: null,
|
||||||
difficulty: null,
|
difficulty: null,
|
||||||
intent: 'informational',
|
country: 'US',
|
||||||
cluster_id: null,
|
cluster_id: null,
|
||||||
status: 'new',
|
status: 'new',
|
||||||
});
|
});
|
||||||
@@ -156,7 +156,7 @@ export default function Keywords() {
|
|||||||
...(searchTerm && { search: searchTerm }),
|
...(searchTerm && { search: searchTerm }),
|
||||||
...(statusFilter && { status: statusFilter }),
|
...(statusFilter && { status: statusFilter }),
|
||||||
...(clusterFilter && { cluster_id: clusterFilter }),
|
...(clusterFilter && { cluster_id: clusterFilter }),
|
||||||
...(intentFilter && { intent: intentFilter }),
|
...(countryFilter && { country: countryFilter }),
|
||||||
...(activeSector?.id && { sector_id: activeSector.id }),
|
...(activeSector?.id && { sector_id: activeSector.id }),
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
page_size: pageSize || 10, // Ensure we always send a page_size
|
page_size: pageSize || 10, // Ensure we always send a page_size
|
||||||
@@ -200,7 +200,7 @@ export default function Keywords() {
|
|||||||
setShowContent(true);
|
setShowContent(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [currentPage, statusFilter, clusterFilter, intentFilter, difficultyFilter, volumeMin, volumeMax, sortBy, sortDirection, searchTerm, activeSite, activeSector, pageSize]);
|
}, [currentPage, statusFilter, clusterFilter, countryFilter, difficultyFilter, volumeMin, volumeMax, sortBy, sortDirection, searchTerm, activeSite, activeSector, pageSize]);
|
||||||
|
|
||||||
// Listen for site and sector changes and refresh data
|
// Listen for site and sector changes and refresh data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -324,8 +324,8 @@ export default function Keywords() {
|
|||||||
toast.error('Please select at least one keyword to cluster');
|
toast.error('Please select at least one keyword to cluster');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ids.length > 20) {
|
if (ids.length > 50) {
|
||||||
toast.error('Maximum 20 keywords allowed for clustering');
|
toast.error('Maximum 50 keywords allowed for clustering');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +536,7 @@ export default function Keywords() {
|
|||||||
keyword: '',
|
keyword: '',
|
||||||
volume: null,
|
volume: null,
|
||||||
difficulty: null,
|
difficulty: null,
|
||||||
intent: 'informational',
|
country: 'US',
|
||||||
cluster_id: null,
|
cluster_id: null,
|
||||||
status: 'new',
|
status: 'new',
|
||||||
});
|
});
|
||||||
@@ -605,8 +605,8 @@ export default function Keywords() {
|
|||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
setStatusFilter,
|
setStatusFilter,
|
||||||
intentFilter,
|
countryFilter,
|
||||||
setIntentFilter,
|
setCountryFilter,
|
||||||
difficultyFilter,
|
difficultyFilter,
|
||||||
setDifficultyFilter,
|
setDifficultyFilter,
|
||||||
clusterFilter,
|
clusterFilter,
|
||||||
@@ -632,7 +632,7 @@ export default function Keywords() {
|
|||||||
formData,
|
formData,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
intentFilter,
|
countryFilter,
|
||||||
difficultyFilter,
|
difficultyFilter,
|
||||||
clusterFilter,
|
clusterFilter,
|
||||||
volumeMin,
|
volumeMin,
|
||||||
@@ -776,7 +776,7 @@ export default function Keywords() {
|
|||||||
keyword: keyword.keyword,
|
keyword: keyword.keyword,
|
||||||
volume: keyword.volume,
|
volume: keyword.volume,
|
||||||
difficulty: keyword.difficulty,
|
difficulty: keyword.difficulty,
|
||||||
intent: keyword.intent,
|
country: keyword.country,
|
||||||
cluster_id: keyword.cluster_id,
|
cluster_id: keyword.cluster_id,
|
||||||
status: keyword.status,
|
status: keyword.status,
|
||||||
});
|
});
|
||||||
@@ -807,7 +807,7 @@ export default function Keywords() {
|
|||||||
filterValues={{
|
filterValues={{
|
||||||
search: searchTerm,
|
search: searchTerm,
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
intent: intentFilter,
|
country: countryFilter,
|
||||||
difficulty: difficultyFilter,
|
difficulty: difficultyFilter,
|
||||||
cluster_id: clusterFilter,
|
cluster_id: clusterFilter,
|
||||||
volumeMin: volumeMin,
|
volumeMin: volumeMin,
|
||||||
@@ -823,8 +823,8 @@ export default function Keywords() {
|
|||||||
} else if (key === 'status') {
|
} else if (key === 'status') {
|
||||||
setStatusFilter(stringValue);
|
setStatusFilter(stringValue);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
} else if (key === 'intent') {
|
} else if (key === 'country') {
|
||||||
setIntentFilter(stringValue);
|
setCountryFilter(stringValue);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
} else if (key === 'difficulty') {
|
} else if (key === 'difficulty') {
|
||||||
setDifficultyFilter(stringValue);
|
setDifficultyFilter(stringValue);
|
||||||
@@ -868,7 +868,7 @@ export default function Keywords() {
|
|||||||
search: searchTerm,
|
search: searchTerm,
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
cluster_id: clusterFilter,
|
cluster_id: clusterFilter,
|
||||||
intent: intentFilter,
|
country: countryFilter,
|
||||||
difficulty: difficultyFilter,
|
difficulty: difficultyFilter,
|
||||||
};
|
};
|
||||||
await handleExport('csv', filterValues);
|
await handleExport('csv', filterValues);
|
||||||
@@ -903,7 +903,7 @@ export default function Keywords() {
|
|||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
setStatusFilter('');
|
setStatusFilter('');
|
||||||
setClusterFilter('');
|
setClusterFilter('');
|
||||||
setIntentFilter('');
|
setCountryFilter('');
|
||||||
setDifficultyFilter('');
|
setDifficultyFilter('');
|
||||||
setVolumeMin('');
|
setVolumeMin('');
|
||||||
setVolumeMax('');
|
setVolumeMax('');
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function SeedKeywords() {
|
|||||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Sector</th>
|
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Sector</th>
|
||||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Volume</th>
|
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Volume</th>
|
||||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Difficulty</th>
|
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Difficulty</th>
|
||||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Intent</th>
|
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Country</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -109,7 +109,7 @@ export default function SeedKeywords() {
|
|||||||
{keyword.difficulty}
|
{keyword.difficulty}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">
|
||||||
<Badge variant="light" color="primary">{keyword.intent_display}</Badge>
|
<Badge variant="light" color="info">{keyword.country_display}</Badge>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [intentFilter, setIntentFilter] = useState('');
|
const [countryFilter, setCountryFilter] = useState('');
|
||||||
const [difficultyFilter, setDifficultyFilter] = useState('');
|
const [difficultyFilter, setDifficultyFilter] = useState('');
|
||||||
|
|
||||||
// Check if user is admin/superuser (role is 'admin' or 'developer')
|
// Check if user is admin/superuser (role is 'admin' or 'developer')
|
||||||
@@ -153,7 +153,7 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchTerm) baseFilters.search = searchTerm;
|
if (searchTerm) baseFilters.search = searchTerm;
|
||||||
if (intentFilter) baseFilters.intent = intentFilter;
|
if (countryFilter) baseFilters.country = countryFilter;
|
||||||
|
|
||||||
// Fetch ALL pages to get complete dataset
|
// Fetch ALL pages to get complete dataset
|
||||||
let allResults: SeedKeyword[] = [];
|
let allResults: SeedKeyword[] = [];
|
||||||
@@ -215,9 +215,9 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
} else if (sortBy === 'difficulty') {
|
} else if (sortBy === 'difficulty') {
|
||||||
aVal = a.difficulty;
|
aVal = a.difficulty;
|
||||||
bVal = b.difficulty;
|
bVal = b.difficulty;
|
||||||
} else if (sortBy === 'intent') {
|
} else if (sortBy === 'country') {
|
||||||
aVal = a.intent.toLowerCase();
|
aVal = a.country.toLowerCase();
|
||||||
bVal = b.intent.toLowerCase();
|
bVal = b.country.toLowerCase();
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,7 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
setTotalCount(0);
|
setTotalCount(0);
|
||||||
setTotalPages(1);
|
setTotalPages(1);
|
||||||
}
|
}
|
||||||
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, intentFilter, difficultyFilter, sortBy, sortDirection, toast]);
|
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, sortBy, sortDirection, toast]);
|
||||||
|
|
||||||
// Load data on mount and when filters change
|
// Load data on mount and when filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -521,28 +521,18 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'intent',
|
key: 'country',
|
||||||
label: 'Intent',
|
label: 'Country',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
sortField: 'intent',
|
sortField: 'country',
|
||||||
render: (value: string) => {
|
render: (value: string) => {
|
||||||
const getIntentColor = (intent: string) => {
|
|
||||||
const lowerIntent = intent?.toLowerCase() || '';
|
|
||||||
if (lowerIntent === 'transactional' || lowerIntent === 'commercial') {
|
|
||||||
return 'success';
|
|
||||||
} else if (lowerIntent === 'navigational') {
|
|
||||||
return 'warning';
|
|
||||||
}
|
|
||||||
return 'info';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
color={getIntentColor(value)}
|
color="info"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={value?.toLowerCase() === 'informational' ? 'light' : undefined}
|
variant="light"
|
||||||
>
|
>
|
||||||
{value}
|
{value || '-'}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -584,15 +574,18 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
placeholder: 'Search keywords...',
|
placeholder: 'Search keywords...',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'intent',
|
key: 'country',
|
||||||
label: 'Intent',
|
label: 'Country',
|
||||||
type: 'select' as const,
|
type: 'select' as const,
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Intent' },
|
{ value: '', label: 'All Countries' },
|
||||||
{ value: 'informational', label: 'Informational' },
|
{ value: 'US', label: 'United States' },
|
||||||
{ value: 'navigational', label: 'Navigational' },
|
{ value: 'CA', label: 'Canada' },
|
||||||
{ value: 'transactional', label: 'Transactional' },
|
{ value: 'GB', label: 'United Kingdom' },
|
||||||
{ value: 'commercial', label: 'Commercial' },
|
{ value: 'AE', label: 'United Arab Emirates' },
|
||||||
|
{ value: 'AU', label: 'Australia' },
|
||||||
|
{ value: 'IN', label: 'India' },
|
||||||
|
{ value: 'PK', label: 'Pakistan' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -691,7 +684,7 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
filters={pageConfig.filters}
|
filters={pageConfig.filters}
|
||||||
filterValues={{
|
filterValues={{
|
||||||
search: searchTerm,
|
search: searchTerm,
|
||||||
intent: intentFilter,
|
country: countryFilter,
|
||||||
difficulty: difficultyFilter,
|
difficulty: difficultyFilter,
|
||||||
}}
|
}}
|
||||||
onFilterChange={(key, value) => {
|
onFilterChange={(key, value) => {
|
||||||
@@ -699,8 +692,8 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
|
|
||||||
if (key === 'search') {
|
if (key === 'search') {
|
||||||
setSearchTerm(stringValue);
|
setSearchTerm(stringValue);
|
||||||
} else if (key === 'intent') {
|
} else if (key === 'country') {
|
||||||
setIntentFilter(stringValue);
|
setCountryFilter(stringValue);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
} else if (key === 'difficulty') {
|
} else if (key === 'difficulty') {
|
||||||
setDifficultyFilter(stringValue);
|
setDifficultyFilter(stringValue);
|
||||||
@@ -762,7 +755,7 @@ export default function IndustriesSectorsKeywords() {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor="import-file">Upload CSV File</Label>
|
<Label htmlFor="import-file">Upload CSV File</Label>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||||
Expected columns: keyword, volume, difficulty, intent, industry_name, sector_name
|
Expected columns: keyword, volume, difficulty, country, industry_name, sector_name
|
||||||
</p>
|
</p>
|
||||||
<FileInput
|
<FileInput
|
||||||
accept=".csv"
|
accept=".csv"
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ export interface KeywordFilters {
|
|||||||
search?: string;
|
search?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
cluster_id?: string;
|
cluster_id?: string;
|
||||||
intent?: string;
|
country?: string;
|
||||||
difficulty_min?: number;
|
difficulty_min?: number;
|
||||||
difficulty_max?: number;
|
difficulty_max?: number;
|
||||||
volume_min?: number;
|
volume_min?: number;
|
||||||
@@ -608,7 +608,7 @@ export interface Keyword {
|
|||||||
keyword: string; // Read-only property from seed_keyword
|
keyword: string; // Read-only property from seed_keyword
|
||||||
volume: number; // Read-only property from seed_keyword or volume_override
|
volume: number; // Read-only property from seed_keyword or volume_override
|
||||||
difficulty: number; // Read-only property from seed_keyword or difficulty_override
|
difficulty: number; // Read-only property from seed_keyword or difficulty_override
|
||||||
intent: string; // Read-only property from seed_keyword
|
country: string; // Read-only property from seed_keyword
|
||||||
volume_override?: number | null;
|
volume_override?: number | null;
|
||||||
difficulty_override?: number | null;
|
difficulty_override?: number | null;
|
||||||
cluster_id: number | null;
|
cluster_id: number | null;
|
||||||
@@ -623,7 +623,7 @@ export interface KeywordCreateData {
|
|||||||
keyword?: string; // For creating new custom keywords
|
keyword?: string; // For creating new custom keywords
|
||||||
volume?: number | null; // For custom keywords
|
volume?: number | null; // For custom keywords
|
||||||
difficulty?: number | null; // For custom keywords
|
difficulty?: number | null; // For custom keywords
|
||||||
intent?: string; // For custom keywords
|
country?: string; // For custom keywords
|
||||||
seed_keyword_id?: number; // For linking existing seed keywords (optional)
|
seed_keyword_id?: number; // For linking existing seed keywords (optional)
|
||||||
volume_override?: number | null;
|
volume_override?: number | null;
|
||||||
difficulty_override?: number | null;
|
difficulty_override?: number | null;
|
||||||
@@ -661,7 +661,7 @@ export async function fetchKeywords(filters: KeywordFilters = {}): Promise<Keywo
|
|||||||
if (filters.search) params.append('search', filters.search);
|
if (filters.search) params.append('search', filters.search);
|
||||||
if (filters.status) params.append('status', filters.status);
|
if (filters.status) params.append('status', filters.status);
|
||||||
if (filters.cluster_id) params.append('cluster_id', filters.cluster_id);
|
if (filters.cluster_id) params.append('cluster_id', filters.cluster_id);
|
||||||
if (filters.intent) params.append('seed_keyword__intent', filters.intent);
|
if (filters.country) params.append('seed_keyword__country', filters.country);
|
||||||
if (filters.difficulty_min !== undefined) params.append('difficulty_min', filters.difficulty_min.toString());
|
if (filters.difficulty_min !== undefined) params.append('difficulty_min', filters.difficulty_min.toString());
|
||||||
if (filters.difficulty_max !== undefined) params.append('difficulty_max', filters.difficulty_max.toString());
|
if (filters.difficulty_max !== undefined) params.append('difficulty_max', filters.difficulty_max.toString());
|
||||||
if (filters.volume_min !== undefined) params.append('volume_min', filters.volume_min.toString());
|
if (filters.volume_min !== undefined) params.append('volume_min', filters.volume_min.toString());
|
||||||
@@ -695,12 +695,12 @@ export async function createKeyword(data: KeywordCreateData): Promise<Keyword> {
|
|||||||
requestData.custom_keyword = data.keyword;
|
requestData.custom_keyword = data.keyword;
|
||||||
requestData.custom_volume = data.volume;
|
requestData.custom_volume = data.volume;
|
||||||
requestData.custom_difficulty = data.difficulty;
|
requestData.custom_difficulty = data.difficulty;
|
||||||
requestData.custom_intent = data.intent || 'informational';
|
requestData.custom_country = data.country || 'US';
|
||||||
// Remove the frontend-only fields
|
// Remove the frontend-only fields
|
||||||
delete requestData.keyword;
|
delete requestData.keyword;
|
||||||
delete requestData.volume;
|
delete requestData.volume;
|
||||||
delete requestData.difficulty;
|
delete requestData.difficulty;
|
||||||
delete requestData.intent;
|
delete requestData.country;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchAPI('/v1/planner/keywords/', {
|
return fetchAPI('/v1/planner/keywords/', {
|
||||||
@@ -2073,8 +2073,8 @@ export interface SeedKeyword {
|
|||||||
sector_slug: string;
|
sector_slug: string;
|
||||||
volume: number;
|
volume: number;
|
||||||
difficulty: number;
|
difficulty: number;
|
||||||
intent: string;
|
country: string;
|
||||||
intent_display: string;
|
country_display: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
@@ -2090,7 +2090,7 @@ export interface SeedKeywordResponse {
|
|||||||
export async function fetchSeedKeywords(filters?: {
|
export async function fetchSeedKeywords(filters?: {
|
||||||
industry?: number;
|
industry?: number;
|
||||||
sector?: number;
|
sector?: number;
|
||||||
intent?: string;
|
country?: string;
|
||||||
search?: string;
|
search?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
page_size?: number;
|
page_size?: number;
|
||||||
@@ -2105,7 +2105,7 @@ export async function fetchSeedKeywords(filters?: {
|
|||||||
params.append('sector', filters.sector.toString());
|
params.append('sector', filters.sector.toString());
|
||||||
params.append('sector_id', filters.sector.toString()); // Also send sector_id for get_queryset
|
params.append('sector_id', filters.sector.toString()); // Also send sector_id for get_queryset
|
||||||
}
|
}
|
||||||
if (filters?.intent) params.append('intent', filters.intent);
|
if (filters?.country) params.append('country', filters.country);
|
||||||
if (filters?.search) params.append('search', filters.search);
|
if (filters?.search) params.append('search', filters.search);
|
||||||
if (filters?.page) params.append('page', filters.page.toString());
|
if (filters?.page) params.append('page', filters.page.toString());
|
||||||
if (filters?.page_size) params.append('page_size', filters.page_size.toString());
|
if (filters?.page_size) params.append('page_size', filters.page_size.toString());
|
||||||
|
|||||||
Reference in New Issue
Block a user