*/ protected $fillable = [ 'title', 'slug', 'description', 'website_url', 'thumbnail', 'user_id', 'category_id', 'status', 'approved_at', 'rejected_at', ]; /** * Get the attributes that should be cast. * * @return array */ protected function casts(): array { return [ 'approved_at' => 'datetime', 'rejected_at' => 'datetime', ]; } /** * Boot the model. */ protected static function boot() { parent::boot(); static::creating(function ($product) { if (empty($product->slug)) { $product->slug = static::generateUniqueSlug($product->title); } }); static::updating(function ($product) { if ($product->isDirty('title')) { $product->slug = static::generateUniqueSlug($product->title, $product->id); } }); } /** * Generate a unique slug from the title. */ protected static function generateUniqueSlug(string $title, ?int $ignoreId = null): string { $slug = Str::slug($title); $originalSlug = $slug; $counter = 1; while (static::slugExists($slug, $ignoreId)) { $slug = $originalSlug . '-' . $counter; $counter++; } return $slug; } /** * Check if a slug exists. */ protected static function slugExists(string $slug, ?int $ignoreId = null): bool { $query = static::where('slug', $slug); if ($ignoreId) { $query->where('id', '!=', $ignoreId); } return $query->exists(); } /** * Get the user that owns the product. */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * Get the category that owns the product. */ public function category(): BelongsTo { return $this->belongsTo(Category::class); } /** * Scope a query to only include approved products. */ public function scopeApproved(Builder $query): Builder { return $query->where('status', 'approved'); } /** * Scope a query to only include pending products. */ public function scopePending(Builder $query): Builder { return $query->where('status', 'pending'); } /** * Scope a query to only include rejected products. */ public function scopeRejected(Builder $query): Builder { return $query->where('status', 'rejected'); } /** * Scope a query to filter by status. */ public function scopeByStatus(Builder $query, string $status): Builder { return $query->where('status', $status); } /** * Scope a query to search products by title and description. */ public function scopeSearch(Builder $query, ?string $search): Builder { if (empty($search)) { return $query; } return $query->where(function ($subQuery) use ($search) { $subQuery->where('title', 'like', '%' . $search . '%') ->orWhere('description', 'like', '%' . $search . '%'); }); } /** * Scope a query to filter by category. */ public function scopeByCategory(Builder $query, ?int $categoryId): Builder { if (empty($categoryId)) { return $query; } return $query->where('category_id', $categoryId); } /** * Scope a query to filter by date range. */ public function scopeDateRange(Builder $query, ?string $range): Builder { if (empty($range)) { return $query; } return match($range) { 'today' => $query->whereDate('created_at', today()), 'week' => $query->where('created_at', '>=', now()->subWeek()), 'month' => $query->where('created_at', '>=', now()->subMonth()), 'year' => $query->where('created_at', '>=', now()->subYear()), default => $query, }; } /** * Scope a query to sort products by popularity (upvotes count). */ public function scopePopular(Builder $query): Builder { return $query->withCount('upvotes')->orderBy('upvotes_count', 'desc'); } /** * Scope a query to sort products. */ public function scopeSortBy(Builder $query, ?string $sort): Builder { if (empty($sort)) { return $query->latest(); } return match($sort) { 'popular' => $query->withCount('upvotes')->orderBy('upvotes_count', 'desc'), 'oldest' => $query->oldest(), 'newest' => $query->latest(), default => $query->latest(), }; } /** * Get the product's thumbnail URL. */ public function getThumbnailUrlAttribute(): ?string { if ($this->thumbnail) { return asset('storage/' . $this->thumbnail); } return null; } /** * Check if the product is pending. */ public function isPending(): bool { return $this->status === 'pending'; } /** * Check if the product is approved. */ public function isApproved(): bool { return $this->status === 'approved'; } /** * Check if the product is rejected. */ public function isRejected(): bool { return $this->status === 'rejected'; } /** * Approve the product. */ public function approve(): void { $this->update([ 'status' => 'approved', 'approved_at' => now(), 'rejected_at' => null, ]); } /** * Reject the product. */ public function reject(): void { $this->update([ 'status' => 'rejected', 'rejected_at' => now(), 'approved_at' => null, ]); } /** * Get the upvotes for the product. */ public function upvotes() { return $this->hasMany(Upvote::class); } /** * Get the comments for the product. */ public function comments() { return $this->hasMany(Comment::class)->latest(); } /** * Get the bookmarks for the product. */ public function bookmarks() { return $this->hasMany(Bookmark::class); } /** * Check if a user has upvoted the product. */ public function isUpvotedBy(?User $user): bool { if (!$user) { return false; } return $this->upvotes()->where('user_id', $user->id)->exists(); } /** * Check if a user has bookmarked the product. */ public function isBookmarkedBy(?User $user): bool { if (!$user) { return false; } return $this->bookmarks()->where('user_id', $user->id)->exists(); } /** * Get the upvotes count for the product. */ public function getUpvotesCountAttribute(): int { return $this->upvotes()->count(); } /** * Get the comments count for the product. */ public function getCommentsCountAttribute(): int { return $this->comments()->count(); } }