Feeds CRUD

This commit is contained in:
myrmidex 2025-07-05 02:37:38 +02:00
parent d4e9e27c41
commit c7302092bb
9 changed files with 609 additions and 0 deletions

View file

@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers;
use App\Models\Feed;
use Illuminate\Http\Request;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
class FeedsController extends Controller
{
public function index(): View
{
$feeds = Feed::orderBy('is_active', 'desc')
->orderBy('name')
->get();
return view('pages.feeds.index', compact('feeds'));
}
public function create(): View
{
return view('pages.feeds.create');
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'url' => 'required|url|unique:feeds,url',
'type' => 'required|in:website,rss',
'language' => 'required|string|size:2',
'description' => 'nullable|string',
'is_active' => 'boolean'
]);
// Default is_active to true if not provided
$validated['is_active'] = $validated['is_active'] ?? true;
Feed::create($validated);
return redirect()->route('feeds.index')
->with('success', 'Feed created successfully!');
}
public function show(Feed $feed): View
{
return view('pages.feeds.show', compact('feed'));
}
public function edit(Feed $feed): View
{
return view('pages.feeds.edit', compact('feed'));
}
public function update(Request $request, Feed $feed): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'url' => 'required|url|unique:feeds,url,' . $feed->id,
'type' => 'required|in:website,rss',
'language' => 'required|string|size:2',
'description' => 'nullable|string',
'is_active' => 'boolean'
]);
// Default is_active to current value if not provided
$validated['is_active'] = $validated['is_active'] ?? $feed->is_active;
$feed->update($validated);
return redirect()->route('feeds.index')
->with('success', 'Feed updated successfully!');
}
public function destroy(Feed $feed): RedirectResponse
{
$feed->delete();
return redirect()->route('feeds.index')
->with('success', 'Feed deleted successfully!');
}
}

71
app/Models/Feed.php Normal file
View file

@ -0,0 +1,71 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
/**
* @property int $id
* @property string $name
* @property string $url
* @property string $type
* @property string $language
* @property string $description
* @property array $settings
* @property bool $is_active
* @property Carbon $last_fetched_at
* @property Carbon $created_at
* @property Carbon $updated_at
* @method static create(array $validated)
* @method static orderBy(string $string, string $string1)
*/
class Feed extends Model
{
protected $fillable = [
'name',
'url',
'type',
'language',
'description',
'settings',
'is_active',
'last_fetched_at'
];
protected $casts = [
'settings' => 'array',
'is_active' => 'boolean',
'last_fetched_at' => 'datetime'
];
public function getTypeDisplayAttribute(): string
{
return match ($this->type) {
'website' => 'Website',
'rss' => 'RSS Feed',
default => 'Unknown'
};
}
public function getStatusAttribute(): string
{
if (!$this->is_active) {
return 'Inactive';
}
if (!$this->last_fetched_at) {
return 'Never fetched';
}
$hoursAgo = $this->last_fetched_at->diffInHours(now());
if ($hoursAgo < 2) {
return 'Recently fetched';
} elseif ($hoursAgo < 24) {
return "Fetched {$hoursAgo}h ago";
} else {
return "Fetched " . $this->last_fetched_at->diffForHumans();
}
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('feeds', function (Blueprint $table) {
$table->id();
$table->string('name'); // "VRT News", "Belga News Agency"
$table->string('url'); // "https://vrt.be" or "https://feeds.example.com/rss.xml"
$table->enum('type', ['website', 'rss']); // Feed type
$table->string('language', 5)->default('en'); // Language code (en, nl, etc.)
$table->text('description')->nullable();
$table->json('settings')->nullable(); // Custom settings per feed type
$table->boolean('is_active')->default(true);
$table->timestamp('last_fetched_at')->nullable();
$table->timestamps();
$table->unique('url');
});
}
public function down(): void
{
Schema::dropIfExists('feeds');
}
};

View file

