first commit

This commit is contained in:
2025-09-13 17:39:17 +03:00
commit e650f3c1b2
73 changed files with 14164 additions and 0 deletions
@@ -0,0 +1,176 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Book;
use App\Models\Category;
use App\Models\Summary;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
class IngestController extends Controller
{
/**
* Upsert books with summaries and categories.
* Accepts either a single book object or an array of books under `books`.
*/
public function upsertBooks(Request $request)
{
$payload = $request->all();
// Normalize to array of items
$items = [];
if (isset($payload['books']) && is_array($payload['books'])) {
$items = $payload['books'];
} elseif ($this->isAssocArray($payload)) {
$items = [$payload];
} elseif (is_array($payload)) {
$items = $payload;
}
if (empty($items)) {
return response()->json([
'message' => 'No books provided.',
], 422);
}
$results = [];
foreach ($items as $index => $item) {
// Basic validation (minimal to satisfy requirement)
$title = $item['title'] ?? null;
$author = $item['author'] ?? null;
if (!$title || !$author) {
$results[] = [
'index' => $index,
'status' => 'error',
'message' => 'title and author are required.',
];
continue;
}
$result = DB::transaction(function () use ($item) {
// Handle category upsert
$categoryData = $item['category'] ?? null;
$category = null;
if ($categoryData) {
$catSlug = $categoryData['slug'] ?? null;
$catName = $categoryData['name'] ?? null;
if (!$catSlug && $catName) {
$catSlug = Str::slug($catName);
}
if ($catSlug) {
$category = Category::where('slug', $catSlug)->first();
}
if (!$category && $catName) {
// Try by name
$category = Category::where('name', $catName)->first();
}
if ($category) {
// Update existing
$category->fill([
'name' => $catName ?? $category->name,
'slug' => $catSlug ?? ($catName ? Str::slug($catName) : $category->slug),
'description' => $categoryData['description'] ?? $category->description,
])->save();
$categoryStatus = 'updated';
} else {
// Create new if enough data
$category = Category::create([
'name' => $catName ?? 'Uncategorized',
'slug' => $catSlug ?? ($catName ? Str::slug($catName) : Str::slug('Uncategorized')),
'description' => $categoryData['description'] ?? null,
]);
$categoryStatus = 'created';
}
}
// Handle book upsert
$bookSlug = $item['slug'] ?? null;
if (!$bookSlug && !empty($item['title'])) {
$bookSlug = Str::slug($item['title']);
}
$bookQuery = Book::query();
if ($bookSlug) {
$bookQuery->where('slug', $bookSlug);
} else {
// Fallback: match by title+author
$bookQuery->where('title', $item['title'])->where('author', $item['author']);
}
$book = $bookQuery->first();
$bookData = [
'title' => $item['title'] ?? ($book->title ?? null),
'slug' => $bookSlug ?? ($book->slug ?? null),
'author' => $item['author'] ?? ($book->author ?? null),
'description' => $item['description'] ?? ($book->description ?? null),
'publication_date' => $item['publication_date'] ?? ($book->publication_date ?? null),
'cover_image' => $item['cover_image'] ?? ($book->cover_image ?? null),
];
if ($category) {
$bookData['category_id'] = $category->id;
} elseif (!empty($item['category_id'])) {
$bookData['category_id'] = $item['category_id'];
}
if ($book) {
$book->fill($bookData)->save();
$bookStatus = 'updated';
} else {
$book = Book::create($bookData);
$bookStatus = 'created';
}
// Handle summary upsert
$summaryData = $item['summary'] ?? null;
$summaryResult = null;
if ($summaryData && is_array($summaryData)) {
$summary = Summary::where('book_id', $book->id)->first();
$sumData = [
'content' => $summaryData['content'] ?? ($summary->content ?? null),
'key_insights' => $summaryData['key_insights'] ?? ($summary->key_insights ?? null),
'book_id' => $book->id,
];
if ($summary) {
$summary->fill($sumData)->save();
$summaryResult = ['status' => 'updated', 'id' => $summary->id];
} else {
$summary = Summary::create($sumData);
$summaryResult = ['status' => 'created', 'id' => $summary->id];
}
}
return [
'book' => [
'status' => $bookStatus,
'id' => $book->id,
'slug' => $book->slug,
],
'category' => $category ? [
'status' => $categoryStatus,
'id' => $category->id,
'slug' => $category->slug,
] : null,
'summary' => $summaryResult,
];
});
$results[] = $result;
}
return response()->json([
'results' => $results,
]);
}
private function isAssocArray($arr): bool
{
if (!is_array($arr)) return false;
return array_keys($arr) !== range(0, count($arr) - 1);
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers;
use App\Models\Book;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class BookController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$books = Book::with('category')->get();
return view('books.index', compact('books'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
$categories = Category::all();
return view('books.create', compact('categories'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'author' => 'required|string|max:255',
'description' => 'nullable|string',
'publication_date' => 'nullable|date',
'cover_image' => 'nullable|string|max:255',
'category_id' => 'required|exists:categories,id',
]);
Book::create([
'title' => $request->title,
'slug' => Str::slug($request->title),
'author' => $request->author,
'description' => $request->description,
'publication_date' => $request->publication_date,
'cover_image' => $request->cover_image,
'category_id' => $request->category_id,
]);
return redirect()->route('books.index')
->with('success', 'Book created successfully.');
}
/**
* Display the specified resource.
*/
public function show(Book $book)
{
return view('books.show', compact('book'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Book $book)
{
$categories = Category::all();
return view('books.edit', compact('book', 'categories'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Book $book)
{
$request->validate([
'title' => 'required|string|max:255',
'author' => 'required|string|max:255',
'description' => 'nullable|string',
'publication_date' => 'nullable|date',
'cover_image' => 'nullable|string|max:255',
'category_id' => 'required|exists:categories,id',
]);
$book->update([
'title' => $request->title,
'slug' => Str::slug($request->title),
'author' => $request->author,
'description' => $request->description,
'publication_date' => $request->publication_date,
'cover_image' => $request->cover_image,
'category_id' => $request->category_id,
]);
return redirect()->route('books.index')
->with('success', 'Book updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Book $book)
{
$book->delete();
return redirect()->route('books.index')
->with('success', 'Book deleted successfully.');
}
}
@@ -0,0 +1,94 @@
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class CategoryController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$categories = Category::all();
return view('categories.index', compact('categories'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('categories.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
]);
Category::create([
'name' => $request->name,
'slug' => Str::slug($request->name),
'description' => $request->description,
]);
return redirect()->route('categories.index')
->with('success', 'Category created successfully.');
}
/**
* Display the specified resource.
*/
public function show(Category $category)
{
return view('categories.show', compact('category'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Category $category)
{
return view('categories.edit', compact('category'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Category $category)
{
$request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
]);
$category->update([
'name' => $request->name,
'slug' => Str::slug($request->name),
'description' => $request->description,
]);
return redirect()->route('categories.index')
->with('success', 'Category updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Category $category)
{
$category->delete();
return redirect()->route('categories.index')
->with('success', 'Category deleted successfully.');
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
@@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers;
use App\Models\Summary;
use App\Models\Book;
use Illuminate\Http\Request;
class SummaryController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$summaries = Summary::with('book')->get();
return view('summaries.index', compact('summaries'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
$books = Book::all();
return view('summaries.create', compact('books'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'content' => 'required|string',
'key_insights' => 'nullable|string',
'book_id' => 'required|exists:books,id',
]);
Summary::create([
'content' => $request->input('content'),
'key_insights' => $request->input('key_insights'),
'book_id' => $request->input('book_id'),
]);
return redirect()->route('summaries.index')
->with('success', 'Summary created successfully.');
}
/**
* Display the specified resource.
*/
public function show(Summary $summary)
{
return view('summaries.show', compact('summary'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Summary $summary)
{
$books = Book::all();
return view('summaries.edit', compact('summary', 'books'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Summary $summary)
{
$request->validate([
'content' => 'required|string',
'key_insights' => 'nullable|string',
'book_id' => 'required|exists:books,id',
]);
$summary->update([
'content' => $request->input('content'),
'key_insights' => $request->input('key_insights'),
'book_id' => $request->input('book_id'),
]);
return redirect()->route('summaries.index')
->with('success', 'Summary updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Summary $summary)
{
$summary->delete();
return redirect()->route('summaries.index')
->with('success', 'Summary deleted successfully.');
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
protected $fillable = [
'title',
'slug',
'author',
'cover_image',
'publication_date',
'description',
'category_id',
];
/**
* Get the category that owns the book.
*/
public function category()
{
return $this->belongsTo(Category::class);
}
/**
* Get the summary associated with the book.
*/
public function summary()
{
return $this->hasOne(Summary::class);
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
protected $fillable = [
'name',
'slug',
'description',
];
/**
* Get the books for the category.
*/
public function books()
{
return $this->hasMany(Book::class);
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Summary extends Model
{
protected $fillable = [
'content',
'key_insights',
'book_id',
];
/**
* Get the book that owns the summary.
*/
public function book()
{
return $this->belongsTo(Book::class);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}