Files
takeone-youtube-clone/app/Models/Comment.php
T
ghassanandClaude Opus 4.7 4d9bc0f501 feat(engagement): video dislike + creator heart + creator pin
Three features that depend on the two additive migrations shipped in
the same commit (safe defaults, cascading foreign keys, no data loss
possible).

## Dislike button
- New video_dislikes table (migration 2026_10_21_000001) with the same
  shape as video_likes.
- Video model: dislikes() relation + isDislikedBy() / getDislikeCount
  Attribute() helpers mirroring the like pair.
- UserController@toggleDislike: endpoint behind POST
  /videos/{video}/toggle-dislike. Mutually exclusive with like — the
  toggle*Like / toggle*Dislike controllers detach the opposite state
  so a user is in exactly one of {liked, disliked, neither}. YouTube
  parity.
- video-actions component: a second thumbs-down button next to the
  existing like. Both reconcile in parallel on either response so the
  UI pair always reflects the authoritative server state.

## Creator heart + pin
- comments table gains hearted + pinned boolean columns (migration
  2026_10_21_000002, both default false, pinned is indexed).
- CommentController@heart + @pin: 403 for anyone but the video owner;
  pin() also rejects nested replies (422) and auto-unpins the
  previously-pinned top-level comment so the "at most one pinned"
  invariant is enforced at the DB level.
- Comment component:
    - Pinned comments render ahead of everything else; reorder()
      clears the comments() relation's default ->latest() so pinned
      DESC becomes the PRIMARY sort instead of a tiebreaker.
    - Creator sees heart + pin toggle buttons in the action bar.
    - Hearted comments carry a small red heart badge in the meta row.
    - Pinned comments carry a "Pinned by {creator}" header and a
      red left accent so they're visually distinct from the stream.

## Tests (9 new, 63 total)
- Dislike toggle adds/removes the row.
- Disliking clears an existing like and vice versa.
- Owner can heart; non-owner gets 403.
- Only one comment pinned at a time; replies can't be pinned (422);
  non-owner gets 403.
- Pinned comments render before newer unpinned ones.

## Deploy note
Running the migrations against the live DB needs:
    php artisan migrate

The two migration files are additive and reversible via `migrate:rollback`.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-10-21 12:18:17 +03:00

67 lines
1.4 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
protected $fillable = [
'video_id',
'user_id',
'parent_id',
'body',
'hearted',
'pinned',
];
protected $casts = [
'hearted' => 'boolean',
'pinned' => 'boolean',
];
// Relationships
public function video()
{
return $this->belongsTo(Video::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')->latest();
}
public function likes()
{
return $this->hasMany(CommentLike::class);
}
// Get mentioned users from comment body
public function getMentionedUsers()
{
preg_match_all('/@(\w+)/', $this->body, $matches);
if (empty($matches[1])) {
return collect();
}
return User::whereIn('username', $matches[1])->get();
}
// Parse mentions and make them clickable
public function getParsedBodyAttribute()
{
$body = e($this->body);
$body = preg_replace('/@(\w+)/', '<a href="/channel/$1" class="user-mention">@$1</a>', $body);
return $body;
}
}