@ -0,0 +1,109 @@
@extends('layouts.app')
@section('content')
<div class="max-w-2xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Add New Feed</h1>
<p class="mt-1 text-sm text-gray-600">Create a new content feed for articles.</p>
</div>
<div class="bg-white shadow sm:rounded-lg">
<form action="{{ route('feeds.store') }}" method="POST" class="px-4 py-5 sm:p-6">
@csrf
<div class="grid grid-cols-1 gap-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<input type="text"
name="name"
id="name"
value="{{ old('name') }}"
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('name') border-red-300 @enderror"
placeholder="VRT News">
@error('name')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="url" class="block text-sm font-medium text-gray-700">URL</label>
<input type="url"
name="url"
id="url"
value="{{ old('url') }}"
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('url') border-red-300 @enderror"
placeholder="https://example.com or https://example.com/feed.xml">
@error('url')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700">Type</label>
<select name="type"
id="type"
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('type') border-red-300 @enderror">
<option value="">Select feed type...</option>
<option value="website" {{ old('type') === 'website' ? 'selected' : '' }}>Website</option>
<option value="rss" {{ old('type') === 'rss' ? 'selected' : '' }}>RSS Feed</option>
</select>
@error('type')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="language" class="block text-sm font-medium text-gray-700">Language</label>
<select name="language"
id="language"
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('language') border-red-300 @enderror">
<option value="en" {{ old('language', 'en') === 'en' ? 'selected' : '' }}>English</option>
<option value="nl" {{ old('language') === 'nl' ? 'selected' : '' }}>Dutch</option>
<option value="fr" {{ old('language') === 'fr' ? 'selected' : '' }}>French</option>
<option value="de" {{ old('language') === 'de' ? 'selected' : '' }}>German</option>
<option value="es" {{ old('language') === 'es' ? 'selected' : '' }}>Spanish</option>
</select>
@error('language')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<textarea name="description"
id="description"
rows="3"
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('description') border-red-300 @enderror"
placeholder="Optional description of this feed...">{{ old('description') }}</textarea>
@error('description')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="flex items-center">
<input type="checkbox"
name="is_active"
id="is_active"
value="1"
{{ old('is_active', true) ? 'checked' : '' }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="is_active" class="ml-2 block text-sm text-gray-900">
Active
</label>
</div>
</div>
<div class="mt-6 flex items-center justify-end space-x-3">
<a href="{{ route('feeds.index') }}" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</a>
<button type="submit" class="bg-indigo-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Create Feed
</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,110 @@
@extends('layouts.app')
@section('content')
<div class="max-w-2xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Edit Feed</h1>
<p class="mt-1 text-sm text-gray-600">Update the details for {{ $feed->name }}.</p>
</div>
<div class="bg-white shadow sm:rounded-lg">
<form action="{{ route('feeds.update', $feed) }}" method="POST" class="px-4 py-5 sm:p-6">
@csrf
@method('PUT')
<div class="grid grid-cols-1 gap-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<input type="text"
name="name"
id="name"
value="{{ old('name', $feed->name) }}"
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('name') border-red-300 @enderror"
placeholder="VRT News">
@error('name')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="url" class="block text-sm font-medium text-gray-700">URL</label>
<input type="url"
name="url"
id="url"
value="{{ old('url', $feed->url) }}"
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('url') border-red-300 @enderror"
placeholder="https://example.com or https://example.com/feed.xml">
@error('url')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700">Type</label>
<select name="type"
id="type"
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('type') border-red-300 @enderror">
<option value="">Select feed type...</option>
<option value="website" {{ old('type', $feed->type) === 'website' ? 'selected' : '' }}>Website</option>
<option value="rss" {{ old('type', $feed->type) === 'rss' ? 'selected' : '' }}>RSS Feed</option>
</select>
@error('type')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="language" class="block text-sm font-medium text-gray-700">Language</label>
<select name="language"
id="language"
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('language') border-red-300 @enderror">
<option value="en" {{ old('language', $feed->language) === 'en' ? 'selected' : '' }}>English</option>
<option value="nl" {{ old('language', $feed->language) === 'nl' ? 'selected' : '' }}>Dutch</option>
<option value="fr" {{ old('language', $feed->language) === 'fr' ? 'selected' : '' }}>French</option>
<option value="de" {{ old('language', $feed->language) === 'de' ? 'selected' : '' }}>German</option>
<option value="es" {{ old('language', $feed->language) === 'es' ? 'selected' : '' }}>Spanish</option>
</select>
@error('language')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<textarea name="description"
id="description"
rows="3"
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('description') border-red-300 @enderror"
placeholder="Optional description of this feed...">{{ old('description', $feed->description) }}</textarea>
@error('description')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="flex items-center">
<input type="checkbox"
name="is_active"
id="is_active"
value="1"
{{ old('is_active', $feed->is_active) ? 'checked' : '' }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="is_active" class="ml-2 block text-sm text-gray-900">
Active
</label>
</div>
</div>
<div class="mt-6 flex items-center justify-end space-x-3">
<a href="{{ route('feeds.index') }}" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</a>
<button type="submit" class="bg-indigo-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Update Feed
</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,87 @@
@extends('layouts.app')
@section('content')
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Feeds</h1>
<a href="{{ route('feeds.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Add New Feed
</a>
</div>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{{ session('success') }}
</div>
@endif
<div class="bg-white shadow overflow-hidden sm:rounded-md">
@if($feeds->count() > 0)
<ul class="divide-y divide-gray-200">
@foreach($feeds as $feed)
<li>
<div class="px-4 py-4 flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0">
@if($feed->type === 'rss')
<i class="fas fa-rss text-orange-500 text-xl"></i>
@else
<i class="fas fa-globe text-blue-500 text-xl"></i>
@endif
</div>
<div class="ml-4">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">{{ $feed->name }}</div>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ $feed->is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
{{ $feed->is_active ? 'Active' : 'Inactive' }}
</span>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{{ $feed->type_display }}
</span>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ strtoupper($feed->language) }}
</span>
</div>
<div class="text-sm text-gray-500">{{ $feed->url }}</div>
@if($feed->description)
<div class="text-sm text-gray-500 mt-1">{{ Str::limit($feed->description, 100) }}</div>
@endif
<div class="text-xs text-gray-400 mt-1">{{ $feed->status }}</div>
</div>
</div>
<div class="flex items-center space-x-2">
<a href="{{ route('feeds.show', $feed) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
View
</a>
<a href="{{ route('feeds.edit', $feed) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
Edit
</a>
<form action="{{ route('feeds.destroy', $feed) }}" method="POST" class="inline"
onsubmit="return confirm('Are you sure you want to delete this feed?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900 text-sm font-medium">
Delete
</button>
</form>
</div>
</div>
</li>
@endforeach
</ul>
@else
<div class="text-center py-12">
<i class="fas fa-rss text-gray-400 text-6xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 mb-2">No feeds yet</h3>
<p class="text-gray-500 mb-4">Get started by adding your first content feed.</p>
<a href="{{ route('feeds.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Add New Feed
</a>
</div>
@endif
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,113 @@
@extends('layouts.app')
@section('content')
<div class="max-w-4xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900">{{ $feed->name }}</h1>
<p class="mt-1 text-sm text-gray-600">{{ $feed->type_display }} {{ strtoupper($feed->language) }}</p>
</div>
<div class="flex items-center space-x-3">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium
{{ $feed->is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
{{ $feed->is_active ? 'Active' : 'Inactive' }}
</span>
<a href="{{ route('feeds.edit', $feed) }}" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">
Edit Feed
</a>
</div>
</div>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">Feed Details</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Information about this content feed.</p>
</div>
<div class="border-t border-gray-200 px-4 py-5 sm:px-6">
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500">Name</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $feed->name }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Type</dt>
<dd class="mt-1 text-sm text-gray-900">
<div class="flex items-center">
@if($feed->type === 'rss')
<i class="fas fa-rss text-orange-500 mr-2"></i>
@else
<i class="fas fa-globe text-blue-500 mr-2"></i>
@endif
{{ $feed->type_display }}
</div>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">URL</dt>
<dd class="mt-1 text-sm text-gray-900">
<a href="{{ $feed->url }}" target="_blank" class="text-indigo-600 hover:text-indigo-500">
{{ $feed->url }}
<i class="fas fa-external-link-alt ml-1 text-xs"></i>
</a>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Language</dt>
<dd class="mt-1 text-sm text-gray-900">{{ strtoupper($feed->language) }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Status</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $feed->status }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Created</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $feed->created_at->format('M j, Y g:i A') }}</dd>
</div>
@if($feed->last_fetched_at)
<div>
<dt class="text-sm font-medium text-gray-500">Last Fetched</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $feed->last_fetched_at->format('M j, Y g:i A') }}</dd>
</div>
@endif
@if($feed->description)
<div class="sm:col-span-2">
<dt class="text-sm font-medium text-gray-500">Description</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $feed->description }}</dd>
</div>
@endif
</dl>
</div>
</div>
<div class="mt-6 flex items-center justify-between">
<a href="{{ route('feeds.index') }}" class="text-indigo-600 hover:text-indigo-500 font-medium">
Back to Feeds
</a>
<div class="flex items-center space-x-3">
<a href="{{ route('feeds.edit', $feed) }}" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">
Edit
</a>
<form action="{{ route('feeds.destroy', $feed) }}" method="POST" class="inline"
onsubmit="return confirm('Are you sure you want to delete this feed?')">
@csrf
@method('DELETE')
<button type="submit" class="bg-red-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-red-700">
Delete Feed
</button>
</form>
</div>
</div>
</div>
</div>
@endsection

