From d98bc7bbb0edbc48fc1522f92bb9dee13d770c83 Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Sun, 13 Oct 2024 20:16:08 +0330 Subject: [PATCH] blog api created --- .../Controllers/Blog/CategoryController.php | 121 +++++++++++++++++ app/Http/Controllers/Blog/HomeController.php | 52 +++++++ app/Http/Controllers/Blog/PostController.php | 127 ++++++++++++++++++ app/Http/Controllers/Blog/TagController.php | 72 ++++++++++ app/Http/Resources/CategoryResource.php | 32 +++++ app/Http/Resources/PostResource.php | 35 +++++ app/Http/Resources/TagResource.php | 29 ++++ app/Http/Resources/User.php | 8 +- app/Http/Resources/UserResource.php | 7 +- app/Models/Blog/Category.php | 25 ++++ app/Models/Blog/Post.php | 99 ++++++++++++++ app/Models/Blog/Tag.php | 22 +++ app/Models/User.php | 7 + database/factories/Blog/CategoryFactory.php | 29 ++++ database/factories/Blog/PostFactory.php | 41 ++++++ database/factories/Blog/TagFactory.php | 30 +++++ database/factories/UserFactory.php | 2 +- ...4_10_13_090800_create_categories_table.php | 32 +++++ .../2024_10_13_090821_create_tags_table.php | 29 ++++ .../2024_10_13_090833_create_posts_table.php | 39 ++++++ ...024_10_13_090850_create_post_tag_table.php | 36 +++++ ...0_13_090906_create_related_posts_table.php | 36 +++++ database/seeders/AboutMeSeeder.php | 58 ++++---- database/seeders/Blog/BlogSeeder.php | 46 +++++++ database/seeders/DatabaseSeeder.php | 8 +- database/seeders/ServiceSeeder.php | 24 ++-- routes/api.php | 20 ++- 27 files changed, 1022 insertions(+), 44 deletions(-) create mode 100644 app/Http/Controllers/Blog/CategoryController.php create mode 100644 app/Http/Controllers/Blog/HomeController.php create mode 100644 app/Http/Controllers/Blog/PostController.php create mode 100644 app/Http/Controllers/Blog/TagController.php create mode 100644 app/Http/Resources/CategoryResource.php create mode 100644 app/Http/Resources/PostResource.php create mode 100644 app/Http/Resources/TagResource.php create mode 100644 app/Models/Blog/Category.php create mode 100644 app/Models/Blog/Post.php create mode 100644 app/Models/Blog/Tag.php create mode 100644 database/factories/Blog/CategoryFactory.php create mode 100644 database/factories/Blog/PostFactory.php create mode 100644 database/factories/Blog/TagFactory.php create mode 100644 database/migrations/2024_10_13_090800_create_categories_table.php create mode 100644 database/migrations/2024_10_13_090821_create_tags_table.php create mode 100644 database/migrations/2024_10_13_090833_create_posts_table.php create mode 100644 database/migrations/2024_10_13_090850_create_post_tag_table.php create mode 100644 database/migrations/2024_10_13_090906_create_related_posts_table.php create mode 100644 database/seeders/Blog/BlogSeeder.php diff --git a/app/Http/Controllers/Blog/CategoryController.php b/app/Http/Controllers/Blog/CategoryController.php new file mode 100644 index 0000000..0de39e8 --- /dev/null +++ b/app/Http/Controllers/Blog/CategoryController.php @@ -0,0 +1,121 @@ +paginate(10); + return CategoryResource::collection($categories); + } + + // نمایش یک دسته‌بندی خاص + public function show($id) + { + $category = Category::with('posts')->findOrFail($id); + return new CategoryResource($category); + } + + /** + * نمایش دسته‌بندی و پست‌های مرتبط با آن بر اساس slug + * + * @param \Illuminate\Http\Request $request + * @param string $slug + * @return \Illuminate\Http\JsonResponse + */ + public function getCategoryWithPosts(Request $request, $slug) + { + // یافتن دسته‌بندی بر اساس slug + $category = Category::where('slug', $slug)->first(); + + if (!$category) { + return response()->json([ + 'message' => 'دسته‌بندی مورد نظر یافت نشد.' + ], 404); + } + + // دریافت تعداد پست‌ها در هر صفحه (پیش‌فرض: ۱۰) + $perPage = $request->input('per_page', 10); + + // دریافت پست‌های مربوط به این دسته‌بندی به ترتیب جدیدترین‌ها + $posts = $category->posts()->with(['category', 'tags', 'author']) + ->orderBy('published_at', 'desc') + ->paginate($perPage); + + // بازگشت اطلاعات دسته‌بندی و پست‌ها + return response()->json([ + 'category' => new CategoryResource($category), + 'posts' => PostResource::collection($posts), + 'pagination' => [ + 'total' => $posts->total(), + 'per_page' => $posts->perPage(), + 'current_page' => $posts->currentPage(), + 'last_page' => $posts->lastPage(), + 'from' => $posts->firstItem(), + 'to' => $posts->lastItem(), + ], + ], 200); + } + + // ذخیره یک دسته‌بندی جدید + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255|unique:categories,name', + 'slug' => 'required|string|unique:categories,slug', + 'description' => 'required|string', + 'short_description' => 'nullable|string|max:500', + 'image' => 'nullable|url', + ]); + + if ($validator->fails()) { + return response()->json($validator->errors(), 422); + } + + $category = Category::create($validator->validated()); + + return new CategoryResource($category); + } + + // به‌روزرسانی یک دسته‌بندی خاص + public function update(Request $request, $id) + { + $category = Category::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'name' => 'sometimes|required|string|max:255|unique:categories,name,' . $category->id, + 'slug' => 'sometimes|required|string|unique:categories,slug,' . $category->id, + 'description' => 'sometimes|required|string', + 'short_description' => 'nullable|string|max:500', + 'image' => 'nullable|url', + ]); + + if ($validator->fails()) { + return response()->json($validator->errors(), 422); + } + + $category->update($validator->validated()); + + return new CategoryResource($category); + } + + // حذف یک دسته‌بندی خاص + public function destroy($id) + { + $category = Category::findOrFail($id); + // قبل از حذف، مطمئن شوید که پست‌ها به دسته‌بندی دیگری منتقل شده‌اند یا حذف شوند + // برای سادگی، اینجا فقط دسته‌بندی را حذف می‌کنیم + $category->delete(); + + return response()->json(['message' => 'Category deleted successfully'], 200); + } +} diff --git a/app/Http/Controllers/Blog/HomeController.php b/app/Http/Controllers/Blog/HomeController.php new file mode 100644 index 0000000..d78ff7e --- /dev/null +++ b/app/Http/Controllers/Blog/HomeController.php @@ -0,0 +1,52 @@ +orderBy('published_at', 'desc') + ->take(5) + ->get(); + + // 2. 5 دسته‌بندی آخر + $latestCategories = Category::orderBy('created_at', 'desc') + ->take(5) + ->get(); + + // 3. 5 پست با بیشترین بازدید + $mostViewedPosts = Post::with(['category', 'tags', 'author']) + ->orderBy('views', 'desc') + ->take(5) + ->get(); + + // 4. 4 پست تصادفی + $randomPosts = Post::with(['category', 'tags', 'author']) + ->inRandomOrder() + ->take(4) + ->get(); + + // بازگشت به صورت JSON با استفاده از منابع (Resources) + return response()->json([ + 'latest_posts' => PostResource::collection($latestPosts), + 'latest_categories' => CategoryResource::collection($latestCategories), + 'most_viewed_posts' => PostResource::collection($mostViewedPosts), + 'random_posts' => PostResource::collection($randomPosts), + ], 200); + } +} diff --git a/app/Http/Controllers/Blog/PostController.php b/app/Http/Controllers/Blog/PostController.php new file mode 100644 index 0000000..7369582 --- /dev/null +++ b/app/Http/Controllers/Blog/PostController.php @@ -0,0 +1,127 @@ +only(['category', 'tag', 'search', 'author', 'published']); + + // دریافت پارامترهای مرتب‌سازی از درخواست + $sortField = $request->get('sort_field', 'published_at'); // فیلد پیش‌فرض: published_at + $sortDirection = $request->get('sort_direction', 'desc'); // جهت پیش‌فرض: نزولی + + // دریافت پارامترهای صفحه‌بندی از درخواست + $perPage = $request->get('per_page', 10); // تعداد آیتم‌ها در هر صفحه (پیش‌فرض: ۱۰) + + // ساخت Query با اعمال فیلترها و مرتب‌سازی + $query = Post::with(['category', 'tags', 'author']) + ->filter($filters) + ->sort($sortField, $sortDirection); + + // اعمال صفحه‌بندی + $posts = $query->paginate($perPage); + + // بازگشت نتایج به صورت JSON با استفاده از Resource + return PostResource::collection($posts); + } + + // نمایش یک پست خاص + public function show($slug) + { + // یافتن پست بر اساس slug و بارگذاری روابط مرتبط + $post = Post::with(['category', 'tags', 'author', 'relatedPosts.category', 'relatedPosts.tags', 'relatedPosts.author']) + ->where('slug', $slug) + ->firstOrFail(); // در صورت عدم وجود پست، خطای 404 برمی‌گرداند + + // بازگشت پست و پست‌های مرتبط به صورت JSON با استفاده از منابع + return response()->json([ + 'post' => new PostResource($post), + 'related_posts' => PostResource::collection($post->relatedPosts->sortByDesc('published_at')->take(5)), // بازگشت ۵ پست مرتبط جدیدترین + ], 200); + } + + // ذخیره یک پست جدید + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'body' => 'required|string', + 'excerpt' => 'nullable|string|max:500', + 'slug' => 'required|string|unique:posts,slug', + 'author_id' => 'required|exists:users,id', + 'category_id' => 'required|exists:categories,id', + 'featured_image' => 'nullable|url', + 'views' => 'nullable|integer', + 'likes' => 'nullable|integer', + 'published_at' => 'nullable|date', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', + ]); + + if ($validator->fails()) { + return response()->json($validator->errors(), 422); + } + + $post = Post::create($validator->validated()); + + if ($request->has('tags')) { + $post->tags()->attach($request->tags); + } + + return new PostResource($post->load(['category', 'tags', 'author'])); + } + + // به‌روزرسانی یک پست خاص + public function update(Request $request, $id) + { + $post = Post::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'title' => 'sometimes|required|string|max:255', + 'body' => 'sometimes|required|string', + 'excerpt' => 'nullable|string|max:500', + 'slug' => 'sometimes|required|string|unique:posts,slug,' . $post->id, + 'author_id' => 'sometimes|required|exists:users,id', + 'category_id' => 'sometimes|required|exists:categories,id', + 'featured_image' => 'nullable|url', + 'views' => 'nullable|integer', + 'likes' => 'nullable|integer', + 'published_at' => 'nullable|date', + 'tags' => 'nullable|array', + 'tags.*' => 'exists:tags,id', + ]); + + if ($validator->fails()) { + return response()->json($validator->errors(), 422); + } + + $post->update($validator->validated()); + + if ($request->has('tags')) { + $post->tags()->sync($request->tags); + } + + return new PostResource($post->load(['category', 'tags', 'author'])); + } + + // حذف یک پست خاص + public function destroy($id) + { + $post = Post::findOrFail($id); + $post->tags()->detach(); + $post->relatedPosts()->detach(); + $post->delete(); + + return response()->json(['message' => 'Post deleted successfully'], 200); + } +} diff --git a/app/Http/Controllers/Blog/TagController.php b/app/Http/Controllers/Blog/TagController.php new file mode 100644 index 0000000..22658ee --- /dev/null +++ b/app/Http/Controllers/Blog/TagController.php @@ -0,0 +1,72 @@ +paginate(10); + return TagResource::collection($tags); + } + + // نمایش یک تگ خاص + public function show($id) + { + $tag = Tag::with('posts')->findOrFail($id); + return new TagResource($tag); + } + + // ذخیره یک تگ جدید + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255|unique:tags,name', + 'slug' => 'required|string|unique:tags,slug', + ]); + + if ($validator->fails()) { + return response()->json($validator->errors(), 422); + } + + $tag = Tag::create($validator->validated()); + + return new TagResource($tag); + } + + // به‌روزرسانی یک تگ خاص + public function update(Request $request, $id) + { + $tag = Tag::findOrFail($id); + + $validator = Validator::make($request->all(), [ + 'name' => 'sometimes|required|string|max:255|unique:tags,name,' . $tag->id, + 'slug' => 'sometimes|required|string|unique:tags,slug,' . $tag->id, + ]); + + if ($validator->fails()) { + return response()->json($validator->errors(), 422); + } + + $tag->update($validator->validated()); + + return new TagResource($tag); + } + + // حذف یک تگ خاص + public function destroy($id) + { + $tag = Tag::findOrFail($id); + $tag->posts()->detach(); + $tag->delete(); + + return response()->json(['message' => 'Tag deleted successfully'], 200); + } +} diff --git a/app/Http/Resources/CategoryResource.php b/app/Http/Resources/CategoryResource.php new file mode 100644 index 0000000..bf21b51 --- /dev/null +++ b/app/Http/Resources/CategoryResource.php @@ -0,0 +1,32 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + 'description' => $this->description, + 'short_description' => $this->short_description, + 'image' => $this->image, + 'posts_count' => $this->whenLoaded('posts', function () { + return $this->posts->count(); + }), + 'created_at' => Carbon::parse($this->created_at)->format('M d Y'), + 'updated_at' => Carbon::parse($this->updated_at)->format('M d Y'), + ]; + } +} diff --git a/app/Http/Resources/PostResource.php b/app/Http/Resources/PostResource.php new file mode 100644 index 0000000..dcb4198 --- /dev/null +++ b/app/Http/Resources/PostResource.php @@ -0,0 +1,35 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'slug' => $this->slug, + 'excerpt' => $this->excerpt, + 'body' => $this->body, + 'author' => new UserResource($this->author), + 'category' => new CategoryResource($this->category), + 'tags' => TagResource::collection($this->tags), + 'featured_image' => $this->featured_image, + 'views' => $this->views, + 'likes' => $this->likes, + 'published_at' => $this->published_at, + 'created_at' => Carbon::parse($this->created_at)->format('M d Y'), + 'updated_at' => Carbon::parse($this->updated_at)->format('M d Y'), + ]; + } +} diff --git a/app/Http/Resources/TagResource.php b/app/Http/Resources/TagResource.php new file mode 100644 index 0000000..45ef441 --- /dev/null +++ b/app/Http/Resources/TagResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + 'posts_count' => $this->whenLoaded('posts', function () { + return $this->posts->count(); + }), + 'created_at' => Carbon::parse($this->created_at)->format('M d Y'), + 'updated_at' => Carbon::parse($this->updated_at)->format('M d Y'), + ]; + } +} diff --git a/app/Http/Resources/User.php b/app/Http/Resources/User.php index d59e3a0..97a875d 100644 --- a/app/Http/Resources/User.php +++ b/app/Http/Resources/User.php @@ -14,6 +14,12 @@ class User extends ResourceCollection */ public function toArray(Request $request): array { - return parent::toArray($request); + return [ + 'id' => $this->id, + 'name' => $this->name, + // اضافه کردن سایر فیلدهای مورد نیاز + 'email' => $this->email, + 'created_at' => $this->created_at, + ]; } } diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index e7eedd1..095e9d1 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -1,7 +1,8 @@ $this->id, 'name' => $this->name, 'email' => $this->email, - 'created_at' => $this->created_at, - 'updated_at' => $this->updated_at, + 'created_at' => Carbon::parse($this->created_at)->format('M d Y'), + 'updated_at' => Carbon::parse($this->updated_at)->format('M d Y'), ]; } } \ No newline at end of file diff --git a/app/Models/Blog/Category.php b/app/Models/Blog/Category.php new file mode 100644 index 0000000..33d3622 --- /dev/null +++ b/app/Models/Blog/Category.php @@ -0,0 +1,25 @@ +hasMany(Post::class); + } +} diff --git a/app/Models/Blog/Post.php b/app/Models/Blog/Post.php new file mode 100644 index 0000000..01801c7 --- /dev/null +++ b/app/Models/Blog/Post.php @@ -0,0 +1,99 @@ +belongsTo(User::class, 'author_id'); + } + + // رابطه با دسته‌بندی + public function category() + { + return $this->belongsTo(Category::class); + } + + // رابطه با تگ‌ها + public function tags() + { + return $this->belongsToMany(Tag::class)->withTimestamps(); + } + + // پست‌های مرتبط + public function relatedPosts() + { + return $this->belongsToMany(Post::class, 'related_posts', 'post_id', 'related_post_id')->withTimestamps(); + } + + // اسکوپ فیلتر + public function scopeFilter($query, $filters) + { + if (isset($filters['category'])) { + $query->whereHas('category', function ($q) use ($filters) { + $q->where('slug', $filters['category']); + }); + } + + if (isset($filters['tag'])) { + $query->whereHas('tags', function ($q) use ($filters) { + $q->where('slug', $filters['tag']); + }); + } + + if (isset($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('title', 'like', '%' . $filters['search'] . '%') + ->orWhere('body', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (isset($filters['author'])) { + $query->whereHas('author', function ($q) use ($filters) { + $q->where('id', $filters['author']); + }); + } + + if (isset($filters['published'])) { + $query->whereNotNull('published_at'); + } + + return $query; + } + + /** + * Scope برای اعمال مرتب‌سازی + */ + public function scopeSort($query, $sortField, $sortDirection = 'asc') + { + // بررسی اینکه فیلد مرتب‌سازی معتبر است یا خیر + $allowedSortFields = ['published_at', 'views', 'likes', 'title']; + if (in_array($sortField, $allowedSortFields)) { + $sortDirection = strtolower($sortDirection) === 'desc' ? 'desc' : 'asc'; + $query->orderBy($sortField, $sortDirection); + } + + return $query; + } +} diff --git a/app/Models/Blog/Tag.php b/app/Models/Blog/Tag.php new file mode 100644 index 0000000..0259f8b --- /dev/null +++ b/app/Models/Blog/Tag.php @@ -0,0 +1,22 @@ +belongsToMany(Post::class)->withTimestamps(); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index def621f..e612c2e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,6 +3,8 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; + +use App\Models\Blog\Post; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -44,4 +46,9 @@ protected function casts(): array 'password' => 'hashed', ]; } + + public function posts() + { + return $this->hasMany(Post::class, 'author_id'); + } } diff --git a/database/factories/Blog/CategoryFactory.php b/database/factories/Blog/CategoryFactory.php new file mode 100644 index 0000000..d2d7f2b --- /dev/null +++ b/database/factories/Blog/CategoryFactory.php @@ -0,0 +1,29 @@ + + */ +class CategoryFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $name = $this->faker->unique()->word; + return [ + 'name' => ucfirst($name), + 'slug' => Str::slug($name), + 'description' => $this->faker->paragraph, + 'short_description' => $this->faker->sentence, + 'image' => $this->faker->imageUrl(640, 480, 'business', true), + ]; + } +} diff --git a/database/factories/Blog/PostFactory.php b/database/factories/Blog/PostFactory.php new file mode 100644 index 0000000..1d1ad54 --- /dev/null +++ b/database/factories/Blog/PostFactory.php @@ -0,0 +1,41 @@ + + */ +class PostFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + + protected $model = Post::class; + + + public function definition(): array + { + $title = $this->faker->sentence; + return [ + 'title' => $title, + 'body' => $this->faker->paragraphs(5, true), + 'excerpt' => $this->faker->text(200), + 'slug' => Str::slug($title) . '-' . $this->faker->unique()->numberBetween(1, 1000), + 'author_id' => User::inRandomOrder()->first()->id ?? User::factory(), + 'category_id' => Category::inRandomOrder()->first()->id ?? Category::factory(), + 'featured_image' => $this->faker->imageUrl(800, 600, 'technics', true), + 'views' => $this->faker->numberBetween(0, 1000), + 'likes' => $this->faker->numberBetween(0, 500), + 'published_at' => $this->faker->dateTimeBetween('-1 years', 'now'), + ]; + } +} diff --git a/database/factories/Blog/TagFactory.php b/database/factories/Blog/TagFactory.php new file mode 100644 index 0000000..b9dc7a3 --- /dev/null +++ b/database/factories/Blog/TagFactory.php @@ -0,0 +1,30 @@ + + */ +class TagFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + + protected $model = Tag::class; + + public function definition(): array + { + $name = $this->faker->unique()->word; + return [ + 'name' => ucfirst($name), + 'slug' => Str::slug($name), + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 584104c..fe1b025 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -37,7 +37,7 @@ public function definition(): array */ public function unverified(): static { - return $this->state(fn (array $attributes) => [ + return $this->state(fn(array $attributes) => [ 'email_verified_at' => null, ]); } diff --git a/database/migrations/2024_10_13_090800_create_categories_table.php b/database/migrations/2024_10_13_090800_create_categories_table.php new file mode 100644 index 0000000..2821e00 --- /dev/null +++ b/database/migrations/2024_10_13_090800_create_categories_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name')->unique(); + $table->string('slug')->unique(); + $table->text('description'); + $table->text('short_description')->nullable(); + $table->string('image')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('categories'); + } +}; diff --git a/database/migrations/2024_10_13_090821_create_tags_table.php b/database/migrations/2024_10_13_090821_create_tags_table.php new file mode 100644 index 0000000..55e6fd9 --- /dev/null +++ b/database/migrations/2024_10_13_090821_create_tags_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name')->unique(); + $table->string('slug')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tags'); + } +}; diff --git a/database/migrations/2024_10_13_090833_create_posts_table.php b/database/migrations/2024_10_13_090833_create_posts_table.php new file mode 100644 index 0000000..4ce54a3 --- /dev/null +++ b/database/migrations/2024_10_13_090833_create_posts_table.php @@ -0,0 +1,39 @@ +id(); + $table->string('title'); + $table->text('body'); + $table->text('excerpt'); // توضیحات مختصر + $table->string('slug')->unique(); + $table->unsignedBigInteger('author_id'); // نویسنده + $table->foreign('author_id')->references('id')->on('users')->onDelete('cascade'); + $table->unsignedBigInteger('category_id'); // دسته بندی + $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade'); + $table->string('featured_image')->nullable(); // تصویر شاخص + $table->integer('views')->default(0); // تعداد بازدید + $table->integer('likes')->default(0); // تعداد لایک + $table->timestamp('published_at')->nullable(); // تاریخ انتشار + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('posts'); + } +}; diff --git a/database/migrations/2024_10_13_090850_create_post_tag_table.php b/database/migrations/2024_10_13_090850_create_post_tag_table.php new file mode 100644 index 0000000..88f930b --- /dev/null +++ b/database/migrations/2024_10_13_090850_create_post_tag_table.php @@ -0,0 +1,36 @@ +id(); + $table->unsignedBigInteger('post_id'); + $table->unsignedBigInteger('tag_id'); + $table->timestamps(); + + // ایجاد کلیدهای خارجی + $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade'); + $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); + + // جلوگیری از ورود تکراری + $table->unique(['post_id', 'tag_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('post_tag'); + } +}; diff --git a/database/migrations/2024_10_13_090906_create_related_posts_table.php b/database/migrations/2024_10_13_090906_create_related_posts_table.php new file mode 100644 index 0000000..fee379e --- /dev/null +++ b/database/migrations/2024_10_13_090906_create_related_posts_table.php @@ -0,0 +1,36 @@ +id(); + $table->unsignedBigInteger('post_id'); + $table->unsignedBigInteger('related_post_id'); + $table->timestamps(); + + // ایجاد کلیدهای خارجی + $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade'); + $table->foreign('related_post_id')->references('id')->on('posts')->onDelete('cascade'); + + // جلوگیری از ورود تکراری + $table->unique(['post_id', 'related_post_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('related_posts'); + } +}; diff --git a/database/seeders/AboutMeSeeder.php b/database/seeders/AboutMeSeeder.php index 194a2e4..1f91ed3 100644 --- a/database/seeders/AboutMeSeeder.php +++ b/database/seeders/AboutMeSeeder.php @@ -1,6 +1,7 @@ 'About Me Title', - 'description' => 'This is a brief description about me.', + 'title' => 'About Me', + 'description' => 'I am a web developer with over 10 years of experience in creating dynamic and user-centric web applications. Specializing in React and Next.js, I build scalable solutions that enhance performance and user experience. I enjoy tackling complex challenges, from improving legacy systems to developing new applications. My focus on frontend development and seamless backend integration allows me to deliver innovative results while staying current with industry trends.', 'image' => '/images/profile.png', ]); // ایجاد رکوردهای Skill Skill::create([ - 'title' => 'FrontEnd', + 'title' => 'HTML & CSS (SASS, Responsive Design)', 'image' => '/images/frontend.png', - 'description' => 'UI design description.', + 'description' => 'HTML & CSS (SASS, Responsive Design) description', 'link' => 'http://example.com/frontend', + 'percentage' => 95, + 'about_me_id' => $aboutMe->id, + ]); + + Skill::create([ + 'title' => 'React.js and Next.js Development', + 'image' => '/images/backend.png', + 'description' => 'React.js and Next.js Development description.', + 'link' => 'http://example.com/backend', + 'percentage' => 90, + 'about_me_id' => $aboutMe->id, + ]); + Skill::create([ + 'title' => 'Backend Development (RESTful APIs, Laravel)', + 'image' => '/images/ui.png', + 'description' => 'Backend Development (RESTful APIs, Laravel) description.', + 'link' => 'http://example.com/ui', + 'percentage' => 85, + 'about_me_id' => $aboutMe->id, + ]); + + Skill::create([ + 'title' => 'WordPress Development and Customization', + 'image' => '/images/ux.png', + 'description' => 'WordPress Development and Customization description.', + 'link' => 'http://example.com/ux', 'percentage' => 90, 'about_me_id' => $aboutMe->id, ]); Skill::create([ - 'title' => 'BackEnd', - 'image' => '/images/backend.png', - 'description' => 'BackEnd description.', - 'link' => 'http://example.com/backend', - 'percentage' => 80, - 'about_me_id' => $aboutMe->id, - ]); - Skill::create([ - 'title' => 'UI Design', - 'image' => '/images/ui.png', - 'description' => 'UI design description.', - 'link' => 'http://example.com/ui', - 'percentage' => 70, - 'about_me_id' => $aboutMe->id, - ]); - - Skill::create([ - 'title' => 'UX Design', + 'title' => 'Version Control (Git, GitHub)', 'image' => '/images/ux.png', - 'description' => 'UX design description.', + 'description' => 'Version Control (Git, GitHub) description.', 'link' => 'http://example.com/ux', - 'percentage' => 83, + 'percentage' => 80, 'about_me_id' => $aboutMe->id, ]); } diff --git a/database/seeders/Blog/BlogSeeder.php b/database/seeders/Blog/BlogSeeder.php new file mode 100644 index 0000000..bd92fa6 --- /dev/null +++ b/database/seeders/Blog/BlogSeeder.php @@ -0,0 +1,46 @@ +command->error('User with id 1 not found.'); + return; + } + + // ایجاد دسته‌بندی‌ها + Category::factory(10)->create(); + + // ایجاد تگ‌ها + Tag::factory(20)->create(); + + // ایجاد پست‌ها و تخصیص به کاربر با id = 1 + Post::factory(50)->create([ + 'author_id' => $user->id, // اختصاص پست‌ها به کاربر id 1 + ])->each(function ($post) { + // اختصاص تگ‌ها به پست + $tags = Tag::inRandomOrder()->take(rand(2, 5))->pluck('id'); + $post->tags()->attach($tags); + + // ایجاد پست‌های مرتبط + $relatedPosts = Post::inRandomOrder()->where('id', '!=', $post->id)->take(rand(1, 3))->pluck('id'); + $post->relatedPosts()->attach($relatedPosts); + }); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index cd25ae6..acbf9cf 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use App\Models\User; +use Database\Seeders\Blog\BlogSeeder; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; @@ -16,11 +17,14 @@ public function run(): void // User::factory(10)->create(); User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + 'name' => 'parsa aghayi', + 'email' => 'ceo@parsaaghayi.ir', ]); $this->call([ + AboutMeSeeder::class, ProjectSeeder::class, + ServiceSeeder::class, + BlogSeeder::class // دیگر seeders ]); } diff --git a/database/seeders/ServiceSeeder.php b/database/seeders/ServiceSeeder.php index 134cf34..63e4860 100644 --- a/database/seeders/ServiceSeeder.php +++ b/database/seeders/ServiceSeeder.php @@ -15,28 +15,28 @@ public function run() { $services = [ [ - 'title' => 'UI/UX', - 'description' => 'Lorem ipsum dolor sit amet consectetur. Morbi diam nisi nam diam interdum', + 'title' => 'Custom Web Development', + 'description' => 'We create tailored web applications that meet your unique business requirements, utilizing the latest technologies for optimal performance.', 'imageSrc' => '/images/ui-ux-vector.svg', - 'altText' => 'UI/UX Vector', + 'altText' => 'Custom Web Development Vector', ], [ - 'title' => 'Web Design', - 'description' => 'Lorem ipsum dolor sit amet consectetur. Morbi diam nisi nam diam interdum', + 'title' => 'Frontend Development', + 'description' => 'Our frontend development services focus on crafting engaging, responsive user interfaces that enhance user experience across all devices.', 'imageSrc' => '/images/web-design-vector.svg', - 'altText' => 'Web Design Vector', + 'altText' => 'Frontend Development Vector', ], [ - 'title' => 'App Design', - 'description' => 'Lorem ipsum dolor sit amet consectetur. Morbi diam nisi nam diam interdum', + 'title' => 'Backend Development', + 'description' => 'We provide reliable backend development solutions, building robust server-side architectures and APIs to ensure smooth and secure application functionality.', 'imageSrc' => '/images/app-design-vector.svg', - 'altText' => 'App Design Vector', + 'altText' => 'Backend Development Vector', ], [ - 'title' => 'Graphic Design', - 'description' => 'Lorem ipsum dolor sit amet consectetur. Morbi diam nisi nam diam interdum', + 'title' => 'Website Maintenance and Support', + 'description' => 'Our ongoing maintenance and support services keep your website updated, secure, and performing at its best, allowing you to focus on your core business.', 'imageSrc' => '/images/graphic-design-vector.svg', - 'altText' => 'Graphic Design Vector', + 'altText' => 'Website Maintenance and Support Vector', ], ]; diff --git a/routes/api.php b/routes/api.php index 7791d6c..4b61988 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,6 +4,10 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\ProjectController; use App\Http\Controllers\AboutMeController; +use App\Http\Controllers\Blog\CategoryController; +use App\Http\Controllers\Blog\HomeController; +use App\Http\Controllers\Blog\PostController; +use App\Http\Controllers\Blog\TagController; use App\Http\Controllers\SkillController; use App\Http\Controllers\ServiceController; @@ -15,4 +19,18 @@ Route::resource('projects', ProjectController::class); Route::resource('about-me', AboutMeController::class); Route::resource('skills', SkillController::class); -Route::apiResource('services', ServiceController::class); \ No newline at end of file +Route::apiResource('services', ServiceController::class); + +// مسیرهای بلاگ +Route::prefix('blog')->group(function () { + // پست‌ها + Route::apiResource('posts', PostController::class); + // دسته‌بندی‌ها + Route::apiResource('categories', CategoryController::class); + // تگ‌ها + Route::apiResource('tags', TagController::class); + // مسیر Homepage + Route::get('homepage', [HomeController::class, 'index']); + Route::get('categories/{slug}/posts', [CategoryController::class, 'getCategoryWithPosts']); + +});