add: image and blog import
fix: category field to Entry model feat: EntriesTable with searchable and sortable columns chore: update CHANGELOG with recent additions and API updates
This commit is contained in:
parent
9f77f8b8d3
commit
ccf38ef6f9
5 changed files with 235 additions and 2 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -2,6 +2,18 @@
|
||||||
|
|
||||||
## 2026-01-08
|
## 2026-01-08
|
||||||
|
|
||||||
|
added text widgets
|
||||||
|
|
||||||
|
added categories
|
||||||
|
|
||||||
|
updated api to access text widgets and categories
|
||||||
|
|
||||||
|
added url and call to action for entries for use in cards
|
||||||
|
|
||||||
|
added import image and blogs import commands
|
||||||
|
|
||||||
|
## 2026-01-08
|
||||||
|
|
||||||
added tags to entry model
|
added tags to entry model
|
||||||
|
|
||||||
added text widget and category
|
added text widget and category
|
||||||
|
|
|
||||||
144
app/Console/Commands/ImportBlogs.php
Normal file
144
app/Console/Commands/ImportBlogs.php
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use App\Models\Entry;
|
||||||
|
|
||||||
|
class ImportBlogs extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'app:import-blogs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Command description';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"title": "Low/No Code FlowiseAI",
|
||||||
|
"slug": "lowno-code-flowiseai",
|
||||||
|
"content": "<p><img alt=\"\" src=\"http://127.0.0.1:8000/storage/99/ZsTHWAQRbdvgRUEwGmCIsm4TyChIjBwiY71VmnnR.webp\"/></p>\n<p>In <a href=\"https://flowiseai.com/\">FlowiseAI</a>, applications based on the JavaScript fork of LangChain are modeled in a 'no/low-code' environment. If you are coming from a closed source world, yet trying to implement devops principles this may fill you with fear, dread and uncertainty. FlowiseAI offers the advantage of using plain-text JSON files to represent each workflow. These files are easy to understand, open, and readily backup-able, unlike opaque proprietary binary formats\nThe data used at runtime and other component prerequisites like credentials are stored in the FlowiseAI data volume, which looks like this</p>\n<p><code>bash\nMode LastWriteTime ..... 8ZS1C0ZBB.webp",
|
||||||
|
"created_at": "2024-12-17 11:24:59",
|
||||||
|
"updated_at": "2024-12-17 12:53:54",
|
||||||
|
"category_id": 1,
|
||||||
|
"blog_date": "2024-02-26 00:00",
|
||||||
|
"is_featured": 0,
|
||||||
|
"published": 1
|
||||||
|
},
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$filePath = '/home/user/projects/laravel/12/media_library/boring-astro-static/imported_database/blogs.json';
|
||||||
|
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
$this->error("File not found: $filePath");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jsonContent = file_get_contents($filePath);
|
||||||
|
$blogs = json_decode($jsonContent, true);
|
||||||
|
|
||||||
|
if (!$blogs) {
|
||||||
|
$this->error("Could not parse JSON file: $filePath");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($blogs as $blog) {
|
||||||
|
// Only process the blog with ID 51
|
||||||
|
if (($blog['id'] ?? null) !== 51) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = $blog['slug'] ?? null;
|
||||||
|
$this->info("Processing blog ID: {$blog['id']} with slug: {$slug}");
|
||||||
|
// Check if the entry already exists
|
||||||
|
$existingEntry = Entry::where('slug', $slug)->first();
|
||||||
|
|
||||||
|
if ($existingEntry) {
|
||||||
|
// Update existing entry with cleaned content
|
||||||
|
$existingEntry->update([
|
||||||
|
'content' => $this->cleanHtmlForFilament($blog['content'] ?? ''),
|
||||||
|
'description' => $this->extractPlainTextFromHtml($blog['content'] ?? ''),
|
||||||
|
]);
|
||||||
|
$this->info("Updated content for: {$existingEntry->title}");
|
||||||
|
} else {
|
||||||
|
// Create new entry
|
||||||
|
Entry::create([
|
||||||
|
'title' => $blog['title'] ?? null,
|
||||||
|
'slug' => $slug,
|
||||||
|
'description' => $this->extractPlainTextFromHtml($blog['content'] ?? ''),
|
||||||
|
'is_published' => $blog['published'] ?? false,
|
||||||
|
'is_featured' => $blog['is_featured'] ?? false,
|
||||||
|
'published_at' => $blog['blog_date'] ?? null,
|
||||||
|
'content' => $this->cleanHtmlForFilament($blog['content'] ?? ''),
|
||||||
|
'category_id' => 1, // Default category
|
||||||
|
]);
|
||||||
|
$this->info("Created new entry: " . ($blog['title'] ?? 'Untitled'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Blogs imported successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract plain text from HTML for description field
|
||||||
|
*/
|
||||||
|
private function extractPlainTextFromHtml(string $html): string
|
||||||
|
{
|
||||||
|
if (empty($html)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode HTML entities and strip tags
|
||||||
|
$text = html_entity_decode(strip_tags($html));
|
||||||
|
|
||||||
|
// Clean up whitespace
|
||||||
|
$text = preg_replace('/\s+/', ' ', $text);
|
||||||
|
|
||||||
|
// Limit to reasonable length for description
|
||||||
|
return trim(substr($text, 0, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean HTML content for Filament rich editor
|
||||||
|
*/
|
||||||
|
private function cleanHtmlForFilament(string $html): string
|
||||||
|
{
|
||||||
|
if (empty($html)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert escaped newlines to actual newlines
|
||||||
|
$html = str_replace(['\\n', '\\r\\n', '\\r'], "\n", $html);
|
||||||
|
|
||||||
|
// Decode HTML entities
|
||||||
|
$html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
|
||||||
|
// Clean up excessive whitespace but preserve paragraph structure
|
||||||
|
$html = preg_replace('/\s*\n\s*/', ' ', $html);
|
||||||
|
$html = preg_replace('/[ \t]+/', ' ', $html);
|
||||||
|
|
||||||
|
// Ensure paragraphs have proper spacing
|
||||||
|
$html = str_replace('</p><p>', '</p>' . "\n" . '<p>', $html);
|
||||||
|
|
||||||
|
return trim($html);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/Console/Commands/ImportImages.php
Normal file
61
app/Console/Commands/ImportImages.php
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class ImportImages extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'app:import-images {--entry-id=1}';
|
||||||
|
protected $description = 'Import images into the media library';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$entryId = $this->option('entry-id');
|
||||||
|
$entry = Entry::find($entryId);
|
||||||
|
|
||||||
|
if (!$entry) {
|
||||||
|
$this->error("Entry with ID {$entryId} not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Storage::disk('public')->files('imported_images');
|
||||||
|
|
||||||
|
if (empty($files)) {
|
||||||
|
$this->info('No files found in storage/app/public/imported_images/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Found " . count($files) . " files to import");
|
||||||
|
|
||||||
|
foreach ($files as $filePath) {
|
||||||
|
try {
|
||||||
|
$fullPath = storage_path('app/public/' . $filePath);
|
||||||
|
$fileName = pathinfo($fullPath, PATHINFO_BASENAME);
|
||||||
|
|
||||||
|
if (!file_exists($fullPath)) {
|
||||||
|
$this->error("File not found: {$fullPath}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already exists
|
||||||
|
$existingMedia = $entry->getMedia()->where('file_name', $fileName)->first();
|
||||||
|
if ($existingMedia) {
|
||||||
|
$this->info("Skipping existing: {$fileName}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$media = $entry->addMedia($fullPath)
|
||||||
|
->toMediaCollection('default');
|
||||||
|
|
||||||
|
$this->info("Imported: {$fileName}");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("Failed to import {$filePath}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Import completed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,18 +17,33 @@ class EntriesTable
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
|
TextColumn::make('id')
|
||||||
|
->label('ID')
|
||||||
|
->sortable(query: function ($query, $direction) {
|
||||||
|
$query->orderBy('id', $direction);
|
||||||
|
}),
|
||||||
SpatieMediaLibraryImageColumn::make('featured_image')
|
SpatieMediaLibraryImageColumn::make('featured_image')
|
||||||
->collection('featured-image')
|
->collection('featured-image')
|
||||||
|
->label('Image')
|
||||||
->circular()
|
->circular()
|
||||||
->stacked()
|
->stacked()
|
||||||
->limit(3),
|
->limit(3),
|
||||||
TextColumn::make('title')
|
TextColumn::make('title')
|
||||||
->searchable(),
|
->searchable()
|
||||||
TextColumn::make('slug')
|
->sortable(),
|
||||||
|
TextColumn::make('category.name')
|
||||||
|
->label('Category')
|
||||||
|
->sortable(query: function ($query, $direction) {
|
||||||
|
$query->join('categories', 'entries.category_id', '=', 'categories.id')
|
||||||
|
->orderBy('categories.name', $direction)
|
||||||
|
->select('entries.*');
|
||||||
|
})
|
||||||
->searchable(),
|
->searchable(),
|
||||||
IconColumn::make('is_published')
|
IconColumn::make('is_published')
|
||||||
|
->label('pub')
|
||||||
->boolean(),
|
->boolean(),
|
||||||
IconColumn::make('is_featured')
|
IconColumn::make('is_featured')
|
||||||
|
->label('feat')
|
||||||
->boolean(),
|
->boolean(),
|
||||||
TextColumn::make('published_at')
|
TextColumn::make('published_at')
|
||||||
->date()
|
->date()
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ class Entry extends Model implements HasMedia, HasRichContent
|
||||||
'content',
|
'content',
|
||||||
'call_to_action_link',
|
'call_to_action_link',
|
||||||
'call_to_action_text',
|
'call_to_action_text',
|
||||||
|
'category_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue