diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index 5f68fb24..bf0250ad 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -7,14 +7,14 @@ use App\Http\Resources\CommentResource; use App\Models\Comment; use App\Services\ModelResolverService; -use Illuminate\Database\Eloquent\Collection; use App\Http\Resources\UserResource; use App\Services\CommentService; -use Auth; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Log; - class CommentController extends Controller { protected $modelResolver; @@ -79,7 +79,7 @@ public function store(StoreCommentRequest $request) // Ensure that they are commenting to the same root // And the depth is not exceeded - validator( + Validator::make( [ 'commentable_id' => $commentableId, 'commentable_type' => $commentableType, @@ -140,61 +140,18 @@ public function store(StoreCommentRequest $request) /** * Display the specified comment, with pagination. */ - public function show(string $commentableType, int $commentableId, int $index) + public function show(Request $request, string $commentableType, int $commentableId, int $index, int $paginationLimit = -1) { - validator( - [ - 'index' => $index, - 'commentable_type' => $commentableType, - ], - [ - 'index' => 'required|integer|min:0', - 'commentable_type' => ['required', Rule::in(config('comment.commentable_types'))] - ] - )->validate(); - - Log::debug("Request is, commentable_type: " . $commentableType . ". id: " . $commentableId . ". index: " . $index); - - $commentableType = $this->modelResolver->getModelClass($commentableType); - $paginatedResults = $this->commentService->getPaginatedComments($commentableType, $commentableId, $index); - $nestedComments = new Collection($paginatedResults['comments']); - - // Lazy eager load the user for the root comment and for each reply. - $nestedComments->load(['user', 'replies.user']); - - // Flatten the comments and replies into the desired format. - $flattenedComments = collect(); - - foreach ($nestedComments as $comment) { - // Transform the root comment. - $flattenedComments->push( - new CommentResource($comment) - ); - - // Transform any loaded replies. - if ($comment->relationLoaded('replies')) { - foreach ($comment->replies as $reply) { - $flattenedComments->push( - new CommentResource($reply) - ); - } - } - } - - // Extract unique users into a separate collection. - $users = collect(); - - foreach ($flattenedComments as $comment) { - if ($comment->relationLoaded('user') && $comment->user) { - $users->put($comment->user->id, new UserResource($comment->user)); - } + if ($paginationLimit == -1) + { + $paginationLimit = config('comment.default_pagination_limit'); } - Log::debug("Returned comments: " . json_encode($flattenedComments)); - return [ - 'comments' => $flattenedComments, - 'users' => $users->values(), - 'has_more_comments' => $paginatedResults['has_more_comments'], - ]; + $sortBy = $request->query('sort_by', 'top'); + + Log::debug("Request is, commentable_type: " . $commentableType . ". id: " . $commentableId . ". index: " . $index . ". Sorting: " . $sortBy); + $paginatedResults = $this->commentService->getPaginatedComments($commentableType, $commentableId, $index, $paginationLimit, $sortBy); + + return $paginatedResults; } } diff --git a/app/Http/Controllers/ComputerScienceResourceController.php b/app/Http/Controllers/ComputerScienceResourceController.php index b63ea7d1..c1732f6e 100644 --- a/app/Http/Controllers/ComputerScienceResourceController.php +++ b/app/Http/Controllers/ComputerScienceResourceController.php @@ -4,13 +4,11 @@ use App\Http\Requests\ComputerScienceResource\StoreResourceRequest; use App\Models\ComputerScienceResource; -use App\Models\ResourceReview; -use Illuminate\Database\Console\DumpCommand; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Log; +use App\Services\CommentService; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; use Inertia\Inertia; -use Auth; class ComputerScienceResourceController extends Controller { @@ -20,7 +18,8 @@ class ComputerScienceResourceController extends Controller public function index() { // Eager load topic tags and other tag types as needed - $resources = ComputerScienceResource::paginate(10); + $resources = ComputerScienceResource::with(['tags', 'votes', 'upvoteSummary', 'reviewSummary', 'commentsCountRelationship']) + ->paginate(10); return Inertia::render('Resources/Index', [ 'resources' => $resources, ]); @@ -75,15 +74,44 @@ public function store(StoreResourceRequest $request) /** * Display the specified resource. */ - public function show(ComputerScienceResource $computerScienceResource) + public function show(Request $request, CommentService $commentService, ComputerScienceResource $computerScienceResource, string $tab = 'reviews') { - $reviews = $computerScienceResource->reviews()->orderByDesc('created_at')->get(); - - return Inertia::render('Resources/Show', [ - 'resource' => fn() => $computerScienceResource->load('user'), - 'reviews' => fn () => $reviews, - ]); + $validTabs = ['reviews', 'discussion', 'edits']; + + if (!in_array($tab, $validTabs)) { + // Redirect to default if invalid + return redirect()->route('resources.show', [ + 'computerScienceResource' => $computerScienceResource->id, + 'tab' => 'reviews', + ]); + } + + // return the resource and tab + $data = [ + 'tab' => $tab, + 'resource' => $computerScienceResource, + ]; + + // Load only the necessary tab data + if ($tab === 'reviews') { + $data['reviews'] = Inertia::defer(fn () => + $computerScienceResource->reviews()->orderByDesc('created_at')->get() + ); + } elseif ($tab === 'edits') { + $data['resourceEdits'] = Inertia::defer(fn () => + $computerScienceResource->edits + ); + } elseif ($tab === 'discussion') { + $sortBy = $request->query('sort_by', 'top'); + $data['discussion'] = Inertia::defer(fn () => + $commentService->getPaginatedComments('resource', $computerScienceResource->id, 0, 150, $sortBy) + ); + $data['discussionSortByValue'] = $sortBy; + } + + return Inertia::render('Resources/Show', $data); } + /** * Remove the specified resource from storage. diff --git a/app/Http/Controllers/ResourceEditsController.php b/app/Http/Controllers/ResourceEditsController.php index 1d5a2b5b..ffa28f53 100644 --- a/app/Http/Controllers/ResourceEditsController.php +++ b/app/Http/Controllers/ResourceEditsController.php @@ -7,28 +7,12 @@ use App\Services\ResourceEditsService; use App\Http\Requests\ResourceEdit\StoreResourceEdit; use App\Http\Resources\ComputerScienceResourceResource; -use Arr; -use Auth; +use Illuminate\Support\Facades\Auth; use Inertia\Inertia; use Log; class ResourceEditsController extends Controller { - /** - * Show all the edits for a given index. - */ - public function index(ComputerScienceResource $computerScienceResource) - { - $edits = $computerScienceResource->edits; - - Log::debug("The edits: " . json_encode($edits)); - - return Inertia::render('ResourceEdits/Index', [ - 'resourceId' => $computerScienceResource->id, - 'resourceEdits' => fn() => $edits, - ]); - } - /** * Return the form to create a edit. */ diff --git a/app/Http/Controllers/ResourceReviewController.php b/app/Http/Controllers/ResourceReviewController.php index 6673ad51..2c768db9 100644 --- a/app/Http/Controllers/ResourceReviewController.php +++ b/app/Http/Controllers/ResourceReviewController.php @@ -6,7 +6,7 @@ use App\Http\Requests\ResourceReview\StoreResourceReview; use App\Models\ComputerScienceResource; use App\Models\ResourceReview; -use Auth; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; use Redirect; diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 5ed235fc..fbe9cdd6 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -37,9 +37,13 @@ public function share(Request $request): array 'location' => $request->url(), ], 'flash' => [ - 'success' => fn () => $request->session()->get('success'), - 'warning' => fn () => $request->session()->get('warning'), - 'error' => fn () => $request->session()->get('error'), + 'success' => $request->session()->get('success'), + 'warning' => $request->session()->get('warning'), + 'error' => $request->session()->get('error'), + ], + 'config' => [ + 'COMMENT_MAX_DEPTH' => config('comment.max_depth'), + 'COMMENT_PAGINATION_LIMIT' => config('comment.pagination_limit'), ] ]; } diff --git a/app/Http/Requests/Comment/StoreCommentRequest.php b/app/Http/Requests/Comment/StoreCommentRequest.php index d961b4ad..c151e359 100644 --- a/app/Http/Requests/Comment/StoreCommentRequest.php +++ b/app/Http/Requests/Comment/StoreCommentRequest.php @@ -4,7 +4,7 @@ use App\Services\ModelResolverService; use Illuminate\Foundation\Http\FormRequest; -use Auth; +use Illuminate\Support\Facades\Auth; use Illuminate\Validation\Rule; class StoreCommentRequest extends FormRequest @@ -37,7 +37,7 @@ public function rules(): array "commentable_type" => [ 'required', 'string', - Rule::in(config('comment.commentable_types')), + Rule::in(config('comment.commentable_types_shorthand')), ], "content" => ["required", "string", "max:4000"], "parent_comment_id" => ["nullable", "exists:App\Models\Comment,id"] diff --git a/app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php b/app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php index 3923bf33..eecb12b1 100644 --- a/app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php +++ b/app/Http/Requests/ComputerScienceResource/StoreResourceRequest.php @@ -3,7 +3,7 @@ namespace App\Http\Requests\ComputerScienceResource; use App\Http\Requests\Shared\ComputerScienceResourceRequest; -use Auth; +use Illuminate\Support\Facades\Auth; use Illuminate\Foundation\Http\FormRequest; class StoreResourceRequest extends FormRequest diff --git a/app/Http/Requests/ResourceEdit/StoreResourceEdit.php b/app/Http/Requests/ResourceEdit/StoreResourceEdit.php index 52e2065a..517654a8 100644 --- a/app/Http/Requests/ResourceEdit/StoreResourceEdit.php +++ b/app/Http/Requests/ResourceEdit/StoreResourceEdit.php @@ -3,7 +3,7 @@ namespace App\Http\Requests\ResourceEdit; use App\Http\Requests\Shared\ComputerScienceResourceRequest; -use Auth; +use Illuminate\Support\Facades\Auth; use Illuminate\Foundation\Http\FormRequest; class StoreResourceEdit extends FormRequest diff --git a/app/Models/ComputerScienceResource.php b/app/Models/ComputerScienceResource.php index 82e0ab01..c75dc95e 100644 --- a/app/Models/ComputerScienceResource.php +++ b/app/Models/ComputerScienceResource.php @@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Casts\Attribute; use Spatie\Tags\HasTags; -use Auth; +use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -24,8 +24,6 @@ class ComputerScienceResource extends Model protected $table = "computer_science_resources"; protected $guarded = []; - - protected $with = ['tags', 'votes', 'upvoteSummary', 'reviewSummary', 'commentsCountRelationship']; protected $appends = ['topic_tags', 'programming_language_tags', 'general_tags', 'vote_score', 'user_vote', 'comments_count']; diff --git a/app/Models/ResourceEdits.php b/app/Models/ResourceEdits.php index c13a344f..df99a59b 100644 --- a/app/Models/ResourceEdits.php +++ b/app/Models/ResourceEdits.php @@ -19,7 +19,7 @@ class ResourceEdits extends Model protected $guarded = []; - protected $with = ['votes','upvoteSummary', 'commentsCountRelationship']; + protected $with = ['votes','upvoteSummary', 'commentsCountRelationship', 'resource']; protected $appends = ['user_vote', 'vote_score', 'comments_count', 'can_merge_edits']; diff --git a/app/Services/CommentService.php b/app/Services/CommentService.php index 754648f9..292306de 100644 --- a/app/Services/CommentService.php +++ b/app/Services/CommentService.php @@ -2,17 +2,21 @@ namespace App\Services; +use App\Http\Resources\CommentResource; +use App\Http\Resources\UserResource; use App\Models\Comment; -use Illuminate\Support\Collection; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; class CommentService { - protected $maxPerPage; + protected $modelResolver; - public function __construct() + function __construct(ModelResolverService $resolver) { - $this->maxPerPage = config('comment.pagination_limit', 10); + $this->modelResolver = $resolver; } /** @@ -23,24 +27,45 @@ public function __construct() * @param int $index * @return array */ - public function getPaginatedComments(string $commentableType, int $commentableId, int $index): array + // TODO: Refactor commentable types, and commentable types short for all other objects + public function getPaginatedComments(string $commentableTypeShort, int $commentableId, int $index, int $paginationLimit = -1, string $sortBy = 'top'): array { + if ($paginationLimit == -1) + { + $paginationLimit = config('comment.default_pagination_limit'); + } + + Validator::make([ + 'index' => $index, + 'commentable_type_short' => $commentableTypeShort, + 'pagination_limit' => $paginationLimit, + 'sort_by' => $sortBy, + ], [ + 'index' => ['required', 'integer', 'min:0'], + 'commentable_type_short' => ['required', Rule::in(config('comment.commentable_types_shorthand'))], + 'pagination_limit' => ['required', 'integer', 'max:' . config('comment.pagination_limit')], + 'sort_by' => ['required', 'string', Rule::in(config('comment.sortable_options'))], + ])->validate(); + + $commentableType = $this->modelResolver->getModelClass($commentableTypeShort); Log::debug("Request is, commentable_type: {$commentableType}. id: {$commentableId}. index: {$index}"); // Get the root comments: - $rootComments = Comment::where([ + $query = Comment::where([ 'commentable_type' => $commentableType, - 'commentable_id' => $commentableId, - 'depth' => 1, - ]) - ->orderBy('created_at') - ->get(); + 'commentable_id' => $commentableId, + 'depth' => 1, + ]); + + // Apply sorting on the comments + app(UpvoteService::class)->applySort($query, $sortBy, Comment::class); + $rootComments = $query->get(); Log::debug("Root comments: " . json_encode($rootComments)); - + // Initialize variables $currentCommentsSum = 0; - $commentsToReturn = []; + $resultingPaginatedComments = []; $currentIndex = 0; $hasMoreComments = false; @@ -48,12 +73,12 @@ public function getPaginatedComments(string $commentableType, int $commentableId $childrenCount = $comment->children_count + 1; // Handle comments that exceed MAX when alone in a page - if ($currentCommentsSum + $childrenCount > $this->maxPerPage) { + if ($currentCommentsSum + $childrenCount > $paginationLimit) { if ($currentCommentsSum === 0) { // Force include oversized comment if it's the first in page Log::warning("Had to force include for oversized comment tree. Should consider increasing the max commentx in config or lowering the replies size limit."); if ($currentIndex === $index) { - $commentsToReturn[] = $comment; + $resultingPaginatedComments[] = $comment; $currentCommentsSum += $childrenCount; } $currentIndex++; @@ -71,13 +96,47 @@ public function getPaginatedComments(string $commentableType, int $commentableId } // Only add comments for the desired index else if ($currentIndex === $index) { - $commentsToReturn[] = $comment; + $resultingPaginatedComments[] = $comment; } $currentCommentsSum += $childrenCount; } + $nestedComments = new Collection($resultingPaginatedComments); + // Lazy eager load the user for the root comment and for each reply. + $nestedComments->load(['user', 'replies.user']); + + // Flatten the comments and replies into the desired format. + $flattenedComments = collect(); + + foreach ($nestedComments as $comment) { + // Transform the root comment. + $flattenedComments->push( + new CommentResource($comment) + ); + + // Transform any loaded replies. + if ($comment->relationLoaded('replies')) { + foreach ($comment->replies as $reply) { + $flattenedComments->push( + new CommentResource($reply) + ); + } + } + } + + // Extract unique users into a separate collection. + $users = collect(); + + foreach ($flattenedComments as $comment) { + if ($comment->relationLoaded('user') && $comment->user) { + $users->put($comment->user->id, new UserResource($comment->user)); + } + } + + Log::debug("Returned comments: " . json_encode($flattenedComments)); return [ - 'comments' => new Collection($commentsToReturn), + 'comments' => $flattenedComments, + 'users' => $users->values(), 'has_more_comments' => $hasMoreComments, ]; } diff --git a/app/Services/UpvoteService.php b/app/Services/UpvoteService.php new file mode 100644 index 00000000..f6ac04a1 --- /dev/null +++ b/app/Services/UpvoteService.php @@ -0,0 +1,62 @@ +getModel()->getTable(); + + switch ($sortBy) { + case 'bottom': + $this->joinUpvoteSummary($query, $modelClass) + ->orderByRaw('COALESCE(upvote_summaries.upvotes, 0) - COALESCE(upvote_summaries.downvotes, 0) ASC'); + break; + + case 'controversial': + $this->joinUpvoteSummary($query, $modelClass) + ->orderByRaw(' + (COALESCE(upvote_summaries.upvotes, 0) + COALESCE(upvote_summaries.downvotes, 0)) - + ABS(COALESCE(upvote_summaries.upvotes, 0) - COALESCE(upvote_summaries.downvotes, 0)) ASC + '); + break; + + case 'mine': + $query->where("{$table}.user_id", Auth::id()) + ->orderBy("{$table}.created_at", 'desc'); + break; + + case 'latest': + $query->orderBy("{$table}.created_at", 'desc'); + break; + + case 'top': + default: + $this->joinUpvoteSummary($query, $modelClass) + ->orderByRaw('COALESCE(upvote_summaries.upvotes, 0) - COALESCE(upvote_summaries.downvotes, 0) DESC'); + break; + } + + return $query; + } + protected function joinUpvoteSummary(Builder $query, string $modelClass): Builder + { + $table = $query->getModel()->getTable(); + + $query->leftJoin('upvote_summaries', function (JoinClause $join) use ($modelClass, $table) { + $join->on('upvote_summaries.upvotable_id', '=', "{$table}.id") + ->where('upvote_summaries.upvotable_type', '=', $modelClass); + }); + + // Joins would include all fields from all joined tables, + // But, we only want the original model + $query->select("{$table}.*"); + + return $query; + } +} diff --git a/app/Traits/HasVotes.php b/app/Traits/HasVotes.php index 3dd7570e..db9bd348 100644 --- a/app/Traits/HasVotes.php +++ b/app/Traits/HasVotes.php @@ -5,7 +5,7 @@ use App\Models\Upvote; use App\Events\UpvoteProcessed; use App\Models\UpvoteSummary; -use Auth; +use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphOne; diff --git a/composer.json b/composer.json index b54b19cd..5ebfb7ee 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": "^8.2", - "inertiajs/inertia-laravel": "^1.0", + "inertiajs/inertia-laravel": "^2.0", "joelbutcher/socialstream": "^6.2", "laravel/framework": "^11.31", "laravel/jetstream": "^5.3", diff --git a/composer.lock b/composer.lock index bffa78f9..b8fa7528 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7e374310093dc0692e1d229ea1a6e12e", + "content-hash": "7725813741904ac1263575e1bcce49b0", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1223,28 +1223,29 @@ }, { "name": "inertiajs/inertia-laravel", - "version": "v1.3.2", + "version": "v2.0.2", "source": { "type": "git", "url": "https://github.com/inertiajs/inertia-laravel.git", - "reference": "7e6a030ffab315099782a4844a2175455f511c68" + "reference": "248e815cf8d41307cbfb735efaa514c118e2f3b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/7e6a030ffab315099782a4844a2175455f511c68", - "reference": "7e6a030ffab315099782a4844a2175455f511c68", + "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/248e815cf8d41307cbfb735efaa514c118e2f3b4", + "reference": "248e815cf8d41307cbfb735efaa514c118e2f3b4", "shasum": "" }, "require": { "ext-json": "*", - "laravel/framework": "^8.74|^9.0|^10.0|^11.0", - "php": "^7.3|~8.0.0|~8.1.0|~8.2.0|~8.3.0|~8.4.0", - "symfony/console": "^5.3|^6.0|^7.0" + "laravel/framework": "^10.0|^11.0|^12.0", + "php": "^8.1.0", + "symfony/console": "^6.2|^7.0" }, "require-dev": { + "laravel/pint": "^1.16", "mockery/mockery": "^1.3.3", - "orchestra/testbench": "^6.45|^7.44|^8.25|^9.3", - "phpunit/phpunit": "^8.0|^9.5.8|^10.4", + "orchestra/testbench": "^8.0|^9.2|^10.0", + "phpunit/phpunit": "^10.4|^11.5", "roave/security-advisories": "dev-master" }, "suggest": { @@ -1256,9 +1257,6 @@ "providers": [ "Inertia\\ServiceProvider" ] - }, - "branch-alias": { - "dev-master": "1.x-dev" } }, "autoload": { @@ -1287,15 +1285,9 @@ ], "support": { "issues": "https://github.com/inertiajs/inertia-laravel/issues", - "source": "https://github.com/inertiajs/inertia-laravel/tree/v1.3.2" + "source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.2" }, - "funding": [ - { - "url": "https://github.com/reinink", - "type": "github" - } - ], - "time": "2024-12-05T14:52:50+00:00" + "time": "2025-04-10T15:08:36+00:00" }, { "name": "joelbutcher/socialstream", diff --git a/config/comment.php b/config/comment.php index 3d6b4997..20f863db 100644 --- a/config/comment.php +++ b/config/comment.php @@ -3,6 +3,8 @@ return [ 'max_replies' => 150, 'max_depth' => 7, // 1-indexed, 1 is the start - 'pagination_limit' => 3, - 'commentable_types' => ['review', 'comment', 'edit', 'resource'] + 'pagination_limit' => 150, + 'default_pagination_limit' => 5, + 'commentable_types_shorthand' => ['review', 'comment', 'edit', 'resource'], + 'sortable_options' => ['latest', 'top', 'bottom', 'controversial', 'mine'] ]; \ No newline at end of file diff --git a/database/migrations/2025_02_09_172232_create_upvote_summaries_table.php b/database/migrations/2025_02_09_172232_create_upvote_summaries_table.php index caf824d1..7718fbb9 100644 --- a/database/migrations/2025_02_09_172232_create_upvote_summaries_table.php +++ b/database/migrations/2025_02_09_172232_create_upvote_summaries_table.php @@ -15,8 +15,8 @@ public function up(): void Schema::create('upvote_summaries', function (Blueprint $table) { $table->id(); $table->morphs('upvotable'); - $table->unsignedBigInteger('upvotes')->default(0); - $table->unsignedBigInteger('downvotes')->default(0); + $table->bigInteger('upvotes')->default(0); + $table->bigInteger('downvotes')->default(0); $table->timestamps(); }); } diff --git a/database/seeders/ComputerScienceResourceSeeder.php b/database/seeders/ComputerScienceResourceSeeder.php index 340c9edd..2e315472 100644 --- a/database/seeders/ComputerScienceResourceSeeder.php +++ b/database/seeders/ComputerScienceResourceSeeder.php @@ -14,6 +14,6 @@ class ComputerScienceResourceSeeder extends Seeder */ public function run(): void { - ComputerScienceResource::factory(1)->create(); + ComputerScienceResource::factory(5)->create(); } } diff --git a/database/seeders/ResourceReviewSeeder.php b/database/seeders/ResourceReviewSeeder.php index 2d9a08ba..bfec227b 100644 --- a/database/seeders/ResourceReviewSeeder.php +++ b/database/seeders/ResourceReviewSeeder.php @@ -13,6 +13,6 @@ class ResourceReviewSeeder extends Seeder */ public function run(): void { - ResourceReview::factory(5)->create(); + ResourceReview::factory(20)->create(); } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index 8d5b38d2..a22feda1 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -19,12 +19,14 @@ public function run(): void Log::info('Running UserSeeder'); User::factory(10)->create(); - User::factory()->create( - [ - 'name'=>'Allan Kong', - 'email'=>'allankong176@gmail.com', - 'password'=>Hash::make('password'), - ] - ); + if (!User::where('name','Allan Kong')->exists()) { + User::factory()->create( + [ + 'name' => 'Allan Kong', + 'email' => 'allankong176@gmail.com', + 'password' => Hash::make('password'), + ] + ); + } } } diff --git a/package-lock.json b/package-lock.json index 2aef6c0d..878c7fe8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@iconify/vue": "^4.3.0", - "@inertiajs/vue3": "^1.0.14", + "@inertiajs/vue3": "^2.0.8", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", "@vitejs/plugin-vue": "^5.0.0", @@ -536,28 +536,26 @@ } }, "node_modules/@inertiajs/core": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-1.3.0.tgz", - "integrity": "sha512-TJ8R1eUYY473m9DaKlCPRdHTdznFWTDuy5VvEzXg3t/hohbDQedLj46yn/uAqziJPEUZJrSftZzPI2NMzL9tQA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.0.8.tgz", + "integrity": "sha512-YE+b5FktbSSaWJt4CjCHy7z3t+IV97G/8kD33mkj2Fqqf+Jfsypd/jsOuxrQGSMDpIyAGR6EDoaiuss6+JuIPA==", "dev": true, "license": "MIT", "dependencies": { - "axios": "^1.6.0", - "deepmerge": "^4.0.0", - "nprogress": "^0.2.0", + "axios": "^1.8.2", + "es-toolkit": "^1.34.1", "qs": "^6.9.0" } }, "node_modules/@inertiajs/vue3": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-1.3.0.tgz", - "integrity": "sha512-GizqdCM3u4JWunit3uUbW4fEmTLKQTi1W7VvPRdrNy8XDt4Qy2cCmfFjq+aH5tHBSS3fI/ngYuhN7XvwqNaKvw==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-2.0.8.tgz", + "integrity": "sha512-XzerZJxxiTE40U6X9MggjQivUpHjHNaDnrts8TVnYIO6iRMSzrgqVMW/9DXIZGAuwE1z832Kj58/sWAefx+/BQ==", "dev": true, "license": "MIT", "dependencies": { - "@inertiajs/core": "1.3.0", - "lodash.clonedeep": "^4.5.0", - "lodash.isequal": "^4.5.0" + "@inertiajs/core": "2.0.8", + "es-toolkit": "^1.33.0" }, "peerDependencies": { "vue": "^3.0.0" @@ -1729,16 +1727,6 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1869,6 +1857,17 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.35.0.tgz", + "integrity": "sha512-kVHyrRoC0eLc1hWJ6npG8nNFtOG+nWfcMI+XE0RaFO0gxd6Ions8r0O/U64QyZgY7IeidUnS5oZlRZYUgMGCAg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", @@ -2379,21 +2378,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -2574,13 +2558,6 @@ "node": ">=0.10.0" } }, - "node_modules/nprogress": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", - "dev": true, - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", diff --git a/package.json b/package.json index c3a539df..102e8918 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "devDependencies": { "@iconify/vue": "^4.3.0", - "@inertiajs/vue3": "^1.0.14", + "@inertiajs/vue3": "^2.0.8", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", "@vitejs/plugin-vue": "^5.0.0", diff --git a/resources/js/Components/Comments/CommentActionsForm.vue b/resources/js/Components/Comments/CommentActionsForm.vue index efeb6fe2..16698661 100644 --- a/resources/js/Components/Comments/CommentActionsForm.vue +++ b/resources/js/Components/Comments/CommentActionsForm.vue @@ -20,7 +20,7 @@ const isOpen = ref(false); const commentableType = inject("commentableType"); const commentableId = inject("commentableId"); -const createdNewComment = inject("createdNewComment"); +const createdNewCommentCallback = inject("createdNewCommentCallback"); const toggleOpen = () => { isOpen.value = !isOpen.value; @@ -44,7 +44,7 @@ const submit = () => { console.log("Successful comment post!"); // Notify the parent that a comment is made - createdNewComment(response.data.new_comment, response.data.user); + createdNewCommentCallback(response.data.new_comment, response.data.user); form.content = ""; isOpen.value = false; diff --git a/resources/js/Components/Comments/CommentList.vue b/resources/js/Components/Comments/CommentList.vue index 29bf372b..c823c71b 100644 --- a/resources/js/Components/Comments/CommentList.vue +++ b/resources/js/Components/Comments/CommentList.vue @@ -2,7 +2,7 @@ import { defineProps, ref } from "vue"; import { Icon } from "@iconify/vue"; import SingleComment from "./SingleComment.vue"; -import { MAX_COMMENT_DEPTH } from "@/Helpers/constants"; +import { getConfigData } from "@/Helpers/config"; const props = defineProps({ idToChildren: { @@ -73,7 +73,7 @@ const toggleCollapse = () => { /> -import { ref, provide, readonly, nextTick, onMounted } from "vue"; +import { ref, provide, readonly, nextTick, onMounted, reactive } from "vue"; import axios from "axios"; import CommentActionsForm from "@/Components/Comments/CommentActionsForm.vue"; +import SortByDropdown from "@/Components/Comments/SortByDropdown.vue"; import CommentList from "./CommentList.vue"; const props = defineProps({ @@ -17,21 +18,37 @@ const props = defineProps({ type: Number, required: true, }, - loadOnMount: { + paginationLimit: { + type: Number, + default: 5, + }, + sortByInitialValue: { + type: String, + default: "top" + }, + loadedCommentData: { + type: Object, + required: false, + default: null, + }, + hasSortByDropdown: { type: Boolean, - default: false, - } + default: true, + }, }); -const users = ref(new Map()); -const can_load_more_comments = ref(true); +const hasLoadedCommentData = props.loadedCommentData != null; +const usersMap = ref(new Map()); +const canLoadMoreComments = ref(true); const currentIndex = ref(0); const isLoading = ref(false); const error = ref(null); const commentsLeft = ref(props.commentsCount); const idToChildren = ref(new Map()); +const sortBy = ref(props.sortByInitialValue); +const hasOpenedComments = ref(false); -const createdNewComment = (newComment, userData) => { +const createdNewCommentCallback = (newComment, userData) => { console.log("Created a new comment!", newComment, userData); updateUsers([userData]); updateCommentHierarchy([newComment]); @@ -53,52 +70,70 @@ const createdNewComment = (newComment, userData) => { provide("commentableId", props.commentableId); provide("commentableType", props.commentableType); -provide("users", readonly(users)); -provide("createdNewComment", createdNewComment); +provide("users", readonly(usersMap)); +provide("createdNewCommentCallback", createdNewCommentCallback); function updateUsers(newUsers) { + //normalize: if it’s a `{ data: { … } }` wrapper, grab `.data` + const users = newUsers.map((u) => u.data ?? u); + // Convert API response users to Map const normalizeUsers = (usersArray) => new Map(usersArray.map((user) => [user.id, user])); // Setting the map to a new value if (currentIndex.value === 0) { - users.value = normalizeUsers(newUsers); + usersMap.value = normalizeUsers(users); } else { - users.value = new Map([...users.value, ...normalizeUsers(newUsers)]); + usersMap.value = new Map([...usersMap.value, ...normalizeUsers(users)]); } } function updateCommentHierarchy(newComments) { - const hierarchyUpdates = {}; + //normalize: if it’s a `{ data: { … } }` wrapper, grab `.data` + const comments = newComments.map((c) => c.data ?? c); - commentsLeft.value -= newComments.length; + commentsLeft.value -= comments.length; - newComments.forEach((comment) => { + const hierarchyUpdates = {}; + comments.forEach((comment) => { + // now comment.parent_comment_id is either a number or null const parentId = comment.parent_comment_id; + if (!hierarchyUpdates[parentId]) { hierarchyUpdates[parentId] = []; } hierarchyUpdates[parentId].push(comment); }); - // Merge updates into idToChildren with reactivity + // merge into your reactive map/object idToChildren.value = { ...idToChildren.value, ...Object.fromEntries( - Object.entries(hierarchyUpdates).map(([parentId, children]) => [ - parentId, - [...(idToChildren.value[parentId] || []), ...children], + Object.entries(hierarchyUpdates).map(([pid, children]) => [ + pid, + [...(idToChildren.value[pid] || []), ...children], ]) ), }; } +function addCommentData(commentData) { + // Update the users + updateUsers(commentData.users); + // Update comment hierarchy for both new and existing comments + updateCommentHierarchy(commentData.comments); + + canLoadMoreComments.value = commentData.has_more_comments; + currentIndex.value++; +} + async function loadComments() { - if (isLoading.value || !can_load_more_comments.value) return; + if (isLoading.value || !canLoadMoreComments.value) return; isLoading.value = true; error.value = null; + hasOpenedComments.value = true; try { const response = await axios.get( @@ -106,18 +141,16 @@ async function loadComments() { id: props.commentableId, type: props.commentableType, index: currentIndex.value, + paginationLimit: props.paginationLimit, + sort_by: sortBy.value, }) ); - console.log(response); + console.log(response.data); + console.log(props.loadedCommentData); // Update the users - updateUsers(response.data.users); - // Update comment hierarchy for both new and existing comments - updateCommentHierarchy(response.data.comments); - - can_load_more_comments.value = response.data.has_more_comments; - currentIndex.value++; + addCommentData(response.data); } catch (err) { console.error("Error fetching comments:", err); error.value = "Failed to load comments. Please try again later."; @@ -126,17 +159,36 @@ async function loadComments() { } } +function handleSortChange(newSortType) { + if (newSortType == sortBy.value) return; + + sortBy.value = newSortType; + + usersMap.value = new Map(); + canLoadMoreComments.value = true; + isLoading.value = false; + error.value = null; + commentsLeft.value = props.commentsCount; + idToChildren.value = new Map(); + + currentIndex.value = 0; + loadComments(); +} + onMounted(() => { - if (props.loadOnMount) - { - console.log('loaded'); - loadComments(); + if (hasLoadedCommentData) { + addCommentData(props.loadedCommentData); } });