lots of improvements
This commit is contained in:
@@ -12,7 +12,7 @@ class BuildGameLibrary extends Command
|
|||||||
{--limit=100 : Number of games to fetch per batch}
|
{--limit=100 : Number of games to fetch per batch}
|
||||||
{--batches=10 : Number of batches to process}
|
{--batches=10 : Number of batches to process}
|
||||||
{--start-year=1980 : Start year for game releases}
|
{--start-year=1980 : Start year for game releases}
|
||||||
{--end-year=2024 : End year for game releases}';
|
{--end-year=2030 : End year for game releases}';
|
||||||
|
|
||||||
protected $description = 'Build a comprehensive game library by fetching games in batches';
|
protected $description = 'Build a comprehensive game library by fetching games in batches';
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Illuminate\Http\RedirectResponse;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\Rules;
|
use Illuminate\Validation\Rules;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ class RegisteredUserController extends Controller
|
|||||||
|
|
||||||
$user = User::create([
|
$user = User::create([
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
|
// 'slug' => $this->generateSlug($request->name),
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
'password' => Hash::make($request->password),
|
'password' => Hash::make($request->password),
|
||||||
]);
|
]);
|
||||||
@@ -47,4 +49,17 @@ class RegisteredUserController extends Controller
|
|||||||
|
|
||||||
return redirect(route('dashboard', absolute: false));
|
return redirect(route('dashboard', absolute: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateSlug(string $name): string
|
||||||
|
{
|
||||||
|
$slug = Str::slug($name);
|
||||||
|
$originalSlug = $slug;
|
||||||
|
$count = 1;
|
||||||
|
|
||||||
|
while (User::where('slug', $slug)->exists()) {
|
||||||
|
$slug = $originalSlug . '-' . $count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\ForumCategory;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ForumCategoryController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$categories = ForumCategory::orderBy('order')->get();
|
||||||
|
return view('forum.index', compact('categories'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
// This would be restricted to admins in a middleware
|
||||||
|
return view('forum.categories.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
// This would be restricted to admins in a middleware
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'order' => 'nullable|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validated['slug'] = Str::slug($validated['name']);
|
||||||
|
|
||||||
|
ForumCategory::create($validated);
|
||||||
|
|
||||||
|
return redirect()->route('forum.index')
|
||||||
|
->with('success', 'Category created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(ForumCategory $category)
|
||||||
|
{
|
||||||
|
$threads = $category->threads()
|
||||||
|
->with('user')
|
||||||
|
->orderBy('is_sticky', 'desc')
|
||||||
|
->orderBy('last_activity_at', 'desc')
|
||||||
|
->paginate(20);
|
||||||
|
|
||||||
|
return view('forum.categories.show', compact('category', 'threads'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(ForumCategory $category)
|
||||||
|
{
|
||||||
|
// This would be restricted to admins in a middleware
|
||||||
|
return view('forum.categories.edit', compact('category'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, ForumCategory $category)
|
||||||
|
{
|
||||||
|
// This would be restricted to admins in a middleware
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'order' => 'nullable|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->name !== $category->name) {
|
||||||
|
$validated['slug'] = Str::slug($validated['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$category->update($validated);
|
||||||
|
|
||||||
|
return redirect()->route('forum.index')
|
||||||
|
->with('success', 'Category updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(ForumCategory $category)
|
||||||
|
{
|
||||||
|
// This would be restricted to admins in a middleware
|
||||||
|
$category->delete();
|
||||||
|
|
||||||
|
return redirect()->route('forum.index')
|
||||||
|
->with('success', 'Category deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\ForumPost;
|
||||||
|
use App\Models\ForumTopic;
|
||||||
|
use App\Models\ForumThread;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ForumPostController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Not needed as posts are displayed within topics
|
||||||
|
return redirect()->route('forum.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$topic_id = $request->query('topic_id');
|
||||||
|
if (!$topic_id) {
|
||||||
|
return redirect()->route('forum.index')
|
||||||
|
->with('error', 'Topic ID is required to create a post.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$topic = ForumTopic::findOrFail($topic_id);
|
||||||
|
$thread = $topic->thread;
|
||||||
|
|
||||||
|
// Check if thread is locked
|
||||||
|
if ($thread->is_locked) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'This thread is locked. You cannot create new posts.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('forum.posts.create', compact('topic', 'thread'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'content' => 'required|string',
|
||||||
|
'topic_id' => 'required|exists:forum_topics,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$topic = ForumTopic::findOrFail($validated['topic_id']);
|
||||||
|
$thread = $topic->thread;
|
||||||
|
|
||||||
|
// Check if thread is locked
|
||||||
|
if ($thread->is_locked) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'This thread is locked. You cannot create new posts.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the post
|
||||||
|
$post = new ForumPost();
|
||||||
|
$post->topic_id = $validated['topic_id'];
|
||||||
|
$post->user_id = auth()->id();
|
||||||
|
$post->content = $validated['content'];
|
||||||
|
$post->save();
|
||||||
|
|
||||||
|
// Update topic and thread last activity
|
||||||
|
$topic->last_activity_at = now();
|
||||||
|
$topic->save();
|
||||||
|
|
||||||
|
$thread->last_activity_at = now();
|
||||||
|
$thread->save();
|
||||||
|
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('success', 'Post created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(ForumPost $post)
|
||||||
|
{
|
||||||
|
// Redirect to the topic with an anchor to the post
|
||||||
|
return redirect()->route('forum.topics.show', $post->topic) . '#post-' . $post->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(ForumPost $post)
|
||||||
|
{
|
||||||
|
// Check if user is authorized to edit this post
|
||||||
|
if (auth()->id() !== $post->user_id && !auth()->user()->hasRole('admin')) {
|
||||||
|
return redirect()->route('forum.topics.show', $post->topic)
|
||||||
|
->with('error', 'You are not authorized to edit this post.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$topic = $post->topic;
|
||||||
|
$thread = $topic->thread;
|
||||||
|
|
||||||
|
// Check if thread is locked
|
||||||
|
if ($thread->is_locked) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'This thread is locked. You cannot edit posts.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('forum.posts.edit', compact('post', 'topic', 'thread'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, ForumPost $post)
|
||||||
|
{
|
||||||
|
// Check if user is authorized to edit this post
|
||||||
|
if (auth()->id() !== $post->user_id && !auth()->user()->hasRole('admin')) {
|
||||||
|
return redirect()->route('forum.topics.show', $post->topic)
|
||||||
|
->with('error', 'You are not authorized to edit this post.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$topic = $post->topic;
|
||||||
|
$thread = $topic->thread;
|
||||||
|
|
||||||
|
// Check if thread is locked
|
||||||
|
if ($thread->is_locked) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'This thread is locked. You cannot edit posts.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'content' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update the post
|
||||||
|
$post->content = $validated['content'];
|
||||||
|
$post->edited_at = now();
|
||||||
|
$post->save();
|
||||||
|
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('success', 'Post updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(ForumPost $post)
|
||||||
|
{
|
||||||
|
// Check if user is authorized to delete this post
|
||||||
|
if (auth()->id() !== $post->user_id && !auth()->user()->hasRole('admin')) {
|
||||||
|
return redirect()->route('forum.topics.show', $post->topic)
|
||||||
|
->with('error', 'You are not authorized to delete this post.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$topic = $post->topic;
|
||||||
|
$thread = $topic->thread;
|
||||||
|
|
||||||
|
// Check if thread is locked
|
||||||
|
if ($thread->is_locked) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'This thread is locked. You cannot delete posts.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is the only post in the topic
|
||||||
|
if ($topic->posts()->count() <= 1) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'Cannot delete the only post in a topic. Delete the topic instead.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$post->delete();
|
||||||
|
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('success', 'Post deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\ForumCategory;
|
||||||
|
use App\Models\ForumThread;
|
||||||
|
use App\Models\ForumTopic;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ForumThreadController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$threads = ForumThread::with(['user', 'category'])
|
||||||
|
->orderBy('is_sticky', 'desc')
|
||||||
|
->orderBy('last_activity_at', 'desc')
|
||||||
|
->paginate(20);
|
||||||
|
|
||||||
|
return view('forum.threads.index', compact('threads'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$categories = ForumCategory::orderBy('order')->get();
|
||||||
|
$category_id = $request->query('category_id');
|
||||||
|
|
||||||
|
return view('forum.threads.create', compact('categories', 'category_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'content' => 'required|string',
|
||||||
|
'category_id' => 'required|exists:forum_categories,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create the thread
|
||||||
|
$thread = new ForumThread();
|
||||||
|
$thread->title = $validated['title'];
|
||||||
|
$thread->slug = Str::slug($validated['title']) . '-' . uniqid();
|
||||||
|
$thread->category_id = $validated['category_id'];
|
||||||
|
$thread->user_id = auth()->id();
|
||||||
|
$thread->last_activity_at = now();
|
||||||
|
$thread->save();
|
||||||
|
|
||||||
|
// Create the initial topic
|
||||||
|
$topic = new ForumTopic();
|
||||||
|
$topic->thread_id = $thread->id;
|
||||||
|
$topic->user_id = auth()->id();
|
||||||
|
$topic->title = $validated['title'];
|
||||||
|
$topic->content = $validated['content'];
|
||||||
|
$topic->last_activity_at = now();
|
||||||
|
$topic->save();
|
||||||
|
|
||||||
|
return redirect()->route('forum.threads.show', $thread)
|
||||||
|
->with('success', 'Thread created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(ForumThread $thread)
|
||||||
|
{
|
||||||
|
// Increment view count
|
||||||
|
$thread->increment('views');
|
||||||
|
|
||||||
|
// Get topics with pagination
|
||||||
|
$topics = $thread->topics()
|
||||||
|
->with('user')
|
||||||
|
->orderBy('is_pinned', 'desc')
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->paginate(15);
|
||||||
|
|
||||||
|
return view('forum.threads.show', compact('thread', 'topics'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(ForumThread $thread)
|
||||||
|
{
|
||||||
|
// Check if user is authorized to edit this thread
|
||||||
|
if (auth()->id() !== $thread->user_id && !auth()->user()->hasRole('admin')) {
|
||||||
|
return redirect()->route('forum.threads.show', $thread)
|
||||||
|
->with('error', 'You are not authorized to edit this thread.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$categories = ForumCategory::orderBy('order')->get();
|
||||||
|
$initialTopic = $thread->topics()->oldest()->first();
|
||||||
|
|
||||||
|
return view('forum.threads.edit', compact('thread', 'categories', 'initialTopic'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, ForumThread $thread)
|
||||||
|
{
|
||||||
|
// Check if user is authorized to edit this thread
|
||||||
|
if (auth()->id() !== $thread->user_id && !auth()->user()->hasRole('admin')) {
|
||||||
|
return redirect()->route('forum.threads.show', $thread)
|
||||||
|
->with('error', 'You are not authorized to edit this thread.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'content' => 'required|string',
|
||||||
|
'category_id' => 'required|exists:forum_categories,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update the thread
|
||||||
|
$thread->title = $validated['title'];
|
||||||
|
if ($thread->isDirty('title')) {
|
||||||
|
$thread->slug = Str::slug($validated['title']) . '-' . uniqid();
|
||||||
|
}
|
||||||
|
$thread->category_id = $validated['category_id'];
|
||||||
|
$thread->save();
|
||||||
|
|
||||||
|
// Update the initial topic
|
||||||
|
$initialTopic = $thread->topics()->oldest()->first();
|
||||||
|
if ($initialTopic) {
|
||||||
|
$initialTopic->title = $validated['title'];
|
||||||
|
$initialTopic->content = $validated['content'];
|
||||||
|
$initialTopic->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('forum.threads.show', $thread)
|
||||||
|
->with('success', 'Thread updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(ForumThread $thread)
|
||||||
|
{
|
||||||
|
// Check if user is authorized to delete this thread
|
||||||
|
if (auth()->id() !== $thread->user_id && !auth()->user()->hasRole('admin')) {
|
||||||
|
return redirect()->route('forum.threads.show', $thread)
|
||||||
|
->with('error', 'You are not authorized to delete this thread.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$categoryId = $thread->category_id;
|
||||||
|
$thread->delete();
|
||||||
|
|
||||||
|
return redirect()->route('forum.categories.show', ['category' => ForumCategory::find($categoryId)])
|
||||||
|
->with('success', 'Thread deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\ForumThread;
|
||||||
|
use App\Models\ForumTopic;
|
||||||
|
use App\Models\ForumPost;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ForumTopicController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Not needed as topics are displayed within threads
|
||||||
|
return redirect()->route('forum.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$thread_id = $request->query('thread_id');
|
||||||
|
if (!$thread_id) {
|
||||||
|
return redirect()->route('forum.index')
|
||||||
|
->with('error', 'Thread ID is required to create a topic.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$thread = ForumThread::findOrFail($thread_id);
|
||||||
|
|
||||||
|
// Check if thread is locked
|
||||||
|
if ($thread->is_locked) {
|
||||||
|
return redirect()->route('forum.threads.show', $thread)
|
||||||
|
->with('error', 'This thread is locked. You cannot create new topics.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('forum.topics.create', compact('thread'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'content' => 'required|string',
|
||||||
|
'thread_id' => 'required|exists:forum_threads,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$thread = ForumThread::findOrFail($validated['thread_id']);
|
||||||
|
|
||||||
|
// Check if thread is locked
|
||||||
|
if ($thread->is_locked) {
|
||||||
|
return redirect()->route('forum.threads.show', $thread)
|
||||||
|
->with('error', 'This thread is locked. You cannot create new topics.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the topic
|
||||||
|
$topic = new ForumTopic();
|
||||||
|
$topic->thread_id = $validated['thread_id'];
|
||||||
|
$topic->user_id = auth()->id();
|
||||||
|
$topic->title = $validated['title'];
|
||||||
|
$topic->content = $validated['content'];
|
||||||
|
$topic->last_activity_at = now();
|
||||||
|
$topic->save();
|
||||||
|
|
||||||
|
// Update thread's last activity
|
||||||
|
$thread->last_activity_at = now();
|
||||||
|
$thread->save();
|
||||||
|
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('success', 'Topic created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(ForumTopic $topic)
|
||||||
|
{
|
||||||
|
$thread = $topic->thread;
|
||||||
|
|
||||||
|
// Get posts with pagination
|
||||||
|
$posts = $topic->posts()
|
||||||
|
->with('user')
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->paginate(15);
|
||||||
|
|
||||||
|
return view('forum.topics.show', compact('topic', 'thread', 'posts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(ForumTopic $topic)
|
||||||
|
{
|
||||||
|
// Check if user is authorized to edit this topic
|
||||||
|
if (auth()->id() !== $topic->user_id && !auth()->user()->hasRole('admin')) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'You are not authorized to edit this topic.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$thread = $topic->thread;
|
||||||
|
|
||||||
|
// Check if thread is locked
|
||||||
|
if ($thread->is_locked) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'This thread is locked. You cannot edit topics.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('forum.topics.edit', compact('topic', 'thread'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, ForumTopic $topic)
|
||||||
|
{
|
||||||
|
// Check if user is authorized to edit this topic
|
||||||
|
if (auth()->id() !== $topic->user_id && !auth()->user()->hasRole('admin')) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'You are not authorized to edit this topic.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$thread = $topic->thread;
|
||||||
|
|
||||||
|
// Check if thread is locked
|
||||||
|
if ($thread->is_locked) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'This thread is locked. You cannot edit topics.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'content' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update the topic
|
||||||
|
$topic->title = $validated['title'];
|
||||||
|
$topic->content = $validated['content'];
|
||||||
|
$topic->save();
|
||||||
|
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('success', 'Topic updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(ForumTopic $topic)
|
||||||
|
{
|
||||||
|
// Check if user is authorized to delete this topic
|
||||||
|
if (auth()->id() !== $topic->user_id && !auth()->user()->hasRole('admin')) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'You are not authorized to delete this topic.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$thread = $topic->thread;
|
||||||
|
|
||||||
|
// Check if thread is locked
|
||||||
|
if ($thread->is_locked) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'This thread is locked. You cannot delete topics.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is the only topic in the thread
|
||||||
|
if ($thread->topics()->count() <= 1) {
|
||||||
|
return redirect()->route('forum.topics.show', $topic)
|
||||||
|
->with('error', 'Cannot delete the only topic in a thread. Delete the thread instead.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$topic->delete();
|
||||||
|
|
||||||
|
return redirect()->route('forum.threads.show', $thread)
|
||||||
|
->with('success', 'Topic deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,7 +101,77 @@ class GameController extends Controller
|
|||||||
|
|
||||||
public function show(Game $game)
|
public function show(Game $game)
|
||||||
{
|
{
|
||||||
$game->load(['platforms', 'genres', 'screenshots', 'videos', 'similar_games', 'dlcs', 'reviews.user']);
|
// Load game relationships except reviews
|
||||||
return view('games.show', compact('game'));
|
$game->load(['platforms', 'genres', 'screenshots', 'videos', 'similar_games', 'dlcs']);
|
||||||
|
|
||||||
|
// Load reviews with users separately (could be paginated if needed)
|
||||||
|
$reviews = $game->reviews()->with('user')->get();
|
||||||
|
|
||||||
|
// Calculate rating distribution percentages
|
||||||
|
$ratingDistribution = $this->calculateRatingDistribution($game);
|
||||||
|
|
||||||
|
return view('games.show', compact('game', 'ratingDistribution', 'reviews'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the rating distribution percentages for a game
|
||||||
|
*
|
||||||
|
* @param Game $game
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function calculateRatingDistribution(Game $game)
|
||||||
|
{
|
||||||
|
$distribution = [];
|
||||||
|
|
||||||
|
// Get the total number of reviews for this game
|
||||||
|
$totalReviews = $game->reviews()->count();
|
||||||
|
|
||||||
|
// If there are no reviews, return empty distribution
|
||||||
|
if ($totalReviews === 0) {
|
||||||
|
for ($i = 10; $i >= 1; $i--) {
|
||||||
|
$distribution[] = [
|
||||||
|
'range' => $i,
|
||||||
|
'count' => 0,
|
||||||
|
'percentage' => 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $distribution;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the count of reviews for each rating range directly from the database
|
||||||
|
$ratingCounts = $game->reviews()
|
||||||
|
->selectRaw('
|
||||||
|
CASE
|
||||||
|
WHEN rating = 1 THEN 1
|
||||||
|
WHEN rating = 2 THEN 2
|
||||||
|
WHEN rating = 3 THEN 3
|
||||||
|
WHEN rating = 4 THEN 4
|
||||||
|
WHEN rating = 5 THEN 5
|
||||||
|
WHEN rating = 6 THEN 6
|
||||||
|
WHEN rating = 7 THEN 7
|
||||||
|
WHEN rating = 8 THEN 8
|
||||||
|
WHEN rating = 9 THEN 9
|
||||||
|
WHEN rating = 10 THEN 10
|
||||||
|
END as range_id,
|
||||||
|
COUNT(*) as count
|
||||||
|
')
|
||||||
|
->groupBy('range_id')
|
||||||
|
->orderBy('range_id', 'desc')
|
||||||
|
->get()
|
||||||
|
->keyBy('range_id');
|
||||||
|
|
||||||
|
// Build the distribution array with percentages
|
||||||
|
for ($i = 10; $i >= 1; $i--) {
|
||||||
|
$count = $ratingCounts->get($i)->count ?? 0;
|
||||||
|
$percentage = ($count / $totalReviews) * 100;
|
||||||
|
|
||||||
|
$distribution[] = [
|
||||||
|
'range' => $i,
|
||||||
|
'count' => $count,
|
||||||
|
'percentage' => $percentage
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $distribution;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ class ReviewController extends Controller
|
|||||||
|
|
||||||
public function create(Game $game)
|
public function create(Game $game)
|
||||||
{
|
{
|
||||||
return view('reviews.create', compact('game'));
|
$platforms = $game->platforms()->get();
|
||||||
|
return view('reviews.create', compact('game', 'platforms'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request, Game $game)
|
public function store(Request $request, Game $game)
|
||||||
@@ -31,6 +32,7 @@ class ReviewController extends Controller
|
|||||||
'content' => 'required|string|min:10|max:1000',
|
'content' => 'required|string|min:10|max:1000',
|
||||||
'recommended' => 'sometimes|nullable|boolean',
|
'recommended' => 'sometimes|nullable|boolean',
|
||||||
'rating' => 'required|between:1,10',
|
'rating' => 'required|between:1,10',
|
||||||
|
'platform_id' => 'required|exists:platforms,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!isset($validated['recommended'])) {
|
if (!isset($validated['recommended'])) {
|
||||||
@@ -54,7 +56,8 @@ class ReviewController extends Controller
|
|||||||
public function edit(Game $game, Review $review)
|
public function edit(Game $game, Review $review)
|
||||||
{
|
{
|
||||||
$this->authorize('update', $review);
|
$this->authorize('update', $review);
|
||||||
return view('reviews.edit', compact('game', 'review'));
|
$platforms = $game->platforms;
|
||||||
|
return view('reviews.edit', compact('game', 'review', 'platforms'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request, Game $game, Review $review)
|
public function update(Request $request, Game $game, Review $review)
|
||||||
@@ -64,7 +67,8 @@ class ReviewController extends Controller
|
|||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'content' => 'required|string|min:10|max:1000',
|
'content' => 'required|string|min:10|max:1000',
|
||||||
'recommended' => 'required|boolean',
|
'recommended' => 'required|boolean',
|
||||||
'rating' => 'required|integer|between:1,20',
|
'rating' => 'required|integer|between:1,10',
|
||||||
|
'platform_id' => 'required|exists:platforms,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$review->update($validated);
|
$review->update($validated);
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ForumCategory extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'slug',
|
||||||
|
'description',
|
||||||
|
'order',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the threads for the category.
|
||||||
|
*/
|
||||||
|
public function threads()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ForumThread::class, 'category_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the route key for the model.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getRouteKeyName()
|
||||||
|
{
|
||||||
|
return 'slug';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ForumPost extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'topic_id',
|
||||||
|
'user_id',
|
||||||
|
'content',
|
||||||
|
'is_solution',
|
||||||
|
'edited_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_solution' => 'boolean',
|
||||||
|
'edited_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the topic that owns the post.
|
||||||
|
*/
|
||||||
|
public function topic()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ForumTopic::class, 'topic_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that owns the post.
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ForumThread extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'category_id',
|
||||||
|
'user_id',
|
||||||
|
'title',
|
||||||
|
'slug',
|
||||||
|
'is_sticky',
|
||||||
|
'is_locked',
|
||||||
|
'views',
|
||||||
|
'last_activity_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_sticky' => 'boolean',
|
||||||
|
'is_locked' => 'boolean',
|
||||||
|
'last_activity_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the category that owns the thread.
|
||||||
|
*/
|
||||||
|
public function category()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ForumCategory::class, 'category_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that owns the thread.
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the topics for the thread.
|
||||||
|
*/
|
||||||
|
public function topics()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ForumTopic::class, 'thread_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the route key for the model.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getRouteKeyName()
|
||||||
|
{
|
||||||
|
return 'slug';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ForumTopic extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'thread_id',
|
||||||
|
'user_id',
|
||||||
|
'title',
|
||||||
|
'content',
|
||||||
|
'is_pinned',
|
||||||
|
'last_activity_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_pinned' => 'boolean',
|
||||||
|
'last_activity_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the thread that owns the topic.
|
||||||
|
*/
|
||||||
|
public function thread()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ForumThread::class, 'thread_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user that owns the topic.
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the posts for the topic.
|
||||||
|
*/
|
||||||
|
public function posts()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ForumPost::class, 'topic_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ class Review extends Model
|
|||||||
'content',
|
'content',
|
||||||
'recommended',
|
'recommended',
|
||||||
'rating',
|
'rating',
|
||||||
'platform',
|
'platform_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@@ -33,4 +33,9 @@ class Review extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Game::class);
|
return $this->belongsTo(Game::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function platform(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Platform::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-2
@@ -9,11 +9,12 @@ use Illuminate\Notifications\Notifiable;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Multicaret\Acquaintances\Traits\Friendable;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable, Friendable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
@@ -47,7 +48,7 @@ class User extends Authenticatable
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function boot()
|
protected static function boot(): void
|
||||||
{
|
{
|
||||||
parent::boot();
|
parent::boot();
|
||||||
|
|
||||||
@@ -93,4 +94,28 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return $this->hasMany(GameList::class);
|
return $this->hasMany(GameList::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the forum threads created by the user.
|
||||||
|
*/
|
||||||
|
public function forumThreads()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ForumThread::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the forum topics created by the user.
|
||||||
|
*/
|
||||||
|
public function forumTopics()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ForumTopic::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the forum posts created by the user.
|
||||||
|
*/
|
||||||
|
public function forumPosts()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ForumPost::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -13,7 +13,8 @@
|
|||||||
"imdbphp/imdbphp": "^3.3",
|
"imdbphp/imdbphp": "^3.3",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"marcreichel/igdb-laravel": "^5.3"
|
"marcreichel/igdb-laravel": "^5.3",
|
||||||
|
"multicaret/laravel-acquaintances": "^3.7"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-debugbar": "^3.15",
|
"barryvdh/laravel-debugbar": "^3.15",
|
||||||
|
|||||||
Generated
+71
-1
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "0814da2c93b6041a9dc275ff1eda9043",
|
"content-hash": "9a128482d8d2ecd18a2ab97af440d77e",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -2216,6 +2216,76 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-03-24T10:02:05+00:00"
|
"time": "2025-03-24T10:02:05+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "multicaret/laravel-acquaintances",
|
||||||
|
"version": "v3.7.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/multicaret/laravel-acquaintances.git",
|
||||||
|
"reference": "18afb0079aa4aae580b7659db7ecd4480cc2fe96"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/multicaret/laravel-acquaintances/zipball/18afb0079aa4aae580b7659db7ecd4480cc2fe96",
|
||||||
|
"reference": "18afb0079aa4aae580b7659db7ecd4480cc2fe96",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"codeclimate/php-test-reporter": "^0.3.2",
|
||||||
|
"doctrine/dbal": "^2.5",
|
||||||
|
"fzaninotto/faker": "~1.4",
|
||||||
|
"laravel/laravel": "5.*|^9.0|^10.0|^11.0",
|
||||||
|
"mockery/mockery": "1.0.x-dev",
|
||||||
|
"phpunit/phpunit": "5.*"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Multicaret\\Acquaintances\\AcquaintancesServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Multicaret\\Acquaintances\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mohamed Kawsara",
|
||||||
|
"email": "mkwsra@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "This light package, with no dependencies, gives Eloquent models the ability to manage friendships (with groups). And interactions such as: Likes, favorites, votes, subscribe, follow, ..etc. And it includes advanced rating system.",
|
||||||
|
"keywords": [
|
||||||
|
"Followers",
|
||||||
|
"Rate",
|
||||||
|
"Rating",
|
||||||
|
"eloquent",
|
||||||
|
"followships",
|
||||||
|
"friend-system",
|
||||||
|
"friends",
|
||||||
|
"friendships",
|
||||||
|
"interactions",
|
||||||
|
"laravel",
|
||||||
|
"like",
|
||||||
|
"social-media",
|
||||||
|
"wish-list"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/multicaret/laravel-acquaintances/issues",
|
||||||
|
"source": "https://github.com/multicaret/laravel-acquaintances/tree/v3.7.2"
|
||||||
|
},
|
||||||
|
"time": "2025-03-23T22:12:11+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nesbot/carbon",
|
"name": "nesbot/carbon",
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
/**
|
||||||
|
* Load migrations from package migrations,
|
||||||
|
* If you published the migration files, please set to `false`.
|
||||||
|
*/
|
||||||
|
'migrations' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Models Related.
|
||||||
|
*/
|
||||||
|
'model_namespace' => (int) app()->version() <= 7 ? 'App' : 'App\Models',
|
||||||
|
'models' => [
|
||||||
|
/*
|
||||||
|
* Model name of User model
|
||||||
|
*/
|
||||||
|
'user' => 'User',
|
||||||
|
/*
|
||||||
|
* Model name of Interaction Relation model
|
||||||
|
*/
|
||||||
|
'interaction_relation' => \Multicaret\Acquaintances\Models\InteractionRelation::class,
|
||||||
|
/*
|
||||||
|
* Model name of Interaction Relation model
|
||||||
|
*/
|
||||||
|
'friendship' => \Multicaret\Acquaintances\Models\Friendship::class,
|
||||||
|
/*
|
||||||
|
* Model name of Interaction Relation model
|
||||||
|
*/
|
||||||
|
'friendship_groups' => \Multicaret\Acquaintances\Models\FriendFriendshipGroups::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'tables' => [
|
||||||
|
/*
|
||||||
|
* Table name of interactions relations.
|
||||||
|
*/
|
||||||
|
'interactions' => 'interactions',
|
||||||
|
/*
|
||||||
|
* user foreign key column name within interactions table.
|
||||||
|
*/
|
||||||
|
'interactions_user_id_fk_column_name' => 'user_id',
|
||||||
|
/*
|
||||||
|
* user foreign key column type within interactions table.
|
||||||
|
*/
|
||||||
|
'interactions_user_id_fk_column_type' => 'unsignedBigInteger',
|
||||||
|
/*
|
||||||
|
* Table name of friendships relations.
|
||||||
|
*/
|
||||||
|
'friendships' => 'friendships',
|
||||||
|
/*
|
||||||
|
* Table name of friendship Groups relations.
|
||||||
|
*/
|
||||||
|
'friendship_groups' => 'friendship_groups',
|
||||||
|
],
|
||||||
|
|
||||||
|
'rating' => [
|
||||||
|
'defaults' => [
|
||||||
|
'amount' => 5,
|
||||||
|
/*
|
||||||
|
* Default type here is 'general', as longs as you have one criteria of rating a model
|
||||||
|
* you can ignore this setting.
|
||||||
|
* It will be the default type of rating of null is provided, if you wish to tweak this type name
|
||||||
|
* use the value below as you wish.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
'type' => 'general',
|
||||||
|
],
|
||||||
|
'types' => [
|
||||||
|
/* Add any other type that your website/application have here,
|
||||||
|
* the following added rating types are for demonstration purposes only.
|
||||||
|
* There is no effect on deleting them nor adding to them, however, its a good practice
|
||||||
|
* to not hard code your rating types, hence, please use this simple array
|
||||||
|
*/
|
||||||
|
'delivery-time',
|
||||||
|
'quality',
|
||||||
|
'communication',
|
||||||
|
'commitment',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
|
||||||
|
'friendships_groups' => [
|
||||||
|
'acquaintances' => 0,
|
||||||
|
'close_friends' => 1,
|
||||||
|
'family' => 2
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -14,6 +14,7 @@ return new class extends Migration
|
|||||||
Schema::create('users', function (Blueprint $table) {
|
Schema::create('users', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
|
$table->string('slug')->unique();
|
||||||
$table->string('email')->unique();
|
$table->string('email')->unique();
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->string('password');
|
$table->string('password');
|
||||||
|
|||||||
+5
-4
@@ -11,8 +11,8 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('users', function (Blueprint $table) {
|
Schema::table('reviews', function (Blueprint $table) {
|
||||||
$table->string('slug')->unique()->after('name');
|
$table->foreignId('platform_id')->nullable()->after('rating')->constrained()->onDelete('set null');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,8 +21,9 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::table('users', function (Blueprint $table) {
|
Schema::table('reviews', function (Blueprint $table) {
|
||||||
$table->dropColumn('slug');
|
$table->dropForeign(['platform_id']);
|
||||||
|
$table->dropColumn('platform_id');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
|
||||||
|
class CreateAcquaintancesFriendshipTable extends Migration
|
||||||
|
{
|
||||||
|
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
|
||||||
|
Schema::create(config('acquaintances.tables.friendships'), function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('sender');
|
||||||
|
$table->morphs('recipient');
|
||||||
|
$table->string('status')->default('pending')->comment('pending/accepted/denied/blocked/');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists(config('acquaintances.tables.friendships'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CreateFriendshipsGroupsTable
|
||||||
|
*/
|
||||||
|
class CreateAcquaintancesFriendshipsGroupsTable extends Migration
|
||||||
|
{
|
||||||
|
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
|
||||||
|
Schema::create(config('acquaintances.tables.friendship_groups'), function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->unsignedBigInteger('friendship_id')->unsigned();
|
||||||
|
$table->morphs('friend');
|
||||||
|
$table->integer('group_id')->unsigned();
|
||||||
|
|
||||||
|
$table->foreign('friendship_id')
|
||||||
|
->references('id')
|
||||||
|
->on(config('acquaintances.tables.friendships'))
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->unique(['friendship_id', 'friend_id', 'friend_type', 'group_id'], 'unique');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists(config('acquaintances.tables.friendship_groups'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Multicaret\Acquaintances\Interaction;
|
||||||
|
|
||||||
|
class CreateAcquaintancesInteractionsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create(config('acquaintances.tables.interactions', 'interactions'), function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$userModel = Interaction::getUserModelName();
|
||||||
|
$userModel = (new $userModel);
|
||||||
|
$userIdFkColumnName = config('acquaintances.tables.interactions_user_id_fk_column_name', 'user_id');
|
||||||
|
|
||||||
|
$userIdFkType = config('acquaintances.tables.interactions_user_id_fk_column_type');
|
||||||
|
$table->{$userIdFkType}($userIdFkColumnName)->index();
|
||||||
|
$table->morphs('subject');
|
||||||
|
$table->string('relation')->default('follow')->comment('follow/like/subscribe/favorite/upvote/downvote');
|
||||||
|
$table->double('relation_value')->nullable();
|
||||||
|
$table->string('relation_type')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
|
||||||
|
$table->foreign($userIdFkColumnName)
|
||||||
|
->references($userModel->getKeyName())
|
||||||
|
->on($userModel->getTable())
|
||||||
|
->onUpdate('cascade')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table(config('acquaintances.tables.interactions', 'interactions'), function ($table) {
|
||||||
|
$table->dropForeign(config('acquaintances.tables.interactions', 'interactions').'_user_id_foreign');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::drop(config('acquaintances.tables.interactions', 'interactions'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('forum_categories', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->integer('order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('forum_categories');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('forum_threads', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('category_id')->constrained('forum_categories')->onDelete('cascade');
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->boolean('is_sticky')->default(false);
|
||||||
|
$table->boolean('is_locked')->default(false);
|
||||||
|
$table->integer('views')->default(0);
|
||||||
|
$table->timestamp('last_activity_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('forum_threads');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('forum_topics', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('thread_id')->constrained('forum_threads')->onDelete('cascade');
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('title');
|
||||||
|
$table->text('content');
|
||||||
|
$table->boolean('is_pinned')->default(false);
|
||||||
|
$table->timestamp('last_activity_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('forum_topics');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('forum_posts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('topic_id')->constrained('forum_topics')->onDelete('cascade');
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->text('content');
|
||||||
|
$table->boolean('is_solution')->default(false);
|
||||||
|
$table->timestamp('edited_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('forum_posts');
|
||||||
|
}
|
||||||
|
};
|
||||||
+3
-3
@@ -1,11 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
laravel.test:
|
laravel.test:
|
||||||
build:
|
build:
|
||||||
context: './vendor/laravel/sail/runtimes/8.4'
|
context: ./docker/8.4
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
WWWGROUP: '${WWWGROUP}'
|
WWWGROUP: '${WWWGROUP}'
|
||||||
image: 'sail-8.4/app'
|
image: sail-8.4/app
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'host.docker.internal:host-gateway'
|
- 'host.docker.internal:host-gateway'
|
||||||
ports:
|
ports:
|
||||||
@@ -36,7 +36,7 @@ services:
|
|||||||
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
||||||
volumes:
|
volumes:
|
||||||
- 'sail-mysql:/var/lib/mysql'
|
- 'sail-mysql:/var/lib/mysql'
|
||||||
- './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
- './docker/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
||||||
networks:
|
networks:
|
||||||
- sail
|
- sail
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=22
|
||||||
|
ARG POSTGRES_VERSION=17
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /usr/share/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y php8.0-cli php8.0-dev \
|
||||||
|
php8.0-pgsql php8.0-sqlite3 php8.0-gd php8.0-imagick \
|
||||||
|
php8.0-curl php8.0-memcached php8.0-mongodb \
|
||||||
|
php8.0-imap php8.0-mysql php8.0-mbstring \
|
||||||
|
php8.0-xml php8.0-zip php8.0-bcmath php8.0-soap \
|
||||||
|
php8.0-intl php8.0-readline php8.0-pcov \
|
||||||
|
php8.0-msgpack php8.0-igbinary php8.0-ldap \
|
||||||
|
php8.0-redis php8.0-swoole php8.0-xdebug \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y mysql-client \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN update-alternatives --set php /usr/bin/php8.0
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.0
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.0/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=22
|
||||||
|
ARG POSTGRES_VERSION=17
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /usr/share/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y php8.1-cli php8.1-dev \
|
||||||
|
php8.1-pgsql php8.1-sqlite3 php8.1-gd php8.1-imagick \
|
||||||
|
php8.1-curl php8.1-mongodb \
|
||||||
|
php8.1-imap php8.1-mysql php8.1-mbstring \
|
||||||
|
php8.1-xml php8.1-zip php8.1-bcmath php8.1-soap \
|
||||||
|
php8.1-intl php8.1-readline \
|
||||||
|
php8.1-ldap \
|
||||||
|
php8.1-msgpack php8.1-igbinary php8.1-redis php8.1-swoole \
|
||||||
|
php8.1-memcached php8.1-pcov php8.1-xdebug \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarn.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y mysql-client \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.1
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.1/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=22
|
||||||
|
ARG POSTGRES_VERSION=17
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y php8.2-cli php8.2-dev \
|
||||||
|
php8.2-pgsql php8.2-sqlite3 php8.2-gd php8.2-imagick \
|
||||||
|
php8.2-curl php8.2-mongodb \
|
||||||
|
php8.2-imap php8.2-mysql php8.2-mbstring \
|
||||||
|
php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \
|
||||||
|
php8.2-intl php8.2-readline \
|
||||||
|
php8.2-ldap \
|
||||||
|
php8.2-msgpack php8.2-igbinary php8.2-redis php8.2-swoole \
|
||||||
|
php8.2-memcached php8.2-pcov php8.2-xdebug \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g pnpm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y mysql-client \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.2
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.2/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=22
|
||||||
|
ARG MYSQL_CLIENT="mysql-client"
|
||||||
|
ARG POSTGRES_VERSION=17
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y php8.3-cli php8.3-dev \
|
||||||
|
php8.3-pgsql php8.3-sqlite3 php8.3-gd \
|
||||||
|
php8.3-curl php8.3-mongodb \
|
||||||
|
php8.3-imap php8.3-mysql php8.3-mbstring \
|
||||||
|
php8.3-xml php8.3-zip php8.3-bcmath php8.3-soap \
|
||||||
|
php8.3-intl php8.3-readline \
|
||||||
|
php8.3-ldap \
|
||||||
|
php8.3-msgpack php8.3-igbinary php8.3-redis \
|
||||||
|
php8.3-memcached php8.3-pcov php8.3-imagick php8.3-xdebug php8.3-swoole \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g pnpm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y $MYSQL_CLIENT \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.3
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.3/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="Taylor Otwell"
|
||||||
|
|
||||||
|
ARG WWWGROUP
|
||||||
|
ARG NODE_VERSION=22
|
||||||
|
ARG MYSQL_CLIENT="mysql-client"
|
||||||
|
ARG POSTGRES_VERSION=17
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||||
|
ENV SUPERVISOR_PHP_USER="sail"
|
||||||
|
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||||
|
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||||
|
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y php8.4-cli php8.4-dev \
|
||||||
|
php8.4-pgsql php8.4-sqlite3 php8.4-gd \
|
||||||
|
php8.4-curl php8.4-mongodb \
|
||||||
|
php8.4-imap php8.4-mysql php8.4-mbstring \
|
||||||
|
php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap \
|
||||||
|
php8.4-intl php8.4-readline \
|
||||||
|
php8.4-ldap \
|
||||||
|
php8.4-msgpack php8.4-igbinary php8.4-redis php8.4-swoole \
|
||||||
|
php8.4-memcached php8.4-pcov php8.4-imagick php8.4-xdebug \
|
||||||
|
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g npm \
|
||||||
|
&& npm install -g pnpm \
|
||||||
|
&& npm install -g bun \
|
||||||
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||||
|
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y yarn \
|
||||||
|
&& apt-get install -y $MYSQL_CLIENT \
|
||||||
|
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||||
|
&& apt-get -y autoremove \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.4
|
||||||
|
|
||||||
|
RUN userdel -r ubuntu
|
||||||
|
RUN groupadd --force -g $WWWGROUP sail
|
||||||
|
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||||
|
|
||||||
|
COPY start-container /usr/local/bin/start-container
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY php.ini /etc/php/8.4/cli/conf.d/99-sail.ini
|
||||||
|
RUN chmod +x /usr/local/bin/start-container
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
|
ENTRYPOINT ["start-container"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[PHP]
|
||||||
|
post_max_size = 100M
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
variables_order = EGPCS
|
||||||
|
pcov.directory = .
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||||
|
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$WWWUSER" ]; then
|
||||||
|
usermod -u $WWWUSER sail
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d /.composer ]; then
|
||||||
|
mkdir /.composer
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R ugo+rw /.composer
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||||
|
exec "$@"
|
||||||
|
else
|
||||||
|
exec gosu $WWWUSER "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
fi
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php]
|
||||||
|
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||||
|
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||||
|
environment=LARAVEL_SAIL="1"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
/usr/bin/mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS testing;
|
||||||
|
GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
|
||||||
|
EOSQL
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS testing;
|
||||||
|
GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
|
||||||
|
EOSQL
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
SELECT 'CREATE DATABASE testing'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'testing')\gexec
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Create Category - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
<li class="breadcrumb-item active">Create Category</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="h3 mb-0">Create New Category</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('forum.categories.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Category Name</label>
|
||||||
|
<input type="text" class="form-control bg-dark text-white @error('name') is-invalid @enderror" id="name" name="name" value="{{ old('name') }}" required>
|
||||||
|
@error('name')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="description" class="form-label">Description</label>
|
||||||
|
<textarea class="form-control bg-dark text-white @error('description') is-invalid @enderror" id="description" name="description" rows="3">{{ old('description') }}</textarea>
|
||||||
|
@error('description')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="order" class="form-label">Display Order</label>
|
||||||
|
<input type="number" class="form-control bg-dark text-white @error('order') is-invalid @enderror" id="order" name="order" value="{{ old('order', 0) }}">
|
||||||
|
<div class="form-text text-muted">Categories with lower numbers will be displayed first.</div>
|
||||||
|
@error('order')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-gradient">Create Category</button>
|
||||||
|
<a href="{{ route('forum.index') }}" class="btn btn-outline-light">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Edit Category - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.categories.show', $category) }}" class="text-decoration-none">{{ $category->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">Edit</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="h3 mb-0">Edit Category</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('forum.categories.update', $category) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Category Name</label>
|
||||||
|
<input type="text" class="form-control bg-dark text-white @error('name') is-invalid @enderror" id="name" name="name" value="{{ old('name', $category->name) }}" required>
|
||||||
|
@error('name')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="description" class="form-label">Description</label>
|
||||||
|
<textarea class="form-control bg-dark text-white @error('description') is-invalid @enderror" id="description" name="description" rows="3">{{ old('description', $category->description) }}</textarea>
|
||||||
|
@error('description')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="order" class="form-label">Display Order</label>
|
||||||
|
<input type="number" class="form-control bg-dark text-white @error('order') is-invalid @enderror" id="order" name="order" value="{{ old('order', $category->order) }}">
|
||||||
|
<div class="form-text text-muted">Categories with lower numbers will be displayed first.</div>
|
||||||
|
@error('order')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-gradient">Update Category</button>
|
||||||
|
<a href="{{ route('forum.categories.show', $category) }}" class="btn btn-outline-light">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', $category->name . ' - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ $category->name }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="h2 fw-bold gradient-text">{{ $category->name }}</h1>
|
||||||
|
<p class="text-muted">{{ $category->description }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@auth
|
||||||
|
<a href="{{ route('forum.threads.create', ['category_id' => $category->id]) }}" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-plus me-1"></i> New Thread
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-sign-in-alt me-1"></i> Login to Post
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($threads->isEmpty())
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
|
||||||
|
<h3 class="text-muted">No threads yet</h3>
|
||||||
|
<p class="text-muted">Be the first to start a discussion in this category!</p>
|
||||||
|
@auth
|
||||||
|
<a href="{{ route('forum.threads.create', ['category_id' => $category->id]) }}" class="btn btn-gradient mt-3">
|
||||||
|
<i class="fas fa-plus me-1"></i> Create Thread
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="btn btn-gradient mt-3">
|
||||||
|
<i class="fas fa-sign-in-alt me-1"></i> Login to Create Thread
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Threads</h5>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-outline-light dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-sort me-1"></i> Sort
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><a class="dropdown-item" href="{{ route('forum.categories.show', ['category' => $category, 'sort' => 'latest']) }}">Latest Activity</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ route('forum.categories.show', ['category' => $category, 'sort' => 'newest']) }}">Newest</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ route('forum.categories.show', ['category' => $category, 'sort' => 'views']) }}">Most Viewed</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
@foreach($threads as $thread)
|
||||||
|
<div class="list-group-item bg-dark text-white border-secondary {{ $thread->is_sticky ? 'bg-dark bg-opacity-75' : '' }}">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
@if($thread->is_sticky)
|
||||||
|
<i class="fas fa-thumbtack fa-lg text-warning"></i>
|
||||||
|
@elseif($thread->is_locked)
|
||||||
|
<i class="fas fa-lock fa-lg text-danger"></i>
|
||||||
|
@else
|
||||||
|
<i class="fas fa-comments fa-lg text-primary"></i>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">
|
||||||
|
<a href="{{ route('forum.threads.show', $thread) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $thread->title }}
|
||||||
|
</a>
|
||||||
|
@if($thread->is_sticky)
|
||||||
|
<span class="badge bg-warning text-dark ms-2">Sticky</span>
|
||||||
|
@endif
|
||||||
|
@if($thread->is_locked)
|
||||||
|
<span class="badge bg-danger ms-2">Locked</span>
|
||||||
|
@endif
|
||||||
|
</h5>
|
||||||
|
<div class="text-muted small">
|
||||||
|
Started by <a href="{{ route('profile.show', $thread->user) }}" class="text-decoration-none">{{ $thread->user->name }}</a>
|
||||||
|
<span class="mx-1">•</span>
|
||||||
|
<span>{{ $thread->created_at->diffForHumans() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 text-center">
|
||||||
|
<div class="text-muted">
|
||||||
|
<span class="d-block">{{ $thread->topics->count() }}</span>
|
||||||
|
<small>Topics</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 text-center">
|
||||||
|
<div class="text-muted">
|
||||||
|
<span class="d-block">{{ $thread->views }}</span>
|
||||||
|
<small>Views</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
@if($thread->last_activity_at)
|
||||||
|
<div class="small text-end">
|
||||||
|
<div class="text-muted">
|
||||||
|
<small>
|
||||||
|
<i class="far fa-clock me-1"></i> {{ $thread->last_activity_at->diffForHumans() }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
{{ $threads->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@auth
|
||||||
|
@if(auth()->user()->hasRole('admin'))
|
||||||
|
<div class="mt-4 d-flex gap-2">
|
||||||
|
<a href="{{ route('forum.categories.edit', $category) }}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-edit me-1"></i> Edit Category
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('forum.categories.destroy', $category) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to delete this category? All threads and posts will be deleted.')">
|
||||||
|
<i class="fas fa-trash me-1"></i> Delete Category
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h2 fw-bold gradient-text">Forum</h1>
|
||||||
|
@auth
|
||||||
|
@if(auth()->user()->hasRole('admin'))
|
||||||
|
<a href="{{ route('forum.categories.create') }}" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-plus me-1"></i> New Category
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($categories->isEmpty())
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
|
||||||
|
<h3 class="text-muted">No categories yet</h3>
|
||||||
|
<p class="text-muted">Check back later for forum discussions.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Categories</h5>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
@foreach($categories as $category)
|
||||||
|
<div class="list-group-item bg-dark text-white border-secondary">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-folder fa-2x text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">
|
||||||
|
<a href="{{ route('forum.categories.show', $category) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $category->name }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<p class="text-muted mb-0">{{ $category->description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 text-center">
|
||||||
|
<div class="text-muted">
|
||||||
|
<span class="d-block">{{ $category->threads->count() }}</span>
|
||||||
|
<small>Threads</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
@if($category->threads->count() > 0)
|
||||||
|
@php
|
||||||
|
$latestThread = $category->threads()->orderBy('last_activity_at', 'desc')->first();
|
||||||
|
@endphp
|
||||||
|
<div class="small">
|
||||||
|
<a href="{{ route('forum.threads.show', $latestThread) }}" class="text-white">
|
||||||
|
{{ Str::limit($latestThread->title, 30) }}
|
||||||
|
</a>
|
||||||
|
<div class="text-muted">
|
||||||
|
<small>
|
||||||
|
<i class="far fa-clock me-1"></i> {{ $latestThread->last_activity_at->diffForHumans() }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="text-muted small">No threads yet</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Forum Statistics</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h3 class="gradient-text">{{ \App\Models\ForumThread::count() }}</h3>
|
||||||
|
<p class="text-muted">Threads</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h3 class="gradient-text">{{ \App\Models\ForumTopic::count() }}</h3>
|
||||||
|
<p class="text-muted">Topics</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h3 class="gradient-text">{{ \App\Models\ForumPost::count() }}</h3>
|
||||||
|
<p class="text-muted">Posts</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Reply to Topic - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.categories.show', $thread->category) }}" class="text-decoration-none">{{ $thread->category->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.threads.show', $thread) }}" class="text-decoration-none">{{ $thread->title }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.topics.show', $topic) }}" class="text-decoration-none">{{ $topic->title }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">Reply</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="h3 mb-0">Reply to Topic</h1>
|
||||||
|
<p class="text-muted mb-0">{{ $topic->title }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Original Topic Content (for reference) -->
|
||||||
|
<div class="card bg-dark border-secondary mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-user-circle fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<a href="{{ route('profile.show', $topic->user) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $topic->user->name }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<div class="text-muted small">
|
||||||
|
<span>{{ $topic->created_at->format('M j, Y g:i A') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="topic-content">
|
||||||
|
{!! nl2br(e(Str::limit($topic->content, 300))) !!}
|
||||||
|
@if(strlen($topic->content) > 300)
|
||||||
|
<a href="{{ route('forum.topics.show', $topic) }}" class="text-primary">Read full topic</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reply Form -->
|
||||||
|
<form action="{{ route('forum.posts.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="topic_id" value="{{ $topic->id }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="content" class="form-label">Your Reply</label>
|
||||||
|
<textarea class="form-control bg-dark text-white @error('content') is-invalid @enderror" id="content" name="content" rows="10" required>{{ old('content') }}</textarea>
|
||||||
|
@error('content')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-gradient">Post Reply</button>
|
||||||
|
<a href="{{ route('forum.topics.show', $topic) }}" class="btn btn-outline-light">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Edit Reply - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.categories.show', $post->topic->thread->category) }}" class="text-decoration-none">{{ $post->topic->thread->category->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.threads.show', $post->topic->thread) }}" class="text-decoration-none">{{ $post->topic->thread->title }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.topics.show', $post->topic) }}" class="text-decoration-none">{{ $post->topic->title }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">Edit Reply</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="h3 mb-0">Edit Reply</h1>
|
||||||
|
<p class="text-muted mb-0">in topic: {{ $post->topic->title }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('forum.posts.update', $post) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="content" class="form-label">Content</label>
|
||||||
|
<textarea class="form-control bg-dark text-white @error('content') is-invalid @enderror" id="content" name="content" rows="10" required>{{ old('content', $post->content) }}</textarea>
|
||||||
|
@error('content')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(auth()->user()->hasRole('admin'))
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="is_solution" name="is_solution" value="1" {{ old('is_solution', $post->is_solution) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="is_solution">
|
||||||
|
Mark as Solution
|
||||||
|
</label>
|
||||||
|
<div class="form-text text-muted">Mark this post as the solution to the topic.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-gradient">Update Reply</button>
|
||||||
|
<a href="{{ route('forum.topics.show', $post->topic) }}#post-{{ $post->id }}" class="btn btn-outline-light">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Create Thread - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
@if(isset($category_id))
|
||||||
|
@php
|
||||||
|
$category = \App\Models\ForumCategory::find($category_id);
|
||||||
|
@endphp
|
||||||
|
@if($category)
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.categories.show', $category) }}" class="text-decoration-none">{{ $category->name }}</a></li>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
<li class="breadcrumb-item active">Create Thread</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="h3 mb-0">Create New Thread</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('forum.threads.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="category_id" class="form-label">Category</label>
|
||||||
|
<select class="form-select bg-dark text-white @error('category_id') is-invalid @enderror" id="category_id" name="category_id" required>
|
||||||
|
<option value="">Select a category</option>
|
||||||
|
@foreach($categories as $category)
|
||||||
|
<option value="{{ $category->id }}" {{ old('category_id', $category_id) == $category->id ? 'selected' : '' }}>
|
||||||
|
{{ $category->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('category_id')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="title" class="form-label">Thread Title</label>
|
||||||
|
<input type="text" class="form-control bg-dark text-white @error('title') is-invalid @enderror" id="title" name="title" value="{{ old('title') }}" required>
|
||||||
|
@error('title')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="content" class="form-label">Initial Post Content</label>
|
||||||
|
<textarea class="form-control bg-dark text-white @error('content') is-invalid @enderror" id="content" name="content" rows="10" required>{{ old('content') }}</textarea>
|
||||||
|
<div class="form-text text-muted">This will be the first topic in your thread.</div>
|
||||||
|
@error('content')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-gradient">Create Thread</button>
|
||||||
|
@if(isset($category_id) && $category)
|
||||||
|
<a href="{{ route('forum.categories.show', $category) }}" class="btn btn-outline-light">Cancel</a>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('forum.index') }}" class="btn btn-outline-light">Cancel</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Edit Thread - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.categories.show', $thread->category) }}" class="text-decoration-none">{{ $thread->category->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.threads.show', $thread) }}" class="text-decoration-none">{{ $thread->title }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">Edit</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="h3 mb-0">Edit Thread</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('forum.threads.update', $thread) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="category_id" class="form-label">Category</label>
|
||||||
|
<select class="form-select bg-dark text-white @error('category_id') is-invalid @enderror" id="category_id" name="category_id" required>
|
||||||
|
@foreach($categories as $category)
|
||||||
|
<option value="{{ $category->id }}" {{ old('category_id', $thread->category_id) == $category->id ? 'selected' : '' }}>
|
||||||
|
{{ $category->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('category_id')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="title" class="form-label">Thread Title</label>
|
||||||
|
<input type="text" class="form-control bg-dark text-white @error('title') is-invalid @enderror" id="title" name="title" value="{{ old('title', $thread->title) }}" required>
|
||||||
|
@error('title')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(isset($initialTopic))
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="content" class="form-label">Initial Post Content</label>
|
||||||
|
<textarea class="form-control bg-dark text-white @error('content') is-invalid @enderror" id="content" name="content" rows="10" required>{{ old('content', $initialTopic->content) }}</textarea>
|
||||||
|
@error('content')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(auth()->user()->hasRole('admin'))
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="is_sticky" name="is_sticky" value="1" {{ old('is_sticky', $thread->is_sticky) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="is_sticky">
|
||||||
|
Sticky Thread
|
||||||
|
</label>
|
||||||
|
<div class="form-text text-muted">Sticky threads appear at the top of the category.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="is_locked" name="is_locked" value="1" {{ old('is_locked', $thread->is_locked) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="is_locked">
|
||||||
|
Lock Thread
|
||||||
|
</label>
|
||||||
|
<div class="form-text text-muted">Locked threads cannot receive new topics or posts.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-gradient">Update Thread</button>
|
||||||
|
<a href="{{ route('forum.threads.show', $thread) }}" class="btn btn-outline-light">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', $thread->title . ' - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.categories.show', $thread->category) }}" class="text-decoration-none">{{ $thread->category->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ $thread->title }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="h2 fw-bold gradient-text">{{ $thread->title }}</h1>
|
||||||
|
<div class="text-muted">
|
||||||
|
<span>Started by <a href="{{ route('profile.show', $thread->user) }}" class="text-decoration-none">{{ $thread->user->name }}</a></span>
|
||||||
|
<span class="mx-1">•</span>
|
||||||
|
<span>{{ $thread->created_at->format('M j, Y g:i A') }}</span>
|
||||||
|
<span class="mx-1">•</span>
|
||||||
|
<span>{{ $thread->views }} views</span>
|
||||||
|
@if($thread->is_sticky)
|
||||||
|
<span class="badge bg-warning text-dark ms-2">Sticky</span>
|
||||||
|
@endif
|
||||||
|
@if($thread->is_locked)
|
||||||
|
<span class="badge bg-danger ms-2">Locked</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@auth
|
||||||
|
@if(!$thread->is_locked)
|
||||||
|
<a href="{{ route('forum.topics.create', ['thread_id' => $thread->id]) }}" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-plus me-1"></i> New Topic
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@if(auth()->id() === $thread->user_id || auth()->user()->hasRole('admin'))
|
||||||
|
<div class="dropdown d-inline-block ms-2">
|
||||||
|
<button class="btn btn-outline-light dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('forum.threads.edit', $thread) }}" class="dropdown-item">
|
||||||
|
<i class="fas fa-edit me-1"></i> Edit Thread
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@if(auth()->user()->hasRole('admin'))
|
||||||
|
<li>
|
||||||
|
<form action="{{ route('forum.threads.update', $thread) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<input type="hidden" name="is_sticky" value="{{ $thread->is_sticky ? 0 : 1 }}">
|
||||||
|
<input type="hidden" name="title" value="{{ $thread->title }}">
|
||||||
|
<input type="hidden" name="category_id" value="{{ $thread->category_id }}">
|
||||||
|
<button type="submit" class="dropdown-item">
|
||||||
|
<i class="fas fa-thumbtack me-1"></i> {{ $thread->is_sticky ? 'Unsticky' : 'Sticky' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form action="{{ route('forum.threads.update', $thread) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<input type="hidden" name="is_locked" value="{{ $thread->is_locked ? 0 : 1 }}">
|
||||||
|
<input type="hidden" name="title" value="{{ $thread->title }}">
|
||||||
|
<input type="hidden" name="category_id" value="{{ $thread->category_id }}">
|
||||||
|
<button type="submit" class="dropdown-item">
|
||||||
|
<i class="fas fa-lock me-1"></i> {{ $thread->is_locked ? 'Unlock' : 'Lock' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li>
|
||||||
|
<form action="{{ route('forum.threads.destroy', $thread) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="dropdown-item text-danger" onclick="return confirm('Are you sure you want to delete this thread? All topics and posts will be deleted.')">
|
||||||
|
<i class="fas fa-trash me-1"></i> Delete Thread
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-sign-in-alt me-1"></i> Login to Post
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($topics->isEmpty())
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
|
||||||
|
<h3 class="text-muted">No topics yet</h3>
|
||||||
|
<p class="text-muted">Be the first to start a topic in this thread!</p>
|
||||||
|
@auth
|
||||||
|
@if(!$thread->is_locked)
|
||||||
|
<a href="{{ route('forum.topics.create', ['thread_id' => $thread->id]) }}" class="btn btn-gradient mt-3">
|
||||||
|
<i class="fas fa-plus me-1"></i> Create Topic
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<div class="alert alert-danger mt-3">
|
||||||
|
<i class="fas fa-lock me-1"></i> This thread is locked. You cannot create new topics.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="btn btn-gradient mt-3">
|
||||||
|
<i class="fas fa-sign-in-alt me-1"></i> Login to Create Topic
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="topics-list">
|
||||||
|
@foreach($topics as $topic)
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-user-circle fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<a href="{{ route('forum.topics.show', $topic) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $topic->title }}
|
||||||
|
</a>
|
||||||
|
@if($topic->is_pinned)
|
||||||
|
<span class="badge bg-warning text-dark ms-2">Pinned</span>
|
||||||
|
@endif
|
||||||
|
</h5>
|
||||||
|
<div class="text-muted small">
|
||||||
|
Posted by <a href="{{ route('profile.show', $topic->user) }}" class="text-decoration-none">{{ $topic->user->name }}</a>
|
||||||
|
<span class="mx-1">•</span>
|
||||||
|
<span>{{ $topic->created_at->diffForHumans() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
<span>{{ $topic->posts->count() }} {{ Str::plural('reply', $topic->posts->count()) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="topic-content">
|
||||||
|
{!! nl2br(e(Str::limit($topic->content, 300))) !!}
|
||||||
|
@if(strlen($topic->content) > 300)
|
||||||
|
<a href="{{ route('forum.topics.show', $topic) }}" class="text-primary">Read more</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-dark border-top border-secondary">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<a href="{{ route('forum.topics.show', $topic) }}" class="btn btn-link text-white p-0">
|
||||||
|
View full topic
|
||||||
|
</a>
|
||||||
|
@auth
|
||||||
|
@if(auth()->id() === $topic->user_id || auth()->user()->hasRole('admin'))
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{{ route('forum.topics.edit', $topic) }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('forum.topics.destroy', $topic) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Are you sure you want to delete this topic?')">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
{{ $topics->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($thread->is_locked)
|
||||||
|
<div class="alert alert-danger mt-4">
|
||||||
|
<i class="fas fa-lock me-1"></i> This thread is locked. You cannot create new topics.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Create Topic - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.categories.show', $thread->category) }}" class="text-decoration-none">{{ $thread->category->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.threads.show', $thread) }}" class="text-decoration-none">{{ $thread->title }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">Create Topic</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="h3 mb-0">Create New Topic</h1>
|
||||||
|
<p class="text-muted mb-0">in thread: {{ $thread->title }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('forum.topics.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="thread_id" value="{{ $thread->id }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="title" class="form-label">Topic Title</label>
|
||||||
|
<input type="text" class="form-control bg-dark text-white @error('title') is-invalid @enderror" id="title" name="title" value="{{ old('title') }}" required>
|
||||||
|
@error('title')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="content" class="form-label">Content</label>
|
||||||
|
<textarea class="form-control bg-dark text-white @error('content') is-invalid @enderror" id="content" name="content" rows="10" required>{{ old('content') }}</textarea>
|
||||||
|
@error('content')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-gradient">Create Topic</button>
|
||||||
|
<a href="{{ route('forum.threads.show', $thread) }}" class="btn btn-outline-light">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Edit Topic - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.categories.show', $thread->category) }}" class="text-decoration-none">{{ $thread->category->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.threads.show', $thread) }}" class="text-decoration-none">{{ $thread->title }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.topics.show', $topic) }}" class="text-decoration-none">{{ $topic->title }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">Edit</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="h3 mb-0">Edit Topic</h1>
|
||||||
|
<p class="text-muted mb-0">in thread: {{ $thread->title }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('forum.topics.update', $topic) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="title" class="form-label">Topic Title</label>
|
||||||
|
<input type="text" class="form-control bg-dark text-white @error('title') is-invalid @enderror" id="title" name="title" value="{{ old('title', $topic->title) }}" required>
|
||||||
|
@error('title')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="content" class="form-label">Content</label>
|
||||||
|
<textarea class="form-control bg-dark text-white @error('content') is-invalid @enderror" id="content" name="content" rows="10" required>{{ old('content', $topic->content) }}</textarea>
|
||||||
|
@error('content')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(auth()->user()->hasRole('admin'))
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="is_pinned" name="is_pinned" value="1" {{ old('is_pinned', $topic->is_pinned) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="is_pinned">
|
||||||
|
Pin Topic
|
||||||
|
</label>
|
||||||
|
<div class="form-text text-muted">Pinned topics appear at the top of the thread.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-gradient">Update Topic</button>
|
||||||
|
<a href="{{ route('forum.topics.show', $topic) }}" class="btn btn-outline-light">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', $topic->title . ' - Forum - ClearGame')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container">
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.index') }}" class="text-decoration-none">Forum</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.categories.show', $thread->category) }}" class="text-decoration-none">{{ $thread->category->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('forum.threads.show', $thread) }}" class="text-decoration-none">{{ $thread->title }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ $topic->title }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="h2 fw-bold gradient-text">{{ $topic->title }}</h1>
|
||||||
|
<div class="text-muted">
|
||||||
|
<span>Started by <a href="{{ route('profile.show', $topic->user) }}" class="text-decoration-none">{{ $topic->user->name }}</a></span>
|
||||||
|
<span class="mx-1">•</span>
|
||||||
|
<span>{{ $topic->created_at->format('M j, Y g:i A') }}</span>
|
||||||
|
@if($topic->is_pinned)
|
||||||
|
<span class="badge bg-warning text-dark ms-2">Pinned</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@auth
|
||||||
|
@if(!$thread->is_locked)
|
||||||
|
<a href="{{ route('forum.posts.create', ['topic_id' => $topic->id]) }}" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-reply me-1"></i> Reply
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@if(auth()->id() === $topic->user_id || auth()->user()->hasRole('admin'))
|
||||||
|
<div class="dropdown d-inline-block ms-2">
|
||||||
|
<button class="btn btn-outline-light dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('forum.topics.edit', $topic) }}" class="dropdown-item">
|
||||||
|
<i class="fas fa-edit me-1"></i> Edit Topic
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@if(auth()->user()->hasRole('admin'))
|
||||||
|
<li>
|
||||||
|
<form action="{{ route('forum.topics.update', $topic) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<input type="hidden" name="is_pinned" value="{{ $topic->is_pinned ? 0 : 1 }}">
|
||||||
|
<input type="hidden" name="title" value="{{ $topic->title }}">
|
||||||
|
<input type="hidden" name="content" value="{{ $topic->content }}">
|
||||||
|
<button type="submit" class="dropdown-item">
|
||||||
|
<i class="fas fa-thumbtack me-1"></i> {{ $topic->is_pinned ? 'Unpin' : 'Pin' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li>
|
||||||
|
<form action="{{ route('forum.topics.destroy', $topic) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="dropdown-item text-danger" onclick="return confirm('Are you sure you want to delete this topic? All posts will be deleted.')">
|
||||||
|
<i class="fas fa-trash me-1"></i> Delete Topic
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-sign-in-alt me-1"></i> Login to Reply
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Original Topic Post -->
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-user-circle fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<a href="{{ route('profile.show', $topic->user) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $topic->user->name }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<div class="text-muted small">
|
||||||
|
<span>{{ $topic->created_at->format('M j, Y g:i A') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="topic-content">
|
||||||
|
{!! nl2br(e($topic->content)) !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-dark border-top border-secondary">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<!-- Placeholder for potential like/share buttons -->
|
||||||
|
</div>
|
||||||
|
@auth
|
||||||
|
@if(auth()->id() === $topic->user_id || auth()->user()->hasRole('admin'))
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{{ route('forum.topics.edit', $topic) }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Replies/Posts -->
|
||||||
|
@if($posts->isEmpty())
|
||||||
|
<div class="card bg-dark text-white mb-4">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
|
||||||
|
<h3 class="text-muted">No replies yet</h3>
|
||||||
|
<p class="text-muted">Be the first to reply to this topic!</p>
|
||||||
|
@auth
|
||||||
|
@if(!$thread->is_locked)
|
||||||
|
<a href="{{ route('forum.posts.create', ['topic_id' => $topic->id]) }}" class="btn btn-gradient mt-3">
|
||||||
|
<i class="fas fa-reply me-1"></i> Post Reply
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<div class="alert alert-danger mt-3">
|
||||||
|
<i class="fas fa-lock me-1"></i> This thread is locked. You cannot post replies.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="btn btn-gradient mt-3">
|
||||||
|
<i class="fas fa-sign-in-alt me-1"></i> Login to Reply
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<h3 class="h5 mb-3">Replies ({{ $posts->count() }})</h3>
|
||||||
|
<div class="posts-list">
|
||||||
|
@foreach($posts as $post)
|
||||||
|
<div class="card bg-dark text-white mb-4" id="post-{{ $post->id }}">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-user-circle fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<a href="{{ route('profile.show', $post->user) }}" class="text-white text-decoration-none">
|
||||||
|
{{ $post->user->name }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<div class="text-muted small">
|
||||||
|
<span>{{ $post->created_at->format('M j, Y g:i A') }}</span>
|
||||||
|
@if($post->edited_at)
|
||||||
|
<span class="mx-1">•</span>
|
||||||
|
<span>Edited {{ $post->edited_at->diffForHumans() }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="#post-{{ $post->id }}" class="text-muted text-decoration-none">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="post-content">
|
||||||
|
{!! nl2br(e($post->content)) !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-dark border-top border-secondary">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
@if($post->is_solution)
|
||||||
|
<span class="badge bg-success">
|
||||||
|
<i class="fas fa-check me-1"></i> Solution
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@auth
|
||||||
|
@if(auth()->id() === $post->user_id || auth()->user()->hasRole('admin'))
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{{ route('forum.posts.edit', $post) }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('forum.posts.destroy', $post) }}" method="POST" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Are you sure you want to delete this post?')">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
{{ $posts->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Reply Form -->
|
||||||
|
@auth
|
||||||
|
@if(!$thread->is_locked)
|
||||||
|
<div class="card bg-dark text-white mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Post a Reply</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('forum.posts.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="topic_id" value="{{ $topic->id }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<textarea name="content" class="form-control bg-dark text-white" rows="5" placeholder="Write your reply here..." required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-gradient">
|
||||||
|
<i class="fas fa-paper-plane me-1"></i> Post Reply
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="alert alert-danger mt-4">
|
||||||
|
<i class="fas fa-lock me-1"></i> This thread is locked. You cannot post replies.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -38,61 +38,74 @@
|
|||||||
<i class="fas fa-filter me-1"></i> Filter
|
<i class="fas fa-filter me-1"></i> Filter
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu p-3" style="min-width: 300px;">
|
<ul class="dropdown-menu p-3" style="min-width: 300px;">
|
||||||
<li class="mb-2">
|
<form action="{{ route('games.index') }}" method="GET">
|
||||||
<label class="form-label text-white mb-1"><i class="fas fa-tv me-1"></i> Platform</label>
|
<!-- Preserve any existing query parameters -->
|
||||||
<select name="platform" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
@if(request('search'))
|
||||||
<option value="">All Platforms</option>
|
<input type="hidden" name="search" value="{{ request('search') }}">
|
||||||
@foreach($platforms as $platform)
|
@endif
|
||||||
<option value="{{ $platform->slug }}" {{ request('platform') == $platform->slug ? 'selected' : '' }}>
|
@if(request('sort'))
|
||||||
{{ $platform->name }}
|
<input type="hidden" name="sort" value="{{ request('sort') }}">
|
||||||
</option>
|
@endif
|
||||||
@endforeach
|
@if(request('direction'))
|
||||||
</select>
|
<input type="hidden" name="direction" value="{{ request('direction') }}">
|
||||||
</li>
|
@endif
|
||||||
<li class="mb-2">
|
|
||||||
<label class="form-label text-white mb-1"><i class="fas fa-tags me-1"></i> Genre</label>
|
<li class="mb-2">
|
||||||
<select name="genre" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
<label class="form-label text-white mb-1"><i class="fas fa-tv me-1"></i> Platform</label>
|
||||||
<option value="">All Genres</option>
|
<select name="platform" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
||||||
@foreach($genres as $genre)
|
<option value="">All Platforms</option>
|
||||||
<option value="{{ $genre->slug }}" {{ request('genre') == $genre->slug ? 'selected' : '' }}>
|
@foreach($platforms as $platform)
|
||||||
{{ $genre->name }}
|
<option value="{{ $platform->slug }}" {{ request('platform') == $platform->slug ? 'selected' : '' }}>
|
||||||
</option>
|
{{ $platform->name }}
|
||||||
@endforeach
|
</option>
|
||||||
</select>
|
@endforeach
|
||||||
</li>
|
</select>
|
||||||
<li class="mb-2">
|
</li>
|
||||||
<label class="form-label text-white mb-1"><i class="fas fa-calendar-alt me-1"></i> Release Year</label>
|
<li class="mb-2">
|
||||||
<select name="year" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
<label class="form-label text-white mb-1"><i class="fas fa-tags me-1"></i> Genre</label>
|
||||||
<option value="">All Years</option>
|
<select name="genre" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
||||||
@foreach($years as $year)
|
<option value="">All Genres</option>
|
||||||
<option value="{{ $year }}" {{ request('year') == $year ? 'selected' : '' }}>
|
@foreach($genres as $genre)
|
||||||
{{ $year }}
|
<option value="{{ $genre->slug }}" {{ request('genre') == $genre->slug ? 'selected' : '' }}>
|
||||||
</option>
|
{{ $genre->name }}
|
||||||
@endforeach
|
</option>
|
||||||
</select>
|
@endforeach
|
||||||
</li>
|
</select>
|
||||||
<li class="mb-2">
|
</li>
|
||||||
<label class="form-label text-white mb-1"><i class="fas fa-star me-1"></i> Rating Range</label>
|
<li class="mb-2">
|
||||||
<select name="rating_range" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
<label class="form-label text-white mb-1"><i class="fas fa-calendar-alt me-1"></i> Release Year</label>
|
||||||
<option value="">All Ratings</option>
|
<select name="year" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
||||||
<option value="90-100" {{ request('rating_range') == '90-100' ? 'selected' : '' }}>90+</option>
|
<option value="">All Years</option>
|
||||||
<option value="80-89" {{ request('rating_range') == '80-89' ? 'selected' : '' }}>80-89</option>
|
@foreach($years as $year)
|
||||||
<option value="70-79" {{ request('rating_range') == '70-79' ? 'selected' : '' }}>70-79</option>
|
<option value="{{ $year }}" {{ request('year') == $year ? 'selected' : '' }}>
|
||||||
<option value="60-69" {{ request('rating_range') == '60-69' ? 'selected' : '' }}>60-69</option>
|
{{ $year }}
|
||||||
<option value="0-59" {{ request('rating_range') == '0-59' ? 'selected' : '' }}>Below 60</option>
|
</option>
|
||||||
</select>
|
@endforeach
|
||||||
</li>
|
</select>
|
||||||
<li class="mt-3">
|
</li>
|
||||||
<a href="{{ route('games.index') }}" class="btn btn-outline-gradient btn-sm w-100">
|
<li class="mb-2">
|
||||||
<i class="fas fa-times me-1"></i> Clear Filters
|
<label class="form-label text-white mb-1"><i class="fas fa-star me-1"></i> Rating Range</label>
|
||||||
</a>
|
<select name="rating_range" class="form-select bg-dark text-white border-0" onchange="this.form.submit()">
|
||||||
</li>
|
<option value="">All Ratings</option>
|
||||||
|
<option value="90-100" {{ request('rating_range') == '90-100' ? 'selected' : '' }}>90+</option>
|
||||||
|
<option value="80-89" {{ request('rating_range') == '80-89' ? 'selected' : '' }}>80-89</option>
|
||||||
|
<option value="70-79" {{ request('rating_range') == '70-79' ? 'selected' : '' }}>70-79</option>
|
||||||
|
<option value="60-69" {{ request('rating_range') == '60-69' ? 'selected' : '' }}>60-69</option>
|
||||||
|
<option value="0-59" {{ request('rating_range') == '0-59' ? 'selected' : '' }}>Below 60</option>
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
<li class="mt-3">
|
||||||
|
<a href="{{ route('games.index') }}" class="btn btn-outline-gradient btn-sm w-100">
|
||||||
|
<i class="fas fa-times me-1"></i> Clear Filters
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</form>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(request()->hasAny(['platform', 'genre', 'year ', 'rating_range']))
|
@if(request()->hasAny(['platform', 'genre', 'year', 'rating_range']))
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
@if(request('platform'))
|
@if(request('platform'))
|
||||||
|
|||||||
@@ -3,48 +3,29 @@
|
|||||||
@section('title', $game->name)
|
@section('title', $game->name)
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<!-- Hero Section -->
|
<div class="container mt-4">
|
||||||
<div class="game-hero position-relative mb-5">
|
<!-- Main Game Info Section -->
|
||||||
@if($game->screenshots()->count())
|
<div class="row mb-4">
|
||||||
<div class="game-hero-bg position-absolute w-100 h-100" style="background: linear-gradient(rgba(18, 18, 18, 0.7), rgba(18, 18, 18, 1)), url('{{ $game->screenshots()->first()->url }}') no-repeat center center; background-size: cover; filter: blur(3px); z-index: -1;"></div>
|
<!-- Left Column: Cover, Rating, Add to List -->
|
||||||
@else
|
<div class="col-md-3">
|
||||||
<div class="game-hero-bg position-absolute w-100 h-100" style="background: linear-gradient(rgba(18, 18, 18, 0.9), rgba(18, 18, 18, 1)); z-index: -1;"></div>
|
<!-- Cover Image -->
|
||||||
@endif
|
<div class="game-cover position-relative mb-3">
|
||||||
|
<img src="{{ $game->cover_url }}" class="img-fluid rounded shadow-lg w-100" alt="{{ $game->name }}">
|
||||||
<div class="container py-5">
|
@if($game->rating)
|
||||||
<div class="row align-items-center">
|
<div class="position-absolute top-0 end-0 bg-dark bg-opacity-75 p-2 rounded-circle m-2 shadow">
|
||||||
<div class="col-lg-3 col-md-4 mb-4 mb-md-0">
|
<div class="d-flex align-items-center justify-content-center">
|
||||||
<div class="game-cover position-relative">
|
<span class="fw-bold text-warning">{{ number_format($game->rating, 1) }}</span>
|
||||||
<img src="{{ $game->cover_url }}" class="img-fluid rounded shadow-lg" alt="{{ $game->name }}">
|
|
||||||
@if($game->rating)
|
|
||||||
<div class="position-absolute top-0 end-0 bg-dark bg-opacity-75 p-2 rounded-circle m-2 shadow">
|
|
||||||
<div class="d-flex align-items-center justify-content-center">
|
|
||||||
<span class="fw-bold text-warning">{{ number_format($game->rating, 1) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-9 col-md-8">
|
|
||||||
<h1 class="display-4 fw-bold mb-2">{{ $game->name }}</h1>
|
|
||||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
|
||||||
@foreach($game->genres()->get() as $genre)
|
|
||||||
<span class="badge bg-primary">{{ $genre->name }}</span>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
|
||||||
@foreach($game->platforms()->get() as $platform)
|
|
||||||
<span class="badge bg-secondary">{{ $platform->name }}</span>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
<p class="lead mb-4">{{ Str::limit($game->summary, 200) }}</p>
|
|
||||||
<div class="d-flex flex-wrap gap-3 align-items-center">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<i class="far fa-calendar-alt me-2"></i>
|
|
||||||
<span>{{ $game->first_release_date->format('F j, Y') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rating Section -->
|
||||||
|
<div class="card bg-dark text-white mb-3">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Rating</h5>
|
||||||
@if($game->rating)
|
@if($game->rating)
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex justify-content-center">
|
||||||
<div class="rating-stars d-flex gap-1">
|
<div class="rating-stars d-flex gap-1">
|
||||||
@for($i = 1; $i <= 5; $i++)
|
@for($i = 1; $i <= 5; $i++)
|
||||||
<i class="fas fa-star {{ $i <= ($game->rating/20) ? 'text-warning' : 'text-secondary' }}"></i>
|
<i class="fas fa-star {{ $i <= ($game->rating/20) ? 'text-warning' : 'text-secondary' }}"></i>
|
||||||
@@ -52,240 +33,214 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="ms-2">({{ number_format($game->rating_count) }} ratings)</span>
|
<span class="ms-2">({{ number_format($game->rating_count) }} ratings)</span>
|
||||||
</div>
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-muted">No ratings yet</p>
|
||||||
@endif
|
@endif
|
||||||
@auth
|
</div>
|
||||||
<div class="dropdown">
|
</div>
|
||||||
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="addToListDropdown"
|
|
||||||
data-bs-toggle="dropdown" aria-expanded="false">
|
<!-- Add to List Button -->
|
||||||
<i class="fas fa-plus me-2"></i>Add to List
|
@auth
|
||||||
</button>
|
<div class="dropdown mb-3 w-100">
|
||||||
<ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="addToListDropdown">
|
<button class="btn btn-outline-primary dropdown-toggle w-100" type="button" id="addToListDropdown"
|
||||||
@foreach(Auth::user()->lists as $list)
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<li>
|
<i class="fas fa-plus me-2"></i>Add to List
|
||||||
<form action="{{ route('lists.addGame', $list) }}" method="POST" class="d-inline">
|
</button>
|
||||||
@csrf
|
<ul class="dropdown-menu dropdown-menu-dark w-100" aria-labelledby="addToListDropdown">
|
||||||
<input type="hidden" name="game_id" value="{{ $game->id }}">
|
@foreach(Auth::user()->lists as $list)
|
||||||
<button type="submit" class="dropdown-item">
|
<li>
|
||||||
<i class="fas fa-list me-2"></i>{{ $list->name }}
|
<form action="{{ route('lists.addGame', $list) }}" method="POST" class="d-inline">
|
||||||
</button>
|
@csrf
|
||||||
</form>
|
<input type="hidden" name="game_id" value="{{ $game->id }}">
|
||||||
</li>
|
<button type="submit" class="dropdown-item">
|
||||||
@endforeach
|
<i class="fas fa-list me-2"></i>{{ $list->name }}
|
||||||
<li><hr class="dropdown-divider"></li>
|
</button>
|
||||||
<li>
|
</form>
|
||||||
<a class="dropdown-item" href="{{ route('lists.create') }}">
|
</li>
|
||||||
<i class="fas fa-plus me-2"></i>Create New List
|
@endforeach
|
||||||
</a>
|
<li><hr class="dropdown-divider"></li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<a class="dropdown-item" href="{{ route('lists.create') }}">
|
||||||
</div>
|
<i class="fas fa-plus me-2"></i>Create New List
|
||||||
@endauth
|
</a>
|
||||||
@auth
|
</li>
|
||||||
<a href="{{ route('games.reviews.create', $game) }}" class="btn btn-gradient">
|
</ul>
|
||||||
<i class="fas fa-star me-2"></i>Write a Review
|
</div>
|
||||||
</a>
|
@endauth
|
||||||
@endauth
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle Column: Title, Release Date, Description -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<!-- Title and Release Date -->
|
||||||
|
<h1 class="fw-bold mb-2">{{ $game->name }}</h1>
|
||||||
|
<p class="text-muted mb-3">
|
||||||
|
<i class="far fa-calendar-alt me-2"></i> Released on {{ $game->first_release_date->format('F j, Y') }}
|
||||||
|
@if(isset($game->company))
|
||||||
|
by {{ $game->company }}
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="card bg-dark text-white mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-3">Description</h5>
|
||||||
|
<p class="card-text">{{ $game->summary }}</p>
|
||||||
|
@if($game->storyline)
|
||||||
|
<h6 class="mt-4 mb-2">Storyline</h6>
|
||||||
|
<p class="card-text">{{ $game->storyline }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column: Platforms, Genres -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<!-- Platforms -->
|
||||||
|
<div class="card bg-dark text-white mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-3">Platforms</h5>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
@foreach($game->platforms()->get() as $platform)
|
||||||
|
<span class="badge bg-secondary">{{ $platform->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Genres -->
|
||||||
|
<div class="card bg-dark text-white mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-3">Genres</h5>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
@foreach($game->genres()->get() as $genre)
|
||||||
|
<span class="badge bg-primary">{{ $genre->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
<!-- Bottom Section -->
|
||||||
<!-- Tabs Navigation -->
|
<div class="row mb-4">
|
||||||
<ul class="nav nav-tabs mb-4" id="gameDetailsTabs" role="tablist">
|
<!-- Left Column: Stats -->
|
||||||
<li class="nav-item" role="presentation">
|
<div class="col-md-4">
|
||||||
<button class="nav-link active" id="about-tab" data-bs-toggle="tab" data-bs-target="#about" type="button" role="tab" aria-controls="about" aria-selected="true">
|
<div class="card bg-dark text-white mb-4">
|
||||||
<i class="fas fa-info-circle me-2"></i>About
|
<div class="card-header">
|
||||||
</button>
|
<h5 class="mb-0">Stats</h5>
|
||||||
</li>
|
</div>
|
||||||
@if($game->screenshots()->count())
|
<div class="card-body">
|
||||||
<li class="nav-item" role="presentation">
|
<ul class="list-group list-group-flush bg-transparent">
|
||||||
<button class="nav-link" id="media-tab" data-bs-toggle="tab" data-bs-target="#media" type="button" role="tab" aria-controls="media" aria-selected="false">
|
<li class="list-group-item bg-dark text-white border-secondary">
|
||||||
<i class="fas fa-images me-2"></i>Media
|
<div class="d-flex justify-content-between">
|
||||||
</button>
|
<span><i class="far fa-calendar-alt me-2"></i>Release Date</span>
|
||||||
</li>
|
<span class="text-end">{{ $game->first_release_date->format('F j, Y') }}</span>
|
||||||
@endif
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link" id="reviews-tab" data-bs-toggle="tab" data-bs-target="#reviews" type="button" role="tab" aria-controls="reviews" aria-selected="false">
|
|
||||||
<i class="fas fa-comments me-2"></i>Reviews
|
|
||||||
@if($game->reviews->count() > 0)
|
|
||||||
<span class="badge bg-primary ms-1">{{ $game->reviews->count() }}</span>
|
|
||||||
@endif
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
@if($game->similar_games->count() > 0)
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link" id="similar-tab" data-bs-toggle="tab" data-bs-target="#similar" type="button" role="tab" aria-controls="similar" aria-selected="false">
|
|
||||||
<i class="fas fa-gamepad me-2"></i>Similar Games
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
@endif
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- Tabs Content -->
|
|
||||||
<div class="tab-content" id="gameDetailsTabsContent">
|
|
||||||
<!-- About Tab -->
|
|
||||||
<div class="tab-pane fade show active" id="about" role="tabpanel" aria-labelledby="about-tab">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="card bg-dark text-white mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Summary</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="card-text">{{ $game->summary }}</p>
|
|
||||||
@if($game->storyline)
|
|
||||||
<h6 class="mt-4 mb-2">Storyline</h6>
|
|
||||||
<p class="card-text">{{ $game->storyline }}</p>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if($game->dlcs->count() > 0)
|
|
||||||
<div class="card bg-dark text-white mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">DLCs & Expansions</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
@foreach($game->dlcs as $dlc)
|
|
||||||
<div class="col-md-6 col-lg-4">
|
|
||||||
<div class="card bg-dark h-100">
|
|
||||||
<img src="{{ $dlc->cover_url }}" class="card-img-top" alt="{{ $dlc->name }}">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title">{{ $dlc->name }}</h6>
|
|
||||||
<a href="{{ route('games.show', $dlc) }}" class="btn btn-sm btn-outline-primary mt-2">View Details</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
|
@if($game->rating)
|
||||||
|
<li class="list-group-item bg-dark text-white border-secondary">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span><i class="fas fa-star me-2"></i>Rating</span>
|
||||||
|
<span class="text-end">{{ number_format($game->rating, 1) }}/100 ({{ number_format($game->rating_count) }} ratings)</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
@if($game->url)
|
||||||
|
<li class="list-group-item bg-dark text-white border-secondary">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span><i class="fas fa-external-link-alt me-2"></i>Website</span>
|
||||||
|
<a href="{{ $game->url }}" target="_blank" class="text-primary">Visit</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@if($reviews->isNotEmpty())
|
||||||
|
<div class="mt-4">
|
||||||
|
<h6 class="mb-3">Rating Distribution</h6>
|
||||||
|
@foreach($ratingDistribution as $rating)
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<div class="rating-label me-2" style="width: 30px;">{{ $rating['range'] }}</div>
|
||||||
|
<div class="progress flex-grow-1" style="height: 10px;">
|
||||||
|
<div class="progress-bar bg-warning" role="progressbar" style="width: {{ $rating['percentage'] }}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="ms-2" style="width: 40px;">{{ $rating['count'] }}</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div class="card bg-dark text-white mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Game Details</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<ul class="list-group list-group-flush bg-transparent">
|
|
||||||
<li class="list-group-item bg-dark text-white border-secondary">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<span><i class="far fa-calendar-alt me-2"></i>Release Date</span>
|
|
||||||
<span class="text-end">{{ $game->first_release_date->format('F j, Y') }}</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
@if($game->rating)
|
|
||||||
<li class="list-group-item bg-dark text-white border-secondary">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<span><i class="fas fa-star me-2"></i>Rating</span>
|
|
||||||
<span class="text-end">{{ number_format($game->rating, 1) }}/100 ({{ number_format($game->rating_count) }} ratings)</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
@endif
|
|
||||||
<li class="list-group-item bg-dark text-white border-secondary">
|
|
||||||
<div>
|
|
||||||
<span><i class="fas fa-tags me-2"></i>Genres</span>
|
|
||||||
<div class="d-flex flex-wrap gap-1 mt-2">
|
|
||||||
@foreach($game->genres()->get() as $genre)
|
|
||||||
<span class="badge bg-primary">{{ $genre->name }}</span>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="list-group-item bg-dark text-white border-secondary">
|
|
||||||
<div>
|
|
||||||
<span><i class="fas fa-desktop me-2"></i>Platforms</span>
|
|
||||||
<div class="d-flex flex-wrap gap-1 mt-2">
|
|
||||||
@foreach($game->platforms()->get() as $platform)
|
|
||||||
<span class="badge bg-secondary">{{ $platform->name }}</span>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
@if($game->url)
|
|
||||||
<li class="list-group-item bg-dark text-white border-secondary">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<span><i class="fas fa-external-link-alt me-2"></i>Website</span>
|
|
||||||
<a href="{{ $game->url }}" target="_blank" class="text-primary">Visit</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
@endif
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Media Tab -->
|
<!-- Right Column: Games on Series (Similar Games) -->
|
||||||
@if($game->screenshots()->count() || $game->videos()->count())
|
<div class="col-md-8">
|
||||||
<div class="tab-pane fade" id="media" role="tabpanel" aria-labelledby="media-tab">
|
@if($game->similar_games->count() > 0)
|
||||||
@if($game->screenshots()->count())
|
|
||||||
<div class="card bg-dark text-white mb-4">
|
<div class="card bg-dark text-white mb-4">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">Screenshots</h5>
|
<h5 class="mb-0">Games on Series</h5>
|
||||||
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="collapse" data-bs-target="#screenshotsCollapse">
|
|
||||||
<i class="fas fa-chevron-down"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse show" id="screenshotsCollapse">
|
<div class="card-body">
|
||||||
<div class="card-body">
|
<div class="row g-3">
|
||||||
<div class="row g-3">
|
@foreach($game->similar_games as $similar)
|
||||||
@foreach($game->screenshots()->get() as $screenshot)
|
<div class="col-md-6 col-lg-4">
|
||||||
<div class="col-md-6 col-lg-4">
|
<div class="card bg-dark h-100 hover-scale">
|
||||||
<a href="{{ $screenshot->url }}" data-lightbox="game-screenshots" data-title="{{ $game->name }} Screenshot">
|
<a href="{{ route('games.show', $similar) }}" class="text-decoration-none">
|
||||||
<img src="{{ $screenshot->url }}" class="img-fluid rounded shadow-sm hover-scale" alt="Game Screenshot">
|
<img src="{{ $similar->cover_url }}" class="card-img-top" alt="{{ $similar->name }}">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title text-white">{{ $similar->name }}</h6>
|
||||||
|
<p class="card-text text-muted small">{{ $similar->first_release_date->format('Y') }}</p>
|
||||||
|
@if($similar->rating)
|
||||||
|
<div class="d-flex align-items-center mt-2">
|
||||||
|
<div class="rating-stars d-flex gap-1">
|
||||||
|
@for($i = 1; $i <= 5; $i++)
|
||||||
|
<i class="fas fa-star {{ $i <= ($similar->rating/20) ? 'text-warning' : 'text-secondary' }}"></i>
|
||||||
|
@endfor
|
||||||
|
</div>
|
||||||
|
<span class="ms-2 small">{{ number_format($similar->rating, 1) }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
</div>
|
||||||
</div>
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($game->videos()->count())
|
@if($game->dlcs->count() > 0)
|
||||||
<div class="card bg-dark text-white mb-4">
|
<div class="card bg-dark text-white mb-4">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">Videos</h5>
|
<h5 class="mb-0">DLCs & Expansions</h5>
|
||||||
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="collapse" data-bs-target="#videosCollapse">
|
|
||||||
<i class="fas fa-chevron-down"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse show" id="videosCollapse">
|
<div class="card-body">
|
||||||
<div class="card-body">
|
<div class="row g-3">
|
||||||
<div class="row g-3">
|
@foreach($game->dlcs as $dlc)
|
||||||
@foreach($game->videos()->get() as $video)
|
<div class="col-md-6 col-lg-4">
|
||||||
<div class="col-md-6">
|
<div class="card bg-dark h-100">
|
||||||
<div class="card bg-dark border-secondary">
|
<img src="{{ $dlc->cover_url }}" class="card-img-top" alt="{{ $dlc->name }}">
|
||||||
<div class="ratio ratio-16x9">
|
<div class="card-body">
|
||||||
<iframe src="https://www.youtube.com/embed/{{ $video->video_id }}"
|
<h6 class="card-title">{{ $dlc->name }}</h6>
|
||||||
title="{{ $video->name }}" allowfullscreen></iframe>
|
<a href="{{ route('games.show', $dlc) }}" class="btn btn-sm btn-outline-primary mt-2">View Details</a>
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title">{{ $video->name }}</h6>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
</div>
|
||||||
|
|
||||||
<!-- Reviews Tab -->
|
<!-- Latest Reviews Section -->
|
||||||
<div class="tab-pane fade" id="reviews" role="tabpanel" aria-labelledby="reviews-tab">
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
<div class="card bg-dark text-white mb-4">
|
<div class="card bg-dark text-white mb-4">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">User Reviews</h5>
|
<h5 class="mb-0">Latest Reviews</h5>
|
||||||
@auth
|
@auth
|
||||||
<a href="{{ route('games.reviews.create', $game) }}" class="btn btn-gradient btn-sm">
|
<a href="{{ route('games.reviews.create', $game) }}" class="btn btn-gradient btn-sm">
|
||||||
<i class="fas fa-plus me-2"></i>Write a Review
|
<i class="fas fa-plus me-2"></i>Write a Review
|
||||||
@@ -293,7 +248,7 @@
|
|||||||
@endauth
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@if($game->reviews->isEmpty())
|
@if($reviews->isEmpty())
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
|
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
|
||||||
<p class="text-muted mb-3">No reviews yet. Be the first to review this game!</p>
|
<p class="text-muted mb-3">No reviews yet. Be the first to review this game!</p>
|
||||||
@@ -308,56 +263,8 @@
|
|||||||
@endauth
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card bg-dark border-secondary h-100">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<h2 class="display-4 fw-bold text-warning mb-0">
|
|
||||||
{{ number_format($game->reviews->avg('rating'), 1) }}
|
|
||||||
</h2>
|
|
||||||
<div class="rating-stars d-flex gap-1 justify-content-center my-2">
|
|
||||||
@for($i = 1; $i <= 5; $i++)
|
|
||||||
<i class="fas fa-star {{ $i <= ($game->reviews->avg('rating')/2) ? 'text-warning' : 'text-secondary' }}"></i>
|
|
||||||
@endfor
|
|
||||||
</div>
|
|
||||||
<p class="text-muted">Based on {{ $game->reviews->count() }} reviews</p>
|
|
||||||
<div class="d-flex justify-content-center gap-3">
|
|
||||||
<div class="text-center">
|
|
||||||
<span class="d-block fw-bold text-success">{{ $game->reviews->where('recommended', true)->count() }}</span>
|
|
||||||
<small class="text-muted">Recommended</small>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<span class="d-block fw-bold text-danger">{{ $game->reviews->where('recommended', false)->count() }}</span>
|
|
||||||
<small class="text-muted">Not Recommended</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="card bg-dark border-secondary h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="mb-3">Rating Distribution</h6>
|
|
||||||
@for($i = 10; $i >= 1; $i--)
|
|
||||||
@php
|
|
||||||
$count = $game->reviews->where('rating', '>=', ($i-1)*2)->where('rating', '<', $i*2)->count();
|
|
||||||
$percentage = $game->reviews->count() > 0 ? ($count / $game->reviews->count()) * 100 : 0;
|
|
||||||
@endphp
|
|
||||||
<div class="d-flex align-items-center mb-2">
|
|
||||||
<div class="rating-label me-2" style="width: 30px;">{{ $i*2-1 }}-{{ $i*2 }}</div>
|
|
||||||
<div class="progress flex-grow-1" style="height: 10px;">
|
|
||||||
<div class="progress-bar bg-warning" role="progressbar" style="width: {{ $percentage }}%"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ms-2" style="width: 40px;">{{ $count }}</div>
|
|
||||||
</div>
|
|
||||||
@endfor
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="reviews-list">
|
<div class="reviews-list">
|
||||||
@foreach($game->reviews as $review)
|
@foreach($reviews->take(3) as $review)
|
||||||
<div class="card bg-dark border-secondary mb-4">
|
<div class="card bg-dark border-secondary mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
@@ -377,13 +284,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="rating-badge me-2 px-2 py-1 rounded" style="background-color: {{ $review->rating >= 15 ? '#28a745' : ($review->rating >= 10 ? '#ffc107' : '#dc3545') }};">
|
<div class="rating-badge me-2 px-2 py-1 rounded" style="background-color: {{ $review->rating >= 7 ? '#28a745' : ($review->rating >= 5 ? '#ffc107' : '#dc3545') }};">
|
||||||
<span class="fw-bold">{{ $review->rating }}/20</span>
|
<span class="fw-bold">{{ $review->rating }}/10</span>
|
||||||
</div>
|
</div>
|
||||||
@if($review->recommended)
|
@if($review->recommended)
|
||||||
<span class="badge bg-success">Recommended</span>
|
<span class="badge bg-success">Recommended</span>
|
||||||
@else
|
|
||||||
<span class="badge bg-danger">Not Recommended</span>
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -419,59 +324,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
|
@if($reviews->count() > 3)
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="{{ route('games.reviews.index', $game) }}" class="btn btn-outline-primary">
|
||||||
|
View All {{ $reviews->count() }} Reviews
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Similar Games Tab -->
|
|
||||||
@if($game->similar_games->count() > 0)
|
|
||||||
<div class="tab-pane fade" id="similar" role="tabpanel" aria-labelledby="similar-tab">
|
|
||||||
<div class="card bg-dark text-white">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Similar Games You Might Like</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-4">
|
|
||||||
@foreach($game->similar_games as $similar)
|
|
||||||
<div class="col-md-6 col-lg-3">
|
|
||||||
<div class="card bg-dark h-100 hover-scale">
|
|
||||||
<a href="{{ route('games.show', $similar) }}" class="text-decoration-none">
|
|
||||||
<img src="{{ $similar->cover_url }}" class="card-img-top" alt="{{ $similar->name }}">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title text-white">{{ $similar->name }}</h6>
|
|
||||||
<p class="card-text text-muted small">{{ $similar->first_release_date->format('Y') }}</p>
|
|
||||||
@if($similar->rating)
|
|
||||||
<div class="d-flex align-items-center mt-2">
|
|
||||||
<div class="rating-stars d-flex gap-1">
|
|
||||||
@for($i = 1; $i <= 5; $i++)
|
|
||||||
<i class="fas fa-star {{ $i <= ($similar->rating/20) ? 'text-warning' : 'text-secondary' }}"></i>
|
|
||||||
@endfor
|
|
||||||
</div>
|
|
||||||
<span class="ms-2 small">{{ number_format($similar->rating, 1) }}</span>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.game-hero {
|
|
||||||
min-height: 400px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-scale {
|
.hover-scale {
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
@@ -480,29 +349,6 @@
|
|||||||
transform: scale(1.03);
|
transform: scale(1.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs {
|
|
||||||
border-bottom: 1px solid #343a40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs .nav-link {
|
|
||||||
color: #eaeaea;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs .nav-link:hover {
|
|
||||||
border-color: transparent;
|
|
||||||
border-bottom: 2px solid var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs .nav-link.active {
|
|
||||||
color: var(--accent);
|
|
||||||
background-color: transparent;
|
|
||||||
border-color: transparent;
|
|
||||||
border-bottom: 2px solid var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-gradient {
|
.btn-gradient {
|
||||||
background: linear-gradient(90deg, var(--primary), var(--accent));
|
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -161,6 +161,11 @@
|
|||||||
<i class="fas fa-gamepad me-1"></i> Games
|
<i class="fas fa-gamepad me-1"></i> Games
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {{ request()->routeIs('forum.*') ? 'active' : '' }}" href="{{ route('forum.index') }}">
|
||||||
|
<i class="fas fa-comments me-1"></i> Forum
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
@auth
|
@auth
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{ request()->routeIs('lists.*') ? 'active' : '' }}" href="{{ route('lists.index') }}">
|
<a class="nav-link {{ request()->routeIs('lists.*') ? 'active' : '' }}" href="{{ route('lists.index') }}">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="rating" class="form-label">Rating (1-20)</label>
|
<label for="rating" class="form-label">Rating (1-10)</label>
|
||||||
<div class="interactive-stars mb-2">
|
<div class="interactive-stars mb-2">
|
||||||
<div class="stars-container" id="starsContainer">
|
<div class="stars-container" id="starsContainer">
|
||||||
@for ($i = 1; $i <= 10; $i++)
|
@for ($i = 1; $i <= 10; $i++)
|
||||||
@@ -21,6 +21,21 @@
|
|||||||
@enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="platform_id" class="form-label">Platform</label>
|
||||||
|
<select name="platform_id" id="platform_id" class="form-control bg-dark text-white @error('platform_id') is-invalid @enderror" required>
|
||||||
|
<option value="">Select a platform</option>
|
||||||
|
@foreach($platforms as $platform)
|
||||||
|
<option value="{{ $platform->id }}" {{ isset($review) && $review->platform_id == $platform->id ? 'selected' : '' }}>
|
||||||
|
{{ $platform->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('platform_id')
|
||||||
|
<div class="text-danger mt-1">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="content" class="form-label">Review</label>
|
<label for="content" class="form-label">Review</label>
|
||||||
<textarea name="content" id="content" class="form-control bg-dark text-white @error('content') is-invalid @enderror" rows="10" required>{{ old('content', $review->content ?? '') }}</textarea>
|
<textarea name="content" id="content" class="form-control bg-dark text-white @error('content') is-invalid @enderror" rows="10" required>{{ old('content', $review->content ?? '') }}</textarea>
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
{{ $review->recommended ? 'Recommended' : 'Not Recommended' }}
|
{{ $review->recommended ? 'Recommended' : 'Not Recommended' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="badge bg-primary">{{ $review->rating }}/10</span>
|
<span class="badge bg-primary">{{ $review->rating }}/10</span>
|
||||||
|
@if($review->platform)
|
||||||
|
<span class="badge bg-info">{{ $review->platform->name }}</span>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-gray-400">{{ $review->created_at->diffForHumans() }}</small>
|
<small class="text-gray-400">{{ $review->created_at->diffForHumans() }}</small>
|
||||||
|
|||||||
@@ -17,9 +17,10 @@
|
|||||||
@endfor
|
@endfor
|
||||||
</div>
|
</div>
|
||||||
@if($review->recommended)
|
@if($review->recommended)
|
||||||
<span class="badge bg-success">Recommended</span>
|
<span class="badge bg-success me-2">Recommended</span>
|
||||||
@else
|
@endif
|
||||||
<span class="badge bg-danger">Not Recommended</span>
|
@if($review->platform)
|
||||||
|
<span class="badge bg-info">{{ $review->platform->name }}</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ use Illuminate\Support\Facades\Route;
|
|||||||
use App\Http\Controllers\GameController;
|
use App\Http\Controllers\GameController;
|
||||||
use App\Http\Controllers\ReviewController;
|
use App\Http\Controllers\ReviewController;
|
||||||
use App\Http\Controllers\GameListController;
|
use App\Http\Controllers\GameListController;
|
||||||
|
use App\Http\Controllers\ForumCategoryController;
|
||||||
|
use App\Http\Controllers\ForumThreadController;
|
||||||
|
use App\Http\Controllers\ForumTopicController;
|
||||||
|
use App\Http\Controllers\ForumPostController;
|
||||||
|
|
||||||
//Route::get('/', function () {
|
//Route::get('/', function () {
|
||||||
// return view('landing');
|
// return view('landing');
|
||||||
@@ -50,4 +54,44 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::delete('lists/{list}/games/{game}', [GameListController::class, 'removeGame'])->name('lists.removeGame');
|
Route::delete('lists/{list}/games/{game}', [GameListController::class, 'removeGame'])->name('lists.removeGame');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Forum Routes
|
||||||
|
Route::prefix('forum')->name('forum.')->group(function () {
|
||||||
|
// Public forum routes
|
||||||
|
Route::get('/', [ForumCategoryController::class, 'index'])->name('index');
|
||||||
|
Route::get('/categories/{category}', [ForumCategoryController::class, 'show'])->name('categories.show');
|
||||||
|
Route::get('/threads/{thread}', [ForumThreadController::class, 'show'])->name('threads.show');
|
||||||
|
Route::get('/topics/{topic}', [ForumTopicController::class, 'show'])->name('topics.show');
|
||||||
|
|
||||||
|
// Authenticated forum routes
|
||||||
|
Route::middleware('auth')->group(function () {
|
||||||
|
// Categories (admin only in a real app)
|
||||||
|
Route::get('/categories/create', [ForumCategoryController::class, 'create'])->name('categories.create');
|
||||||
|
Route::post('/categories', [ForumCategoryController::class, 'store'])->name('categories.store');
|
||||||
|
Route::get('/categories/{category}/edit', [ForumCategoryController::class, 'edit'])->name('categories.edit');
|
||||||
|
Route::put('/categories/{category}', [ForumCategoryController::class, 'update'])->name('categories.update');
|
||||||
|
Route::delete('/categories/{category}', [ForumCategoryController::class, 'destroy'])->name('categories.destroy');
|
||||||
|
|
||||||
|
// Threads
|
||||||
|
Route::get('/threads/create', [ForumThreadController::class, 'create'])->name('threads.create');
|
||||||
|
Route::post('/threads', [ForumThreadController::class, 'store'])->name('threads.store');
|
||||||
|
Route::get('/threads/{thread}/edit', [ForumThreadController::class, 'edit'])->name('threads.edit');
|
||||||
|
Route::put('/threads/{thread}', [ForumThreadController::class, 'update'])->name('threads.update');
|
||||||
|
Route::delete('/threads/{thread}', [ForumThreadController::class, 'destroy'])->name('threads.destroy');
|
||||||
|
|
||||||
|
// Topics
|
||||||
|
Route::get('/topics/create', [ForumTopicController::class, 'create'])->name('topics.create');
|
||||||
|
Route::post('/topics', [ForumTopicController::class, 'store'])->name('topics.store');
|
||||||
|
Route::get('/topics/{topic}/edit', [ForumTopicController::class, 'edit'])->name('topics.edit');
|
||||||
|
Route::put('/topics/{topic}', [ForumTopicController::class, 'update'])->name('topics.update');
|
||||||
|
Route::delete('/topics/{topic}', [ForumTopicController::class, 'destroy'])->name('topics.destroy');
|
||||||
|
|
||||||
|
// Posts
|
||||||
|
Route::get('/posts/create', [ForumPostController::class, 'create'])->name('posts.create');
|
||||||
|
Route::post('/posts', [ForumPostController::class, 'store'])->name('posts.store');
|
||||||
|
Route::get('/posts/{post}/edit', [ForumPostController::class, 'edit'])->name('posts.edit');
|
||||||
|
Route::put('/posts/{post}', [ForumPostController::class, 'update'])->name('posts.update');
|
||||||
|
Route::delete('/posts/{post}', [ForumPostController::class, 'destroy'])->name('posts.destroy');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
Reference in New Issue
Block a user