Add keywords

This commit is contained in:
myrmidex 2025-07-05 23:54:43 +02:00
parent bea1e67b19
commit 626085ed67
6 changed files with 198 additions and 3 deletions

View file

@ -81,6 +81,7 @@ public function getStatusAttribute(): string
public function channels(): BelongsToMany public function channels(): BelongsToMany
{ {
return $this->belongsToMany(PlatformChannel::class, 'feed_platform_channels') return $this->belongsToMany(PlatformChannel::class, 'feed_platform_channels')
->using(FeedPlatformChannel::class)
->withPivot(['is_active', 'priority', 'filters']) ->withPivot(['is_active', 'priority', 'filters'])
->withTimestamps(); ->withTimestamps();
} }

View file

@ -0,0 +1,53 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Carbon;
/**
* @property int $feed_id
* @property int $platform_channel_id
* @property bool $is_active
* @property int $priority
* @property array $filters
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class FeedPlatformChannel extends Pivot
{
protected $table = 'feed_platform_channels';
public $incrementing = false;
protected $fillable = [
'feed_id',
'platform_channel_id',
'is_active',
'priority',
'filters'
];
protected $casts = [
'is_active' => 'boolean',
'filters' => 'array'
];
public function feed(): BelongsTo
{
return $this->belongsTo(Feed::class);
}
public function platformChannel(): BelongsTo
{
return $this->belongsTo(PlatformChannel::class);
}
public function keywords(): HasMany
{
return $this->hasMany(Keyword::class, 'feed_id', 'feed_id')
->where('platform_channel_id', $this->platform_channel_id);
}
}

43
app/Models/Keyword.php Normal file
View file

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/**
* @property int $id
* @property int $feed_id
* @property Feed $feed
* @property int $platform_channel_id
* @property PlatformChannel $platformChannel
* @property string $keyword
* @property bool $is_active
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class Keyword extends Model
{
protected $fillable = [
'feed_id',
'platform_channel_id',
'keyword',
'is_active'
];
protected $casts = [
'is_active' => 'boolean'
];
public function feed(): BelongsTo
{
return $this->belongsTo(Feed::class);
}
public function platformChannel(): BelongsTo
{
return $this->belongsTo(PlatformChannel::class);
}
}

View file

@ -58,6 +58,7 @@ public function getFullNameAttribute(): string
public function feeds(): BelongsToMany public function feeds(): BelongsToMany
{ {
return $this->belongsToMany(Feed::class, 'feed_platform_channels') return $this->belongsToMany(Feed::class, 'feed_platform_channels')
->using(FeedPlatformChannel::class)
->withPivot(['is_active', 'priority', 'filters']) ->withPivot(['is_active', 'priority', 'filters'])
->withTimestamps(); ->withTimestamps();
} }

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('keywords', function (Blueprint $table) {
$table->id();
$table->foreignId('feed_id')->constrained()->onDelete('cascade');
$table->foreignId('platform_channel_id')->constrained()->onDelete('cascade');
$table->string('keyword');
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->unique(['feed_id', 'platform_channel_id', 'keyword']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('keywords');
}
};

View file

@ -75,13 +75,43 @@ class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-
</div> </div>
<div> <div>
<label for="filters" class="block text-sm font-medium text-gray-700">Filters (Optional)</label> <label class="block text-sm font-medium text-gray-700 mb-3">Keywords (Optional)</label>
<div id="keywords-container" class="space-y-2">
<div class="keyword-input-group flex items-center space-x-2">
<input type="text"
name="keywords[]"
class="flex-1 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Enter keyword">
<button type="button"
class="remove-keyword bg-red-500 hover:bg-red-600 text-white px-3 py-2 rounded-md text-sm"
onclick="removeKeyword(this)">
Remove
</button>
</div>
</div>
<button type="button"
id="add-keyword"
class="mt-2 bg-green-500 hover:bg-green-600 text-white px-3 py-2 rounded-md text-sm"
onclick="addKeyword()">
Add Keyword
</button>
<p class="mt-1 text-sm text-gray-500">Articles will be filtered to only include content matching these keywords</p>
@error('keywords')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
@error('keywords.*')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="filters" class="block text-sm font-medium text-gray-700">Other Filters (Optional)</label>
<textarea name="filters" <textarea name="filters"
id="filters" id="filters"
rows="4" rows="4"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('filters') border-red-300 @enderror" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('filters') border-red-300 @enderror"
placeholder='{"keywords": ["technology", "AI"], "exclude_keywords": ["sports"]}'>{{ old('filters') }}</textarea> placeholder='{"exclude_keywords": ["sports"], "min_length": 100}'>{{ old('filters') }}</textarea>
<p class="mt-1 text-sm text-gray-500">JSON format for content filtering rules</p> <p class="mt-1 text-sm text-gray-500">JSON format for additional content filtering rules</p>
@error('filters') @error('filters')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p> <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror @enderror
@ -124,4 +154,38 @@ class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-
@endif @endif
</div> </div>
</div> </div>
<script>
function addKeyword() {
const container = document.getElementById('keywords-container');
const newGroup = document.createElement('div');
newGroup.className = 'keyword-input-group flex items-center space-x-2';
newGroup.innerHTML = `
<input type="text"
name="keywords[]"
class="flex-1 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Enter keyword">
<button type="button"
class="remove-keyword bg-red-500 hover:bg-red-600 text-white px-3 py-2 rounded-md text-sm"
onclick="removeKeyword(this)">
Remove
</button>
`;
container.appendChild(newGroup);
}
function removeKeyword(button) {
const container = document.getElementById('keywords-container');
const groups = container.querySelectorAll('.keyword-input-group');
// Don't remove if it's the last remaining input
if (groups.length > 1) {
button.parentElement.remove();
} else {
// Clear the input value instead of removing the field
const input = button.parentElement.querySelector('input');
input.value = '';
}
}
</script>
@endsection @endsection