View file

@ -17,6 +17,10 @@
<i class="fas fa-hashtag mr-3"></i> <i class="fas fa-hashtag mr-3"></i>
Channels Channels
</a> </a>
<a href="/feeds" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors {{ request()->is('feeds*') ? 'bg-gray-700 text-white' : '' }}">
<i class="fas fa-rss mr-3"></i>
Feeds
</a>
<a href="/logs" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors {{ request()->is('logs') ? 'bg-gray-700 text-white' : '' }}"> <a href="/logs" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors {{ request()->is('logs') ? 'bg-gray-700 text-white' : '' }}">
<i class="fas fa-list mr-3"></i> <i class="fas fa-list mr-3"></i>
Logs Logs

View file

@ -25,3 +25,4 @@
Route::post('/platforms/{platformAccount}/set-active', [App\Http\Controllers\PlatformAccountsController::class, 'setActive'])->name('platforms.set-active'); Route::post('/platforms/{platformAccount}/set-active', [App\Http\Controllers\PlatformAccountsController::class, 'setActive'])->name('platforms.set-active');
Route::resource('channels', App\Http\Controllers\PlatformChannelsController::class)->names('channels'); Route::resource('channels', App\Http\Controllers\PlatformChannelsController::class)->names('channels');
Route::resource('feeds', App\Http\Controllers\FeedsController::class)->names('feeds');