active('WaitlistPage')) { return view('waitlist.index'); } // Get trending products (most upvoted in the last 7 days) $trendingProducts = Product::query() ->with(['user', 'category']) ->approved() ->where('created_at', '>=', now()->subDays(7)) ->withCount('upvotes') ->orderBy('upvotes_count', 'desc') ->limit(6) ->get(); // Get recently added products $recentProducts = Product::query() ->with(['user', 'category']) ->approved() ->latest() ->limit(12) ->get(); // Get popular categories with product counts $categories = Category::query() ->withCount(['products' => function($query) { $query->approved(); }]) ->having('products_count', '>', 0) ->orderBy('products_count', 'desc') ->limit(8) ->get(); return view('welcome', compact('trendingProducts', 'recentProducts', 'categories')); } /** * Display a listing of the products. */ public function index(Request $request) { $validated = $request->validate([ 'search' => 'nullable|string|max:255', 'category' => 'nullable|integer|exists:categories,id', 'status' => 'nullable|string|in:pending,approved,rejected', 'date_range' => 'nullable|string|in:today,week,month,year', 'sort' => 'nullable|string|in:newest,oldest,popular', ]); // Build the query with all filters and search $query = Product::query() ->with(['user', 'category']) ->approved() // Only show approved products on the public index ->search($request->search) ->byCategory($request->category) ->dateRange($request->date_range) ->sortBy($request->sort); // Paginate results with query parameters preserved $products = $query->paginate(12)->appends($request->query()); // Get all categories for the filter dropdown $categories = Category::all(); return view('products.index', compact('products', 'categories')); } /** * Show the form for creating a new product. */ public function create() { $categories = Category::all(); $tags = Tag::all(); return view('products.create', compact('categories', 'tags')); } /** * Store a newly created product in storage. */ public function store(Request $request) { Gate::authorize('create-product'); $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'required|string', 'website_url' => 'required|url', 'thumbnail' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', 'category_id' => 'required|exists:categories,id', 'tags' => 'nullable|array', 'tags.*' => 'exists:tags,id', ] + $this->detailRules()); foreach (array_keys(Product::BADGES) as $badge) { $validated[$badge] = $request->boolean($badge); } $validated['user_id'] = Auth::id(); $validated['status'] = 'pending'; // Handle thumbnail upload if ($request->hasFile('thumbnail')) { $path = $request->file('thumbnail')->store('thumbnails', 'public'); $validated['thumbnail'] = $path; } $product = Product::create($validated); // Attach tags if provided if ($request->has('tags')) { $product->tags()->attach($request->tags); } $this->syncDetails($request, $product); return redirect()->route('products.show', $product) ->with('success', 'Product submitted successfully! It will be reviewed by our moderators.'); } /** * Display the specified product. */ public function show(Product $product) { // Only show approved products to non-owners if (!$product->isApproved() && $product->user_id !== Auth::id() && !Auth::user()?->hasAnyRole(['admin', 'moderator'])) { abort(404); } $product->load(['user', 'category', 'tags', 'comments.user', 'upvotes', 'bookmarks']); return view('products.show', compact('product')); } /** * Show the form for editing the specified product. */ public function edit(Product $product) { Gate::authorize('edit-product', $product); $categories = Category::all(); $tags = Tag::all(); return view('products.edit', compact('product', 'categories', 'tags')); } /** * Update the specified product in storage. */ public function update(Request $request, Product $product) { Gate::authorize('edit-product', $product); $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'required|string', 'website_url' => 'required|url', 'thumbnail' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', 'category_id' => 'required|exists:categories,id', 'tags' => 'nullable|array', 'tags.*' => 'exists:tags,id', ] + $this->detailRules()); // Badges are locked for non-moderators once the product is approved if ($product->status !== 'approved' || Auth::user()->hasAnyRole(['moderator', 'admin'])) { foreach (array_keys(Product::BADGES) as $badge) { $validated[$badge] = $request->boolean($badge); } } else { $validated = array_diff_key($validated, Product::BADGES); } // Handle thumbnail upload if ($request->hasFile('thumbnail')) { // Delete old thumbnail if exists if ($product->thumbnail) { Storage::disk('public')->delete($product->thumbnail); } $path = $request->file('thumbnail')->store('thumbnails', 'public'); $validated['thumbnail'] = $path; } $product->update($validated); $this->syncDetails($request, $product); // Sync tags (will remove old tags and add new ones) $product->tags()->sync($request->tags ?? []); return redirect()->route('products.show', $product) ->with('success', 'Product updated successfully!'); } /** * Validation rules for the product detail fields. */ protected function detailRules(): array { return [ 'free_tier' => 'boolean', 'open_source' => 'boolean', 'eu_hosted' => 'boolean', 'eu_made' => 'boolean', 'screenshots' => 'nullable|array', 'screenshots.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048', 'remove_screenshots' => 'nullable|array', 'remove_screenshots.*' => 'integer', ]; } /** * Sync related product detail data (badges are stored on the product itself). */ protected function syncDetails(Request $request, Product $product): void { // Only rows belonging to this product may be removed $ids = array_filter((array) $request->input('remove_screenshots', [])); if ($ids) { $product->screenshots()->whereIn('id', $ids)->get()->each(function ($shot) { Storage::disk('public')->delete($shot->path); $shot->delete(); }); } if ($request->hasFile('screenshots')) { $position = ($product->screenshots()->max('position') ?? -1) + 1; foreach ($request->file('screenshots') as $file) { $product->screenshots()->create([ 'path' => $file->store('screenshots', 'public'), 'position' => $position++, ]); } } } /** * Remove the specified product from storage. */ public function destroy(Product $product) { Gate::authorize('delete-product', $product); // Delete thumbnail if exists if ($product->thumbnail) { Storage::disk('public')->delete($product->thumbnail); } foreach ($product->screenshots as $shot) { Storage::disk('public')->delete($shot->path); } $product->delete(); return redirect()->route('products.index') ->with('success', 'Product deleted successfully!'); } /** * Show the product management dashboard for admin/moderator. */ public function manage(Request $request) { Gate::authorize('approve-product'); $query = Product::with(['user', 'category']); // Filter by status if ($request->has('status') && in_array($request->status, ['pending', 'approved', 'rejected'])) { $query->byStatus($request->status); } else { $query->pending(); // Default to pending } $products = $query->latest()->paginate(20); $statusCounts = [ 'pending' => Product::pending()->count(), 'approved' => Product::approved()->count(), 'rejected' => Product::rejected()->count(), ]; return view('products.manage', compact('products', 'statusCounts')); } /** * Approve a product. */ public function approve(Product $product) { Gate::authorize('approve-product'); $product->approve(); return back()->with('success', 'Product approved successfully!'); } /** * Reject a product. */ public function reject(Product $product) { Gate::authorize('reject-product'); $product->reject(); return back()->with('success', 'Product rejected.'); } }