mirror of
https://github.com/m1ngsama/php.git
synced 2025-12-24 16:01:19 +00:00
Compare commits
11 commits
488384740b
...
c5f77c9ad8
| Author | SHA1 | Date | |
|---|---|---|---|
| c5f77c9ad8 | |||
| 8a17eb65c0 | |||
| 7a9fd58601 | |||
| 2e3a97c09d | |||
| d0f10e0590 | |||
| 4efa2b09e3 | |||
| b081d23f88 | |||
| a158e64985 | |||
| 21ace0f38e | |||
| b453c1020a | |||
| 34f73d3207 |
32 changed files with 1624 additions and 12 deletions
49
README_FORUM.md
Normal file
49
README_FORUM.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Reddit-Like Community Forum
|
||||
|
||||
A full-featured community forum built with Laravel, inspired by Reddit.
|
||||
|
||||
## Features
|
||||
|
||||
- User authentication (registration, login, logout)
|
||||
- Community creation and management
|
||||
- Post creation with multiple types (text, link, image)
|
||||
- Nested comment threading
|
||||
- Upvote/downvote system for posts and comments
|
||||
- User karma tracking
|
||||
- Community subscriptions
|
||||
- User profiles with activity history
|
||||
|
||||
## Database Schema
|
||||
|
||||
- **Users**: User accounts with karma scores
|
||||
- **Communities**: Subreddit-like communities
|
||||
- **Posts**: User-submitted content
|
||||
- **Comments**: Nested comments on posts
|
||||
- **Votes**: Upvotes and downvotes
|
||||
- **Community_User**: User subscriptions to communities
|
||||
|
||||
## Routes
|
||||
|
||||
- `/` - Home page with popular posts
|
||||
- `/register` - User registration
|
||||
- `/login` - User login
|
||||
- `/communities` - Browse all communities
|
||||
- `/r/{community}` - View community posts
|
||||
- `/posts/{post}` - View post details and comments
|
||||
- `/u/{user}` - View user profile
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Laravel 12
|
||||
- Tailwind CSS
|
||||
- Blade Templates
|
||||
- MySQL/SQLite
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository
|
||||
2. Run `composer install`
|
||||
3. Copy `.env.example` to `.env`
|
||||
4. Generate application key: `php artisan key:generate`
|
||||
5. Run migrations: `php artisan migrate`
|
||||
6. Start the server: `php artisan serve`
|
||||
66
app/Http/Controllers/AuthController.php
Normal file
66
app/Http/Controllers/AuthController.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function showRegister()
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
public function register(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
'karma' => 0,
|
||||
]);
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect('/')->with('success', 'Registration successful!');
|
||||
}
|
||||
|
||||
public function showLogin()
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required',
|
||||
]);
|
||||
|
||||
if (Auth::attempt($credentials, $request->boolean('remember'))) {
|
||||
$request->session()->regenerate();
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => 'The provided credentials do not match our records.',
|
||||
])->onlyInput('email');
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
37
app/Http/Controllers/CommentController.php
Normal file
37
app/Http/Controllers/CommentController.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Comment;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
public function store(Request $request, Post $post)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'content' => 'required|string',
|
||||
'parent_id' => 'nullable|exists:comments,id',
|
||||
]);
|
||||
|
||||
$comment = Comment::create([
|
||||
'post_id' => $post->id,
|
||||
'user_id' => auth()->id(),
|
||||
'parent_id' => $validated['parent_id'] ?? null,
|
||||
'content' => $validated['content'],
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Comment posted successfully!');
|
||||
}
|
||||
|
||||
public function destroy(Comment $comment)
|
||||
{
|
||||
if ($comment->user_id !== auth()->id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$comment->delete();
|
||||
return back()->with('success', 'Comment deleted successfully!');
|
||||
}
|
||||
}
|
||||
63
app/Http/Controllers/CommunityController.php
Normal file
63
app/Http/Controllers/CommunityController.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Community;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CommunityController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$communities = Community::withCount(['posts', 'subscribers'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
return view('communities.index', compact('communities'));
|
||||
}
|
||||
|
||||
public function show(Community $community)
|
||||
{
|
||||
$posts = $community->posts()
|
||||
->with(['user', 'comments'])
|
||||
->orderBy('votes', 'desc')
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
return view('communities.show', compact('community', 'posts'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('communities.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255|unique:communities',
|
||||
'description' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$community = Community::create([
|
||||
'name' => $validated['name'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'created_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return redirect()->route('communities.show', $community)
|
||||
->with('success', 'Community created successfully!');
|
||||
}
|
||||
|
||||
public function subscribe(Community $community)
|
||||
{
|
||||
auth()->user()->subscribedCommunities()->attach($community->id);
|
||||
return back()->with('success', 'Subscribed to ' . $community->name);
|
||||
}
|
||||
|
||||
public function unsubscribe(Community $community)
|
||||
{
|
||||
auth()->user()->subscribedCommunities()->detach($community->id);
|
||||
return back()->with('success', 'Unsubscribed from ' . $community->name);
|
||||
}
|
||||
}
|
||||
19
app/Http/Controllers/HomeController.php
Normal file
19
app/Http/Controllers/HomeController.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$posts = Post::with(['user', 'community', 'comments'])
|
||||
->orderBy('votes', 'desc')
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
return view('home', compact('posts'));
|
||||
}
|
||||
}
|
||||
55
app/Http/Controllers/PostController.php
Normal file
55
app/Http/Controllers/PostController.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Community;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PostController extends Controller
|
||||
{
|
||||
public function create()
|
||||
{
|
||||
$communities = Community::orderBy('name')->get();
|
||||
return view('posts.create', compact('communities'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'community_id' => 'required|exists:communities,id',
|
||||
'title' => 'required|string|max:255',
|
||||
'content' => 'nullable|string',
|
||||
'url' => 'nullable|url',
|
||||
'type' => 'required|in:text,link,image',
|
||||
]);
|
||||
|
||||
$post = Post::create([
|
||||
'community_id' => $validated['community_id'],
|
||||
'user_id' => auth()->id(),
|
||||
'title' => $validated['title'],
|
||||
'content' => $validated['content'] ?? null,
|
||||
'url' => $validated['url'] ?? null,
|
||||
'type' => $validated['type'],
|
||||
]);
|
||||
|
||||
return redirect()->route('posts.show', $post)
|
||||
->with('success', 'Post created successfully!');
|
||||
}
|
||||
|
||||
public function show(Post $post)
|
||||
{
|
||||
$post->load(['user', 'community', 'comments.user', 'comments.replies.user']);
|
||||
return view('posts.show', compact('post'));
|
||||
}
|
||||
|
||||
public function destroy(Post $post)
|
||||
{
|
||||
if ($post->user_id !== auth()->id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$post->delete();
|
||||
return redirect('/')->with('success', 'Post deleted successfully!');
|
||||
}
|
||||
}
|
||||
24
app/Http/Controllers/UserController.php
Normal file
24
app/Http/Controllers/UserController.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function show(User $user)
|
||||
{
|
||||
$posts = $user->posts()
|
||||
->with(['community', 'comments'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
$comments = $user->comments()
|
||||
->with(['post', 'post.community'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
return view('users.show', compact('user', 'posts', 'comments'));
|
||||
}
|
||||
}
|
||||
93
app/Http/Controllers/VoteController.php
Normal file
93
app/Http/Controllers/VoteController.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Comment;
|
||||
use App\Models\Post;
|
||||
use App\Models\Vote;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VoteController extends Controller
|
||||
{
|
||||
public function votePost(Request $request, Post $post)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'vote' => 'required|in:1,-1',
|
||||
]);
|
||||
|
||||
$existingVote = Vote::where([
|
||||
'user_id' => auth()->id(),
|
||||
'voteable_id' => $post->id,
|
||||
'voteable_type' => Post::class,
|
||||
])->first();
|
||||
|
||||
if ($existingVote) {
|
||||
if ($existingVote->vote == $validated['vote']) {
|
||||
$existingVote->delete();
|
||||
$this->updateVoteCount($post);
|
||||
return back();
|
||||
}
|
||||
$existingVote->update(['vote' => $validated['vote']]);
|
||||
} else {
|
||||
Vote::create([
|
||||
'user_id' => auth()->id(),
|
||||
'voteable_id' => $post->id,
|
||||
'voteable_type' => Post::class,
|
||||
'vote' => $validated['vote'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->updateVoteCount($post);
|
||||
$this->updateUserKarma($post->user);
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function voteComment(Request $request, Comment $comment)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'vote' => 'required|in:1,-1',
|
||||
]);
|
||||
|
||||
$existingVote = Vote::where([
|
||||
'user_id' => auth()->id(),
|
||||
'voteable_id' => $comment->id,
|
||||
'voteable_type' => Comment::class,
|
||||
])->first();
|
||||
|
||||
if ($existingVote) {
|
||||
if ($existingVote->vote == $validated['vote']) {
|
||||
$existingVote->delete();
|
||||
$this->updateVoteCount($comment);
|
||||
return back();
|
||||
}
|
||||
$existingVote->update(['vote' => $validated['vote']]);
|
||||
} else {
|
||||
Vote::create([
|
||||
'user_id' => auth()->id(),
|
||||
'voteable_id' => $comment->id,
|
||||
'voteable_type' => Comment::class,
|
||||
'vote' => $validated['vote'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->updateVoteCount($comment);
|
||||
$this->updateUserKarma($comment->user);
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
private function updateVoteCount($model)
|
||||
{
|
||||
$model->votes = $model->votes()->sum('vote');
|
||||
$model->save();
|
||||
}
|
||||
|
||||
private function updateUserKarma($user)
|
||||
{
|
||||
$postKarma = $user->posts()->sum('votes');
|
||||
$commentKarma = $user->comments()->sum('votes');
|
||||
$user->karma = $postKarma + $commentKarma;
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
56
app/Models/Comment.php
Normal file
56
app/Models/Comment.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Comment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'post_id',
|
||||
'user_id',
|
||||
'parent_id',
|
||||
'content',
|
||||
'votes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'votes' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function post()
|
||||
{
|
||||
return $this->belongsTo(Post::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(Comment::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function replies()
|
||||
{
|
||||
return $this->hasMany(Comment::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function votes()
|
||||
{
|
||||
return $this->morphMany(Vote::class, 'voteable');
|
||||
}
|
||||
|
||||
public function userVote($userId)
|
||||
{
|
||||
return $this->votes()->where('user_id', $userId)->first();
|
||||
}
|
||||
}
|
||||
50
app/Models/Community.php
Normal file
50
app/Models/Community.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Community extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($community) {
|
||||
if (empty($community->slug)) {
|
||||
$community->slug = Str::slug($community->name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function subscribers()
|
||||
{
|
||||
return $this->belongsToMany(User::class)->withTimestamps();
|
||||
}
|
||||
|
||||
public function posts()
|
||||
{
|
||||
return $this->hasMany(Post::class);
|
||||
}
|
||||
|
||||
public function getRouteKeyName()
|
||||
{
|
||||
return 'slug';
|
||||
}
|
||||
}
|
||||
53
app/Models/Post.php
Normal file
53
app/Models/Post.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'community_id',
|
||||
'user_id',
|
||||
'title',
|
||||
'content',
|
||||
'url',
|
||||
'type',
|
||||
'votes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'votes' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function community()
|
||||
{
|
||||
return $this->belongsTo(Community::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function comments()
|
||||
{
|
||||
return $this->hasMany(Comment::class);
|
||||
}
|
||||
|
||||
public function votes()
|
||||
{
|
||||
return $this->morphMany(Vote::class, 'voteable');
|
||||
}
|
||||
|
||||
public function userVote($userId)
|
||||
{
|
||||
return $this->votes()->where('user_id', $userId)->first();
|
||||
}
|
||||
}
|
||||
|
|
@ -21,23 +21,14 @@ class User extends Authenticatable
|
|||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'karma',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
|
@ -45,4 +36,29 @@ protected function casts(): array
|
|||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
public function communities()
|
||||
{
|
||||
return $this->hasMany(Community::class, 'created_by');
|
||||
}
|
||||
|
||||
public function subscribedCommunities()
|
||||
{
|
||||
return $this->belongsToMany(Community::class)->withTimestamps();
|
||||
}
|
||||
|
||||
public function posts()
|
||||
{
|
||||
return $this->hasMany(Post::class);
|
||||
}
|
||||
|
||||
public function comments()
|
||||
{
|
||||
return $this->hasMany(Comment::class);
|
||||
}
|
||||
|
||||
public function votes()
|
||||
{
|
||||
return $this->hasMany(Vote::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
app/Models/Vote.php
Normal file
35
app/Models/Vote.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Vote extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'voteable_id',
|
||||
'voteable_type',
|
||||
'vote',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'vote' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function voteable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->integer('karma')->default(0)->after('email');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('karma');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('communities', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('slug')->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->foreignId('created_by')->constrained('users')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('communities');
|
||||
}
|
||||
};
|
||||
31
database/migrations/2024_01_01_000005_create_posts_table.php
Normal file
31
database/migrations/2024_01_01_000005_create_posts_table.php
Normal file
|
|
@ -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
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('community_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('title');
|
||||
$table->text('content')->nullable();
|
||||
$table->string('url')->nullable();
|
||||
$table->enum('type', ['text', 'link', 'image'])->default('text');
|
||||
$table->integer('votes')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['community_id', 'created_at']);
|
||||
$table->index(['user_id', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('posts');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('comments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('post_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('parent_id')->nullable()->constrained('comments')->onDelete('cascade');
|
||||
$table->text('content');
|
||||
$table->integer('votes')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['post_id', 'created_at']);
|
||||
$table->index(['parent_id', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('comments');
|
||||
}
|
||||
};
|
||||
26
database/migrations/2024_01_01_000007_create_votes_table.php
Normal file
26
database/migrations/2024_01_01_000007_create_votes_table.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('votes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->morphs('voteable');
|
||||
$table->tinyInteger('vote');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'voteable_id', 'voteable_type']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('votes');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('community_user', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('community_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['community_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('community_user');
|
||||
}
|
||||
};
|
||||
93
database/seeders/ForumSeeder.php
Normal file
93
database/seeders/ForumSeeder.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Comment;
|
||||
use App\Models\Community;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Models\Vote;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class ForumSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$users = User::factory(10)->create();
|
||||
|
||||
$communities = [];
|
||||
$communityNames = ['Programming', 'Gaming', 'Music', 'Movies', 'Technology'];
|
||||
|
||||
foreach ($communityNames as $name) {
|
||||
$communities[] = Community::create([
|
||||
'name' => $name,
|
||||
'description' => "A community for {$name} enthusiasts",
|
||||
'created_by' => $users->random()->id,
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->subscribedCommunities()->attach(
|
||||
collect($communities)->random(rand(1, 3))->pluck('id')
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($communities as $community) {
|
||||
$posts = [];
|
||||
for ($i = 0; $i < rand(5, 15); $i++) {
|
||||
$posts[] = Post::create([
|
||||
'community_id' => $community->id,
|
||||
'user_id' => $users->random()->id,
|
||||
'title' => fake()->sentence(),
|
||||
'content' => rand(0, 1) ? fake()->paragraphs(rand(1, 3), true) : null,
|
||||
'type' => 'text',
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($posts as $post) {
|
||||
for ($i = 0; $i < rand(0, 5); $i++) {
|
||||
$comment = Comment::create([
|
||||
'post_id' => $post->id,
|
||||
'user_id' => $users->random()->id,
|
||||
'content' => fake()->paragraph(),
|
||||
]);
|
||||
|
||||
if (rand(0, 1)) {
|
||||
Comment::create([
|
||||
'post_id' => $post->id,
|
||||
'user_id' => $users->random()->id,
|
||||
'parent_id' => $comment->id,
|
||||
'content' => fake()->paragraph(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($users->random(rand(1, 8)) as $voter) {
|
||||
Vote::create([
|
||||
'user_id' => $voter->id,
|
||||
'voteable_id' => $post->id,
|
||||
'voteable_type' => Post::class,
|
||||
'vote' => rand(0, 1) ? 1 : -1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Post::all() as $post) {
|
||||
$post->votes = $post->votes()->sum('vote');
|
||||
$post->save();
|
||||
}
|
||||
|
||||
foreach (Comment::all() as $comment) {
|
||||
$comment->votes = $comment->votes()->sum('vote');
|
||||
$comment->save();
|
||||
}
|
||||
|
||||
foreach (User::all() as $user) {
|
||||
$postKarma = $user->posts()->sum('votes');
|
||||
$commentKarma = $user->comments()->sum('votes');
|
||||
$user->karma = $postKarma + $commentKarma;
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
46
resources/views/auth/login.blade.php
Normal file
46
resources/views/auth/login.blade.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
@extends('layout')
|
||||
|
||||
@section('title', 'Login')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-md mx-auto bg-white rounded-lg shadow p-8">
|
||||
<h1 class="text-2xl font-bold mb-6">Login</h1>
|
||||
|
||||
<form action="{{ route('login') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="email" class="block text-gray-700 font-bold mb-2">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email') }}" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
@error('email')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="block text-gray-700 font-bold mb-2">Password</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
@error('password')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="remember" class="mr-2">
|
||||
<span class="text-gray-700">Remember me</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-orange-600 text-white py-2 rounded hover:bg-orange-700 font-bold">
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="mt-4 text-center text-gray-600">
|
||||
Don't have an account? <a href="{{ route('register') }}" class="text-orange-600 hover:underline">Sign up</a>
|
||||
</p>
|
||||
</div>
|
||||
@endsection
|
||||
54
resources/views/auth/register.blade.php
Normal file
54
resources/views/auth/register.blade.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
@extends('layout')
|
||||
|
||||
@section('title', 'Register')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-md mx-auto bg-white rounded-lg shadow p-8">
|
||||
<h1 class="text-2xl font-bold mb-6">Create Account</h1>
|
||||
|
||||
<form action="{{ route('register') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block text-gray-700 font-bold mb-2">Username</label>
|
||||
<input type="text" id="name" name="name" value="{{ old('name') }}" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
@error('name')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="email" class="block text-gray-700 font-bold mb-2">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email') }}" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
@error('email')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="block text-gray-700 font-bold mb-2">Password</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
@error('password')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="password_confirmation" class="block text-gray-700 font-bold mb-2">Confirm Password</label>
|
||||
<input type="password" id="password_confirmation" name="password_confirmation" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-orange-600 text-white py-2 rounded hover:bg-orange-700 font-bold">
|
||||
Sign Up
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="mt-4 text-center text-gray-600">
|
||||
Already have an account? <a href="{{ route('login') }}" class="text-orange-600 hover:underline">Login</a>
|
||||
</p>
|
||||
</div>
|
||||
@endsection
|
||||
39
resources/views/communities/create.blade.php
Normal file
39
resources/views/communities/create.blade.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
@extends('layout')
|
||||
|
||||
@section('title', 'Create Community')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow p-8">
|
||||
<h1 class="text-2xl font-bold mb-6">Create a Community</h1>
|
||||
|
||||
<form action="{{ route('communities.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block text-gray-700 font-bold mb-2">Name</label>
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-700 mr-2">r/</span>
|
||||
<input type="text" id="name" name="name" value="{{ old('name') }}" required
|
||||
class="flex-1 px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mt-1">Community names cannot be changed</p>
|
||||
@error('name')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="description" class="block text-gray-700 font-bold mb-2">Description</label>
|
||||
<textarea id="description" name="description" rows="4"
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-orange-600 text-white px-6 py-2 rounded hover:bg-orange-700 font-bold">
|
||||
Create Community
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
35
resources/views/communities/index.blade.php
Normal file
35
resources/views/communities/index.blade.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
@extends('layout')
|
||||
|
||||
@section('title', 'Communities')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-3xl font-bold">Communities</h1>
|
||||
@auth
|
||||
<a href="{{ route('communities.create') }}" class="bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700">Create Community</a>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
@forelse($communities as $community)
|
||||
<div class="bg-white rounded-lg shadow mb-4 p-6">
|
||||
<h2 class="text-2xl font-bold mb-2">
|
||||
<a href="{{ route('communities.show', $community) }}" class="hover:text-orange-600">r/{{ $community->name }}</a>
|
||||
</h2>
|
||||
@if($community->description)
|
||||
<p class="text-gray-700 mb-3">{{ $community->description }}</p>
|
||||
@endif
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ $community->posts_count }} posts • {{ $community->subscribers_count }} subscribers
|
||||
• Created {{ $community->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-600">No communities yet. Be the first to create one!</p>
|
||||
@endforelse
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $communities->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
94
resources/views/communities/show.blade.php
Normal file
94
resources/views/communities/show.blade.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
@extends('layout')
|
||||
|
||||
@section('title', 'r/' . $community->name)
|
||||
|
||||
@section('content')
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-2">
|
||||
<h1 class="text-3xl font-bold mb-6">r/{{ $community->name }}</h1>
|
||||
|
||||
@forelse($posts as $post)
|
||||
<div class="bg-white rounded-lg shadow mb-4 p-4 flex">
|
||||
<div class="flex flex-col items-center mr-4">
|
||||
@auth
|
||||
<form action="{{ route('posts.vote', $post) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="vote" value="1">
|
||||
<button type="submit" class="text-gray-400 hover:text-orange-600">▲</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="text-gray-400">▲</span>
|
||||
@endauth
|
||||
|
||||
<span class="font-bold {{ $post->votes > 0 ? 'text-orange-600' : ($post->votes < 0 ? 'text-blue-600' : 'text-gray-600') }}">
|
||||
{{ $post->votes }}
|
||||
</span>
|
||||
|
||||
@auth
|
||||
<form action="{{ route('posts.vote', $post) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="vote" value="-1">
|
||||
<button type="submit" class="text-gray-400 hover:text-blue-600">▼</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="text-gray-400">▼</span>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<h2 class="text-xl font-semibold mb-2">
|
||||
<a href="{{ route('posts.show', $post) }}" class="hover:text-orange-600">{{ $post->title }}</a>
|
||||
</h2>
|
||||
<div class="text-sm text-gray-600 mb-2">
|
||||
Posted by <a href="{{ route('users.show', $post->user) }}" class="hover:underline">u/{{ $post->user->name }}</a>
|
||||
• {{ $post->created_at->diffForHumans() }}
|
||||
</div>
|
||||
@if($post->content)
|
||||
<p class="text-gray-700 mb-2">{{ Str::limit($post->content, 200) }}</p>
|
||||
@endif
|
||||
@if($post->url)
|
||||
<a href="{{ $post->url }}" target="_blank" class="text-blue-600 hover:underline">{{ $post->url }}</a>
|
||||
@endif
|
||||
<div class="mt-2">
|
||||
<a href="{{ route('posts.show', $post) }}" class="text-gray-600 hover:underline">
|
||||
{{ $post->comments->count() }} comments
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-600">No posts yet. Be the first to post!</p>
|
||||
@endforelse
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $posts->links() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<h2 class="text-xl font-bold mb-4">About r/{{ $community->name }}</h2>
|
||||
@if($community->description)
|
||||
<p class="text-gray-700 mb-4">{{ $community->description }}</p>
|
||||
@endif
|
||||
<div class="text-sm text-gray-600 mb-4">
|
||||
Created by <a href="{{ route('users.show', $community->creator) }}" class="hover:underline">u/{{ $community->creator->name }}</a>
|
||||
• {{ $community->created_at->diffForHumans() }}
|
||||
</div>
|
||||
@auth
|
||||
@if(auth()->user()->subscribedCommunities->contains($community))
|
||||
<form action="{{ route('communities.unsubscribe', $community) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="w-full bg-gray-200 text-gray-800 px-4 py-2 rounded hover:bg-gray-300">Unsubscribe</button>
|
||||
</form>
|
||||
@else
|
||||
<form action="{{ route('communities.subscribe', $community) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="w-full bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700">Subscribe</button>
|
||||
</form>
|
||||
@endif
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
81
resources/views/home.blade.php
Normal file
81
resources/views/home.blade.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
@extends('layout')
|
||||
|
||||
@section('title', 'Home')
|
||||
|
||||
@section('content')
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-2">
|
||||
<h1 class="text-3xl font-bold mb-6">Popular Posts</h1>
|
||||
|
||||
@forelse($posts as $post)
|
||||
<div class="bg-white rounded-lg shadow mb-4 p-4 flex">
|
||||
<div class="flex flex-col items-center mr-4">
|
||||
@auth
|
||||
<form action="{{ route('posts.vote', $post) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="vote" value="1">
|
||||
<button type="submit" class="text-gray-400 hover:text-orange-600">▲</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="text-gray-400">▲</span>
|
||||
@endauth
|
||||
|
||||
<span class="font-bold {{ $post->votes > 0 ? 'text-orange-600' : ($post->votes < 0 ? 'text-blue-600' : 'text-gray-600') }}">
|
||||
{{ $post->votes }}
|
||||
</span>
|
||||
|
||||
@auth
|
||||
<form action="{{ route('posts.vote', $post) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="vote" value="-1">
|
||||
<button type="submit" class="text-gray-400 hover:text-blue-600">▼</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="text-gray-400">▼</span>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<h2 class="text-xl font-semibold mb-2">
|
||||
<a href="{{ route('posts.show', $post) }}" class="hover:text-orange-600">{{ $post->title }}</a>
|
||||
</h2>
|
||||
<div class="text-sm text-gray-600 mb-2">
|
||||
<a href="{{ route('communities.show', $post->community) }}" class="font-bold hover:underline">r/{{ $post->community->name }}</a>
|
||||
• Posted by <a href="{{ route('users.show', $post->user) }}" class="hover:underline">u/{{ $post->user->name }}</a>
|
||||
• {{ $post->created_at->diffForHumans() }}
|
||||
</div>
|
||||
@if($post->content)
|
||||
<p class="text-gray-700 mb-2">{{ Str::limit($post->content, 200) }}</p>
|
||||
@endif
|
||||
@if($post->url)
|
||||
<a href="{{ $post->url }}" target="_blank" class="text-blue-600 hover:underline">{{ $post->url }}</a>
|
||||
@endif
|
||||
<div class="mt-2">
|
||||
<a href="{{ route('posts.show', $post) }}" class="text-gray-600 hover:underline">
|
||||
{{ $post->comments->count() }} comments
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-600">No posts yet. Be the first to create one!</p>
|
||||
@endforelse
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $posts->links() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<h2 class="text-xl font-bold mb-4">About</h2>
|
||||
<p class="text-gray-700 mb-4">Welcome to our Reddit-like community forum! Share your thoughts, engage in discussions, and connect with others.</p>
|
||||
@auth
|
||||
<a href="{{ route('posts.create') }}" class="block w-full text-center bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700">Create Post</a>
|
||||
@else
|
||||
<a href="{{ route('register') }}" class="block w-full text-center bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700">Sign Up</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
48
resources/views/layout.blade.php
Normal file
48
resources/views/layout.blade.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('title', 'Reddit Clone')</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<nav class="bg-white shadow-lg">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center space-x-8">
|
||||
<a href="{{ route('home') }}" class="text-2xl font-bold text-orange-600">Reddit Clone</a>
|
||||
<a href="{{ route('home') }}" class="text-gray-700 hover:text-gray-900">Home</a>
|
||||
<a href="{{ route('communities.index') }}" class="text-gray-700 hover:text-gray-900">Communities</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
@auth
|
||||
<a href="{{ route('posts.create') }}" class="bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700">Create Post</a>
|
||||
<a href="{{ route('communities.create') }}" class="text-gray-700 hover:text-gray-900">Create Community</a>
|
||||
<a href="{{ route('users.show', auth()->user()) }}" class="text-gray-700 hover:text-gray-900">{{ auth()->user()->name }} ({{ auth()->user()->karma }})</a>
|
||||
<form action="{{ route('logout') }}" method="POST" class="inline">
|
||||
@csrf
|
||||
<button type="submit" class="text-gray-700 hover:text-gray-900">Logout</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="text-gray-700 hover:text-gray-900">Login</a>
|
||||
<a href="{{ route('register') }}" class="bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700">Sign Up</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
@yield('content')
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
80
resources/views/partials/comment.blade.php
Normal file
80
resources/views/partials/comment.blade.php
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<div class="mb-4" style="margin-left: {{ $depth * 20 }}px;">
|
||||
<div class="border-l-2 border-gray-200 pl-4">
|
||||
<div class="flex items-start mb-2">
|
||||
<div class="flex flex-col items-center mr-2">
|
||||
@auth
|
||||
<form action="{{ route('comments.vote', $comment) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="vote" value="1">
|
||||
<button type="submit" class="text-gray-400 hover:text-orange-600 text-xs">▲</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="text-gray-400 text-xs">▲</span>
|
||||
@endauth
|
||||
|
||||
<span class="text-xs font-bold {{ $comment->votes > 0 ? 'text-orange-600' : ($comment->votes < 0 ? 'text-blue-600' : 'text-gray-600') }}">
|
||||
{{ $comment->votes }}
|
||||
</span>
|
||||
|
||||
@auth
|
||||
<form action="{{ route('comments.vote', $comment) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="vote" value="-1">
|
||||
<button type="submit" class="text-gray-400 hover:text-blue-600 text-xs">▼</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="text-gray-400 text-xs">▼</span>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="text-sm text-gray-600 mb-1">
|
||||
<a href="{{ route('users.show', $comment->user) }}" class="font-bold hover:underline">u/{{ $comment->user->name }}</a>
|
||||
• {{ $comment->created_at->diffForHumans() }}
|
||||
</div>
|
||||
<p class="text-gray-800 whitespace-pre-wrap">{{ $comment->content }}</p>
|
||||
|
||||
@auth
|
||||
<div class="mt-2 text-sm">
|
||||
<button onclick="toggleReplyForm({{ $comment->id }})" class="text-gray-600 hover:underline">Reply</button>
|
||||
@if($comment->user_id === auth()->id())
|
||||
<form action="{{ route('comments.destroy', $comment) }}" method="POST" class="inline ml-2">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:underline" onclick="return confirm('Are you sure?')">Delete</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div id="reply-form-{{ $comment->id }}" class="hidden mt-2">
|
||||
<form action="{{ route('comments.store', $comment->post) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="parent_id" value="{{ $comment->id }}">
|
||||
<textarea name="content" rows="3" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500"></textarea>
|
||||
<button type="submit" class="mt-2 bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700 text-sm">
|
||||
Reply
|
||||
</button>
|
||||
<button type="button" onclick="toggleReplyForm({{ $comment->id }})" class="mt-2 text-gray-600 hover:underline text-sm ml-2">
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@foreach($comment->replies as $reply)
|
||||
@include('partials.comment', ['comment' => $reply, 'depth' => $depth + 1])
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($depth === 0)
|
||||
<script>
|
||||
function toggleReplyForm(commentId) {
|
||||
const form = document.getElementById('reply-form-' + commentId);
|
||||
form.classList.toggle('hidden');
|
||||
}
|
||||
</script>
|
||||
@endif
|
||||
88
resources/views/posts/create.blade.php
Normal file
88
resources/views/posts/create.blade.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
@extends('layout')
|
||||
|
||||
@section('title', 'Create Post')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow p-8">
|
||||
<h1 class="text-2xl font-bold mb-6">Create a Post</h1>
|
||||
|
||||
<form action="{{ route('posts.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="community_id" class="block text-gray-700 font-bold mb-2">Community</label>
|
||||
<select id="community_id" name="community_id" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
<option value="">Select a community</option>
|
||||
@foreach($communities as $community)
|
||||
<option value="{{ $community->id }}" {{ old('community_id') == $community->id ? 'selected' : '' }}>
|
||||
r/{{ $community->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('community_id')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="type" class="block text-gray-700 font-bold mb-2">Post Type</label>
|
||||
<select id="type" name="type" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
<option value="text" {{ old('type') == 'text' ? 'selected' : '' }}>Text</option>
|
||||
<option value="link" {{ old('type') == 'link' ? 'selected' : '' }}>Link</option>
|
||||
<option value="image" {{ old('type') == 'image' ? 'selected' : '' }}>Image</option>
|
||||
</select>
|
||||
@error('type')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="title" class="block text-gray-700 font-bold mb-2">Title</label>
|
||||
<input type="text" id="title" name="title" value="{{ old('title') }}" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
@error('title')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4" id="content-field">
|
||||
<label for="content" class="block text-gray-700 font-bold mb-2">Content</label>
|
||||
<textarea id="content" name="content" rows="6"
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">{{ old('content') }}</textarea>
|
||||
@error('content')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6 hidden" id="url-field">
|
||||
<label for="url" class="block text-gray-700 font-bold mb-2">URL</label>
|
||||
<input type="url" id="url" name="url" value="{{ old('url') }}"
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500">
|
||||
@error('url')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-orange-600 text-white px-6 py-2 rounded hover:bg-orange-700 font-bold">
|
||||
Create Post
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('type').addEventListener('change', function() {
|
||||
const contentField = document.getElementById('content-field');
|
||||
const urlField = document.getElementById('url-field');
|
||||
|
||||
if (this.value === 'text') {
|
||||
contentField.classList.remove('hidden');
|
||||
urlField.classList.add('hidden');
|
||||
} else {
|
||||
contentField.classList.add('hidden');
|
||||
urlField.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
89
resources/views/posts/show.blade.php
Normal file
89
resources/views/posts/show.blade.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
@extends('layout')
|
||||
|
||||
@section('title', $post->title)
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div class="flex">
|
||||
<div class="flex flex-col items-center mr-4">
|
||||
@auth
|
||||
<form action="{{ route('posts.vote', $post) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="vote" value="1">
|
||||
<button type="submit" class="text-gray-400 hover:text-orange-600">▲</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="text-gray-400">▲</span>
|
||||
@endauth
|
||||
|
||||
<span class="font-bold text-lg {{ $post->votes > 0 ? 'text-orange-600' : ($post->votes < 0 ? 'text-blue-600' : 'text-gray-600') }}">
|
||||
{{ $post->votes }}
|
||||
</span>
|
||||
|
||||
@auth
|
||||
<form action="{{ route('posts.vote', $post) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="vote" value="-1">
|
||||
<button type="submit" class="text-gray-400 hover:text-blue-600">▼</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="text-gray-400">▼</span>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="text-sm text-gray-600 mb-2">
|
||||
<a href="{{ route('communities.show', $post->community) }}" class="font-bold hover:underline">r/{{ $post->community->name }}</a>
|
||||
• Posted by <a href="{{ route('users.show', $post->user) }}" class="hover:underline">u/{{ $post->user->name }}</a>
|
||||
• {{ $post->created_at->diffForHumans() }}
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold mb-4">{{ $post->title }}</h1>
|
||||
@if($post->content)
|
||||
<p class="text-gray-700 mb-4 whitespace-pre-wrap">{{ $post->content }}</p>
|
||||
@endif
|
||||
@if($post->url)
|
||||
<a href="{{ $post->url }}" target="_blank" class="text-blue-600 hover:underline">{{ $post->url }}</a>
|
||||
@endif
|
||||
|
||||
@auth
|
||||
@if($post->user_id === auth()->id())
|
||||
<form action="{{ route('posts.destroy', $post) }}" method="POST" class="mt-4">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:underline" onclick="return confirm('Are you sure?')">Delete Post</button>
|
||||
</form>
|
||||
@endif
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@auth
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Add a Comment</h2>
|
||||
<form action="{{ route('comments.store', $post) }}" method="POST">
|
||||
@csrf
|
||||
<textarea name="content" rows="4" required
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:border-orange-500"></textarea>
|
||||
@error('content')
|
||||
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<button type="submit" class="mt-2 bg-orange-600 text-white px-4 py-2 rounded hover:bg-orange-700">
|
||||
Comment
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-bold mb-4">Comments ({{ $post->comments->count() }})</h2>
|
||||
|
||||
@forelse($post->comments->whereNull('parent_id') as $comment)
|
||||
@include('partials.comment', ['comment' => $comment, 'depth' => 0])
|
||||
@empty
|
||||
<p class="text-gray-600">No comments yet. Be the first to comment!</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
57
resources/views/users/show.blade.php
Normal file
57
resources/views/users/show.blade.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
@extends('layout')
|
||||
|
||||
@section('title', 'u/' . $user->name)
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h1 class="text-3xl font-bold mb-2">u/{{ $user->name }}</h1>
|
||||
<div class="text-gray-600">
|
||||
<p>Karma: {{ $user->karma }}</p>
|
||||
<p>Joined {{ $user->created_at->diffForHumans() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold mb-4">Posts</h2>
|
||||
@forelse($posts as $post)
|
||||
<div class="bg-white rounded-lg shadow mb-4 p-4">
|
||||
<h3 class="text-xl font-semibold mb-2">
|
||||
<a href="{{ route('posts.show', $post) }}" class="hover:text-orange-600">{{ $post->title }}</a>
|
||||
</h3>
|
||||
<div class="text-sm text-gray-600">
|
||||
<a href="{{ route('communities.show', $post->community) }}" class="font-bold hover:underline">r/{{ $post->community->name }}</a>
|
||||
• {{ $post->votes }} votes
|
||||
• {{ $post->comments->count() }} comments
|
||||
• {{ $post->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-600">No posts yet.</p>
|
||||
@endforelse
|
||||
<div class="mt-4">
|
||||
{{ $posts->links() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold mb-4">Comments</h2>
|
||||
@forelse($comments as $comment)
|
||||
<div class="bg-white rounded-lg shadow mb-4 p-4">
|
||||
<p class="text-gray-700 mb-2">{{ Str::limit($comment->content, 200) }}</p>
|
||||
<div class="text-sm text-gray-600">
|
||||
On <a href="{{ route('posts.show', $comment->post) }}" class="hover:underline">{{ $comment->post->title }}</a>
|
||||
in <a href="{{ route('communities.show', $comment->post->community) }}" class="font-bold hover:underline">r/{{ $comment->post->community->name }}</a>
|
||||
• {{ $comment->votes }} votes
|
||||
• {{ $comment->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-600">No comments yet.</p>
|
||||
@endforelse
|
||||
<div class="mt-4">
|
||||
{{ $comments->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -1,7 +1,41 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\CommentController;
|
||||
use App\Http\Controllers\CommunityController;
|
||||
use App\Http\Controllers\HomeController;
|
||||
use App\Http\Controllers\PostController;
|
||||
use App\Http\Controllers\UserController;
|
||||
use App\Http\Controllers\VoteController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||
|
||||
Route::get('/register', [AuthController::class, 'showRegister'])->name('register');
|
||||
Route::post('/register', [AuthController::class, 'register']);
|
||||
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
|
||||
|
||||
Route::get('/communities', [CommunityController::class, 'index'])->name('communities.index');
|
||||
Route::get('/r/{community}', [CommunityController::class, 'show'])->name('communities.show');
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::get('/communities/create', [CommunityController::class, 'create'])->name('communities.create');
|
||||
Route::post('/communities', [CommunityController::class, 'store'])->name('communities.store');
|
||||
Route::post('/r/{community}/subscribe', [CommunityController::class, 'subscribe'])->name('communities.subscribe');
|
||||
Route::post('/r/{community}/unsubscribe', [CommunityController::class, 'unsubscribe'])->name('communities.unsubscribe');
|
||||
|
||||
Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create');
|
||||
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
|
||||
Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy');
|
||||
|
||||
Route::post('/posts/{post}/comments', [CommentController::class, 'store'])->name('comments.store');
|
||||
Route::delete('/comments/{comment}', [CommentController::class, 'destroy'])->name('comments.destroy');
|
||||
|
||||
Route::post('/posts/{post}/vote', [VoteController::class, 'votePost'])->name('posts.vote');
|
||||
Route::post('/comments/{comment}/vote', [VoteController::class, 'voteComment'])->name('comments.vote');
|
||||
});
|
||||
|
||||
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
|
||||
Route::get('/u/{user}', [UserController::class, 'show'])->name('users.show');
|
||||
|
|
|
|||
Loading…
Reference in a new issue