Add full notification preferences system
Users can now control which in-app and email notifications they receive from their Settings tab on their channel page. Bell (in-app) preferences: - New comment on my video (default: on) - Reply to my comment (default: on) - Comment liked (default: on) - Video liked (default: on) - New subscriber (default: on) - New video from channels I follow (default: on) - New post from channels I follow (default: on) - New user registration — super admins only (default: on) Email preferences (same set plus): - Comment liked (default: off — too noisy) - Video liked (default: off — too noisy) - New post from channels I follow (default: off) - My video finished processing (default: on) - Weekly activity digest every Monday (default: on) - New user registration — super admins only (default: on) Implementation: - Migration: notification_preferences JSON column on users table - User::notificationPref($key) helper with typed defaults - All existing notification classes updated to check prefs in via() - 4 new notification classes: NewSubscriberNotification, VideoLikedNotification, NewPostNotification, WeeklyDigestNotification - 8 new email views matching existing dark theme - SendWeeklyDigest artisan command, scheduled every Monday 09:00 - NewSubscriberNotification wired into UserController::toggleSubscribe - VideoLikedNotification wired into UserController::toggleLike - NewPostNotification wired into PostController::store (to all subscribers) - Bell renderer updated for new_subscriber, video_like, new_post types - Preferences saved via AJAX (POST /settings/notifications) — instant toggle with automatic revert on failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3fe167e33f
commit
f8d13457fa
33
app/Console/Commands/SendWeeklyDigest.php
Normal file
33
app/Console/Commands/SendWeeklyDigest.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Notifications\WeeklyDigestNotification;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SendWeeklyDigest extends Command
|
||||
{
|
||||
protected $signature = 'digest:weekly';
|
||||
protected $description = 'Send weekly activity digest emails to creators who have it enabled';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$users = User::whereHas('videos')->get();
|
||||
|
||||
$sent = 0;
|
||||
foreach ($users as $user) {
|
||||
if (! $user->notificationPref('email_weekly_digest')) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$user->notify(new WeeklyDigestNotification($user));
|
||||
$sent++;
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('Weekly digest failed for user ' . $user->id . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("Sent weekly digest to {$sent} users.");
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,12 @@ class Kernel extends ConsoleKernel
|
||||
$nas->clearNasCache(24);
|
||||
}
|
||||
})->daily()->name('nas-cache-evict')->withoutOverlapping();
|
||||
|
||||
// Weekly activity digest — every Monday at 9:00 AM (Bahrain time)
|
||||
$schedule->command('digest:weekly')
|
||||
->weeklyOn(1, '09:00')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -6,6 +6,7 @@ use App\Models\Post;
|
||||
use App\Models\PostImage;
|
||||
use App\Models\PostVideo;
|
||||
use App\Models\User;
|
||||
use App\Notifications\NewPostNotification;
|
||||
use App\Services\NasSyncService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@ -115,6 +116,12 @@ class PostController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Notify subscribers of new post
|
||||
$author = $user->fresh();
|
||||
$author->subscribers()->each(function (User $subscriber) use ($post, $author) {
|
||||
try { $subscriber->notify(new NewPostNotification($post, $author)); } catch (\Throwable) {}
|
||||
});
|
||||
|
||||
return back()->with('toast_success', 'Post shared!');
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,8 @@ use App\Models\AuditLog;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use App\Notifications\NewSubscriberNotification;
|
||||
use App\Notifications\VideoLikedNotification;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@ -151,6 +153,22 @@ class UserController extends Controller
|
||||
return redirect()->route('channel')->with('toast_success', 'Password updated!')->with('_open_tab', 'settings');
|
||||
}
|
||||
|
||||
// Save a single notification preference toggle (AJAX)
|
||||
public function updateNotificationPreferences(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'key' => ['required', 'string', 'in:' . implode(',', array_keys(User::notifDefaults()))],
|
||||
'value' => ['required', 'boolean'],
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$prefs = $user->notification_preferences ?? [];
|
||||
$prefs[$request->key] = (bool) $request->value;
|
||||
$user->update(['notification_preferences' => $prefs]);
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
// Logout all other devices
|
||||
public function logoutAllDevices(Request $request)
|
||||
{
|
||||
@ -348,6 +366,9 @@ class UserController extends Controller
|
||||
} else {
|
||||
$video->likes()->attach($user->id);
|
||||
$liked = true;
|
||||
if ($video->user_id && $video->user_id !== $user->id) {
|
||||
try { $video->user->notify(new VideoLikedNotification($video, $user)); } catch (\Throwable) {}
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
@ -370,6 +391,7 @@ class UserController extends Controller
|
||||
} else {
|
||||
$me->subscriptions()->attach($user->id);
|
||||
$subscribed = true;
|
||||
try { $user->notify(new NewSubscriberNotification($me)); } catch (\Throwable) {}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
|
||||
@ -43,6 +43,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
'two_factor_secret',
|
||||
'two_factor_enabled',
|
||||
'banner',
|
||||
'notification_preferences',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@ -51,11 +52,46 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'two_factor_enabled' => 'boolean',
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'two_factor_enabled' => 'boolean',
|
||||
'notification_preferences' => 'array',
|
||||
];
|
||||
|
||||
// Defaults for each preference key (true = on, false = off)
|
||||
private const NOTIF_DEFAULTS = [
|
||||
'notif_new_comment' => true,
|
||||
'notif_new_reply' => true,
|
||||
'notif_comment_like' => true,
|
||||
'notif_video_like' => true,
|
||||
'notif_new_subscriber' => true,
|
||||
'notif_new_video_from_sub' => true,
|
||||
'notif_new_post_from_sub' => true,
|
||||
'notif_new_user_reg' => true,
|
||||
'email_new_comment' => true,
|
||||
'email_new_reply' => true,
|
||||
'email_comment_like' => false,
|
||||
'email_video_like' => false,
|
||||
'email_new_subscriber' => true,
|
||||
'email_new_video_from_sub' => true,
|
||||
'email_new_post_from_sub' => false,
|
||||
'email_video_processed' => true,
|
||||
'email_new_user_reg' => true,
|
||||
'email_weekly_digest' => true,
|
||||
];
|
||||
|
||||
public function notificationPref(string $key): bool
|
||||
{
|
||||
$prefs = $this->notification_preferences ?? [];
|
||||
$default = self::NOTIF_DEFAULTS[$key] ?? true;
|
||||
return isset($prefs[$key]) ? (bool) $prefs[$key] : $default;
|
||||
}
|
||||
|
||||
public static function notifDefaults(): array
|
||||
{
|
||||
return self::NOTIF_DEFAULTS;
|
||||
}
|
||||
|
||||
protected $appends = ['avatar_url', 'banner_url'];
|
||||
|
||||
// Auto-generate a unique slug-based username when creating a user without one
|
||||
|
||||
@ -5,6 +5,7 @@ namespace App\Notifications;
|
||||
use App\Models\Comment;
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@ -18,7 +19,10 @@ class NewCommentLikeNotification extends Notification
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_comment_like')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_comment_like')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
@ -35,4 +39,16 @@ class NewCommentLikeNotification extends Notification
|
||||
'comment_preview' => Str::limit($this->comment->body, 80),
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->liker->name . ' liked your comment')
|
||||
->view('emails.comment-liked', [
|
||||
'video' => $this->video,
|
||||
'comment' => $this->comment,
|
||||
'liker' => $this->liker,
|
||||
'recipient'=> $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ namespace App\Notifications;
|
||||
use App\Models\Comment;
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@ -18,7 +19,10 @@ class NewCommentNotification extends Notification
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_comment')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_comment')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
@ -35,4 +39,16 @@ class NewCommentNotification extends Notification
|
||||
'comment_preview' => Str::limit($this->comment->body, 80),
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->commenter->name . ' commented on your video')
|
||||
->view('emails.new-comment', [
|
||||
'video' => $this->video,
|
||||
'comment' => $this->comment,
|
||||
'commenter'=> $this->commenter,
|
||||
'recipient'=> $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
46
app/Notifications/NewPostNotification.php
Normal file
46
app/Notifications/NewPostNotification.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class NewPostNotification extends Notification
|
||||
{
|
||||
public function __construct(public Post $post, public User $author) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_post_from_sub')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_post_from_sub')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'new_post',
|
||||
'post_id' => $this->post->id,
|
||||
'author_id' => $this->author->id,
|
||||
'author_name' => $this->author->name,
|
||||
'author_avatar' => $this->author->avatar_url,
|
||||
'author_channel' => $this->author->channel,
|
||||
'post_preview' => Str::limit(strip_tags($this->post->body ?? ''), 100),
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->author->name . ' posted something new')
|
||||
->view('emails.new-post-from-sub', [
|
||||
'post' => $this->post,
|
||||
'author' => $this->author,
|
||||
'recipient' => $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ namespace App\Notifications;
|
||||
use App\Models\Comment;
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@ -18,7 +19,10 @@ class NewReplyNotification extends Notification
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_reply')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_reply')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
@ -35,4 +39,16 @@ class NewReplyNotification extends Notification
|
||||
'comment_preview' => Str::limit($this->reply->body, 80),
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->replier->name . ' replied to your comment')
|
||||
->view('emails.new-reply', [
|
||||
'video' => $this->video,
|
||||
'reply' => $this->reply,
|
||||
'replier' => $this->replier,
|
||||
'recipient'=> $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
41
app/Notifications/NewSubscriberNotification.php
Normal file
41
app/Notifications/NewSubscriberNotification.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class NewSubscriberNotification extends Notification
|
||||
{
|
||||
public function __construct(public User $subscriber) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_subscriber')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_subscriber')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'new_subscriber',
|
||||
'actor_id' => $this->subscriber->id,
|
||||
'actor_name' => $this->subscriber->name,
|
||||
'actor_avatar' => $this->subscriber->avatar_url,
|
||||
'actor_channel' => $this->subscriber->channel,
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->subscriber->name . ' subscribed to your channel')
|
||||
->view('emails.new-subscriber', [
|
||||
'subscriber' => $this->subscriber,
|
||||
'recipient' => $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -3,8 +3,8 @@
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class NewUserRegistered extends Notification
|
||||
{
|
||||
@ -12,7 +12,10 @@ class NewUserRegistered extends Notification
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database', 'mail'];
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_user_reg')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_user_reg')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
|
||||
@ -4,17 +4,19 @@ namespace App\Notifications;
|
||||
|
||||
use App\Models\Video;
|
||||
use App\Models\User;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class NewVideoUploaded extends Notification
|
||||
{
|
||||
public function __construct(public Video $video, public User $uploader)
|
||||
{
|
||||
}
|
||||
public function __construct(public Video $video, public User $uploader) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_video_from_sub')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_video_from_sub')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
@ -30,4 +32,15 @@ class NewVideoUploaded extends Notification
|
||||
'uploader_avatar' => $this->uploader->avatar_url,
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->uploader->name . ' uploaded a new video')
|
||||
->view('emails.new-video-from-sub', [
|
||||
'video' => $this->video,
|
||||
'uploader' => $this->uploader,
|
||||
'recipient'=> $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
46
app/Notifications/VideoLikedNotification.php
Normal file
46
app/Notifications/VideoLikedNotification.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class VideoLikedNotification extends Notification
|
||||
{
|
||||
public function __construct(public Video $video, public User $liker) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_video_like')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_video_like')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'video_like',
|
||||
'video_id' => $this->video->id,
|
||||
'video_route_key' => $this->video->getRouteKey(),
|
||||
'video_title' => $this->video->title,
|
||||
'video_thumbnail' => $this->video->thumbnail,
|
||||
'actor_id' => $this->liker->id,
|
||||
'actor_name' => $this->liker->name,
|
||||
'actor_avatar' => $this->liker->avatar_url,
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->liker->name . ' liked your video')
|
||||
->view('emails.video-liked', [
|
||||
'video' => $this->video,
|
||||
'liker' => $this->liker,
|
||||
'recipient'=> $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
60
app/Notifications/WeeklyDigestNotification.php
Normal file
60
app/Notifications/WeeklyDigestNotification.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class WeeklyDigestNotification extends Notification
|
||||
{
|
||||
public array $stats;
|
||||
|
||||
public function __construct(public User $creator)
|
||||
{
|
||||
$videoIds = $creator->videos()->pluck('id');
|
||||
|
||||
$views = DB::table('video_views')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->where('created_at', '>=', now()->subWeek())
|
||||
->count();
|
||||
|
||||
$likes = DB::table('video_likes')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->where('created_at', '>=', now()->subWeek())
|
||||
->count();
|
||||
|
||||
$newSubs = DB::table('user_subscriptions')
|
||||
->where('channel_id', $creator->id)
|
||||
->where('created_at', '>=', now()->subWeek())
|
||||
->count();
|
||||
|
||||
$topVideo = $creator->videos()
|
||||
->withCount(['viewers as week_views' => fn($q) => $q->where('video_views.created_at', '>=', now()->subWeek())])
|
||||
->orderByDesc('week_views')
|
||||
->first();
|
||||
|
||||
$this->stats = [
|
||||
'views' => $views,
|
||||
'likes' => $likes,
|
||||
'new_subs' => $newSubs,
|
||||
'top_video' => $topVideo,
|
||||
];
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return $notifiable->notificationPref('email_weekly_digest') ? ['mail'] : [];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('Your weekly TAKEONE summary 📊')
|
||||
->view('emails.weekly-digest', [
|
||||
'creator' => $this->creator,
|
||||
'stats' => $this->stats,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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->json('notification_preferences')->nullable()->after('banner');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('notification_preferences');
|
||||
});
|
||||
}
|
||||
};
|
||||
27
resources/views/emails/comment-liked.blade.php
Normal file
27
resources/views/emails/comment-liked.blade.php
Normal file
@ -0,0 +1,27 @@
|
||||
<x-emails.layout subject="{{ $liker->name }} liked your comment">
|
||||
|
||||
<div style="width:64px;height:64px;border-radius:50%;background:rgba(230,30,30,.12);border:1px solid rgba(230,30,30,.25);margin:0 auto 24px;text-align:center;line-height:64px;font-size:28px;">👍</div>
|
||||
|
||||
<h1 class="email-title">Your comment got a like!</h1>
|
||||
<p class="email-subtitle">On the video <strong style="color:#f1f1f1;">{{ $video->title }}</strong>.</p>
|
||||
|
||||
<p class="email-text">Hi <strong style="color:#f1f1f1;">{{ $recipient->name }}</strong>,</p>
|
||||
<p class="email-text">
|
||||
<strong style="color:#f1f1f1;">{{ $liker->name }}</strong> liked your comment:
|
||||
</p>
|
||||
|
||||
<div class="email-infobox">
|
||||
<div class="email-infobox-label">Your Comment</div>
|
||||
<div style="color:#e0e0e0;font-size:14px;line-height:1.6;padding:4px 0;">
|
||||
"{{ \Illuminate\Support\Str::limit(strip_tags($comment->body), 200) }}"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="email-btn-wrap">
|
||||
<a href="{{ route('videos.show', $video) }}" class="email-btn">▶ View Video</a>
|
||||
</div>
|
||||
|
||||
<hr class="email-divider">
|
||||
<p class="email-note">You can manage your notification preferences in your <a href="{{ route('settings') }}">account settings</a>.</p>
|
||||
|
||||
</x-emails.layout>
|
||||
27
resources/views/emails/new-comment.blade.php
Normal file
27
resources/views/emails/new-comment.blade.php
Normal file
@ -0,0 +1,27 @@
|
||||
<x-emails.layout subject="{{ $commenter->name }} commented on your video">
|
||||
|
||||
<div style="width:64px;height:64px;border-radius:50%;background:rgba(230,30,30,.12);border:1px solid rgba(230,30,30,.25);margin:0 auto 24px;text-align:center;line-height:64px;font-size:28px;">💬</div>
|
||||
|
||||
<h1 class="email-title">New comment on your video</h1>
|
||||
<p class="email-subtitle">Someone left a comment on <strong style="color:#f1f1f1;">{{ $video->title }}</strong>.</p>
|
||||
|
||||
<p class="email-text">Hi <strong style="color:#f1f1f1;">{{ $recipient->name }}</strong>,</p>
|
||||
<p class="email-text">
|
||||
<strong style="color:#f1f1f1;">{{ $commenter->name }}</strong> commented on your video:
|
||||
</p>
|
||||
|
||||
<div class="email-infobox">
|
||||
<div class="email-infobox-label">Comment</div>
|
||||
<div style="color:#e0e0e0;font-size:14px;line-height:1.6;padding:4px 0;">
|
||||
"{{ \Illuminate\Support\Str::limit(strip_tags($comment->body), 200) }}"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="email-btn-wrap">
|
||||
<a href="{{ route('videos.show', $video) }}" class="email-btn">💬 View Comment</a>
|
||||
</div>
|
||||
|
||||
<hr class="email-divider">
|
||||
<p class="email-note">You can manage your notification preferences in your <a href="{{ route('settings') }}">account settings</a>.</p>
|
||||
|
||||
</x-emails.layout>
|
||||
26
resources/views/emails/new-post-from-sub.blade.php
Normal file
26
resources/views/emails/new-post-from-sub.blade.php
Normal file
@ -0,0 +1,26 @@
|
||||
<x-emails.layout subject="{{ $author->name }} posted something new">
|
||||
|
||||
<div style="width:64px;height:64px;border-radius:50%;background:rgba(230,30,30,.12);border:1px solid rgba(230,30,30,.25);margin:0 auto 24px;text-align:center;line-height:64px;font-size:28px;">📝</div>
|
||||
|
||||
<h1 class="email-title">New post from {{ $author->name }}</h1>
|
||||
<p class="email-subtitle">A channel you follow shared something.</p>
|
||||
|
||||
<p class="email-text">Hi <strong style="color:#f1f1f1;">{{ $recipient->name }}</strong>,</p>
|
||||
|
||||
@if($post->body)
|
||||
<div class="email-infobox">
|
||||
<div class="email-infobox-label">Post</div>
|
||||
<div style="color:#e0e0e0;font-size:14px;line-height:1.6;padding:4px 0;">
|
||||
"{{ \Illuminate\Support\Str::limit(strip_tags($post->body), 300) }}"
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="email-btn-wrap">
|
||||
<a href="{{ route('channel', $author->channel) }}" class="email-btn">📝 View Post</a>
|
||||
</div>
|
||||
|
||||
<hr class="email-divider">
|
||||
<p class="email-note">You can manage your notification preferences in your <a href="{{ route('settings') }}">account settings</a>.</p>
|
||||
|
||||
</x-emails.layout>
|
||||
27
resources/views/emails/new-reply.blade.php
Normal file
27
resources/views/emails/new-reply.blade.php
Normal file
@ -0,0 +1,27 @@
|
||||
<x-emails.layout subject="{{ $replier->name }} replied to your comment">
|
||||
|
||||
<div style="width:64px;height:64px;border-radius:50%;background:rgba(230,30,30,.12);border:1px solid rgba(230,30,30,.25);margin:0 auto 24px;text-align:center;line-height:64px;font-size:28px;">↩️</div>
|
||||
|
||||
<h1 class="email-title">Someone replied to your comment</h1>
|
||||
<p class="email-subtitle">On the video <strong style="color:#f1f1f1;">{{ $video->title }}</strong>.</p>
|
||||
|
||||
<p class="email-text">Hi <strong style="color:#f1f1f1;">{{ $recipient->name }}</strong>,</p>
|
||||
<p class="email-text">
|
||||
<strong style="color:#f1f1f1;">{{ $replier->name }}</strong> replied to your comment:
|
||||
</p>
|
||||
|
||||
<div class="email-infobox">
|
||||
<div class="email-infobox-label">Reply</div>
|
||||
<div style="color:#e0e0e0;font-size:14px;line-height:1.6;padding:4px 0;">
|
||||
"{{ \Illuminate\Support\Str::limit(strip_tags($reply->body), 200) }}"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="email-btn-wrap">
|
||||
<a href="{{ route('videos.show', $video) }}" class="email-btn">↩️ View Reply</a>
|
||||
</div>
|
||||
|
||||
<hr class="email-divider">
|
||||
<p class="email-note">You can manage your notification preferences in your <a href="{{ route('settings') }}">account settings</a>.</p>
|
||||
|
||||
</x-emails.layout>
|
||||
21
resources/views/emails/new-subscriber.blade.php
Normal file
21
resources/views/emails/new-subscriber.blade.php
Normal file
@ -0,0 +1,21 @@
|
||||
<x-emails.layout subject="{{ $subscriber->name }} subscribed to your channel">
|
||||
|
||||
<div style="width:64px;height:64px;border-radius:50%;background:rgba(230,30,30,.12);border:1px solid rgba(230,30,30,.25);margin:0 auto 24px;text-align:center;line-height:64px;font-size:28px;">🔔</div>
|
||||
|
||||
<h1 class="email-title">You have a new subscriber!</h1>
|
||||
<p class="email-subtitle">Your channel is growing.</p>
|
||||
|
||||
<p class="email-text">Hi <strong style="color:#f1f1f1;">{{ $recipient->name }}</strong>,</p>
|
||||
<p class="email-text">
|
||||
<strong style="color:#f1f1f1;">{{ $subscriber->name }}</strong> just subscribed to your channel.
|
||||
Keep creating great content!
|
||||
</p>
|
||||
|
||||
<div class="email-btn-wrap">
|
||||
<a href="{{ route('channel', $subscriber->channel) }}" class="email-btn">👤 View Their Profile</a>
|
||||
</div>
|
||||
|
||||
<hr class="email-divider">
|
||||
<p class="email-note">You can manage your notification preferences in your <a href="{{ route('settings') }}">account settings</a>.</p>
|
||||
|
||||
</x-emails.layout>
|
||||
44
resources/views/emails/new-video-from-sub.blade.php
Normal file
44
resources/views/emails/new-video-from-sub.blade.php
Normal file
@ -0,0 +1,44 @@
|
||||
<x-emails.layout subject="{{ $uploader->name }} uploaded a new video">
|
||||
|
||||
<div style="width:64px;height:64px;border-radius:50%;background:rgba(230,30,30,.12);border:1px solid rgba(230,30,30,.25);margin:0 auto 24px;text-align:center;line-height:64px;font-size:28px;">🎬</div>
|
||||
|
||||
<h1 class="email-title">New video from {{ $uploader->name }}</h1>
|
||||
<p class="email-subtitle">A channel you follow just posted.</p>
|
||||
|
||||
<p class="email-text">Hi <strong style="color:#f1f1f1;">{{ $recipient->name }}</strong>,</p>
|
||||
<p class="email-text">
|
||||
<strong style="color:#f1f1f1;">{{ $uploader->name }}</strong> uploaded a new video:
|
||||
<strong style="color:#f1f1f1;">"{{ $video->title }}"</strong>
|
||||
</p>
|
||||
|
||||
@if($video->thumbnail)
|
||||
<div class="email-thumb-wrap">
|
||||
<a href="{{ route('videos.show', $video) }}">
|
||||
<img src="{{ route('media.thumbnail', $video->thumbnail) }}" alt="{{ $video->title }}">
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="email-infobox">
|
||||
@if($video->description)
|
||||
<div class="email-inforow">
|
||||
<span class="email-inforow-key">Description</span>
|
||||
<span class="email-inforow-val">{{ \Illuminate\Support\Str::limit($video->description, 100) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($video->formatted_duration)
|
||||
<div class="email-inforow">
|
||||
<span class="email-inforow-key">Duration</span>
|
||||
<span class="email-inforow-val">{{ $video->formatted_duration }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="email-btn-wrap">
|
||||
<a href="{{ route('videos.show', $video) }}" class="email-btn">▶ Watch Now</a>
|
||||
</div>
|
||||
|
||||
<hr class="email-divider">
|
||||
<p class="email-note">You can manage your notification preferences in your <a href="{{ route('settings') }}">account settings</a>.</p>
|
||||
|
||||
</x-emails.layout>
|
||||
28
resources/views/emails/video-liked.blade.php
Normal file
28
resources/views/emails/video-liked.blade.php
Normal file
@ -0,0 +1,28 @@
|
||||
<x-emails.layout subject="{{ $liker->name }} liked your video">
|
||||
|
||||
<div style="width:64px;height:64px;border-radius:50%;background:rgba(230,30,30,.12);border:1px solid rgba(230,30,30,.25);margin:0 auto 24px;text-align:center;line-height:64px;font-size:28px;">❤️</div>
|
||||
|
||||
<h1 class="email-title">Someone liked your video!</h1>
|
||||
|
||||
<p class="email-text">Hi <strong style="color:#f1f1f1;">{{ $recipient->name }}</strong>,</p>
|
||||
<p class="email-text">
|
||||
<strong style="color:#f1f1f1;">{{ $liker->name }}</strong> liked your video
|
||||
<strong style="color:#f1f1f1;">"{{ $video->title }}"</strong>.
|
||||
</p>
|
||||
|
||||
@if($video->thumbnail)
|
||||
<div class="email-thumb-wrap">
|
||||
<a href="{{ route('videos.show', $video) }}">
|
||||
<img src="{{ route('media.thumbnail', $video->thumbnail) }}" alt="{{ $video->title }}">
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="email-btn-wrap">
|
||||
<a href="{{ route('videos.show', $video) }}" class="email-btn">▶ Watch Video</a>
|
||||
</div>
|
||||
|
||||
<hr class="email-divider">
|
||||
<p class="email-note">You can manage your notification preferences in your <a href="{{ route('settings') }}">account settings</a>.</p>
|
||||
|
||||
</x-emails.layout>
|
||||
63
resources/views/emails/weekly-digest.blade.php
Normal file
63
resources/views/emails/weekly-digest.blade.php
Normal file
@ -0,0 +1,63 @@
|
||||
<x-emails.layout subject="Your weekly TAKEONE summary 📊">
|
||||
|
||||
<div style="width:64px;height:64px;border-radius:50%;background:rgba(230,30,30,.12);border:1px solid rgba(230,30,30,.25);margin:0 auto 24px;text-align:center;line-height:64px;font-size:28px;">📊</div>
|
||||
|
||||
<h1 class="email-title">Your week in numbers</h1>
|
||||
<p class="email-subtitle">Here's how your channel did in the last 7 days.</p>
|
||||
|
||||
<p class="email-text">Hi <strong style="color:#f1f1f1;">{{ $creator->name }}</strong>,</p>
|
||||
|
||||
<div class="email-infobox">
|
||||
<div class="email-infobox-label">This Week's Stats</div>
|
||||
<div class="email-inforow">
|
||||
<span class="email-inforow-key">👀 Views</span>
|
||||
<span class="email-inforow-val">{{ number_format($stats['views']) }}</span>
|
||||
</div>
|
||||
<div class="email-inforow">
|
||||
<span class="email-inforow-key">❤️ Likes</span>
|
||||
<span class="email-inforow-val">{{ number_format($stats['likes']) }}</span>
|
||||
</div>
|
||||
<div class="email-inforow">
|
||||
<span class="email-inforow-key">🔔 New Subscribers</span>
|
||||
<span class="email-inforow-val">{{ number_format($stats['new_subs']) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($stats['top_video'])
|
||||
<p class="email-text" style="margin-top:20px;">Your top video this week:</p>
|
||||
|
||||
@if($stats['top_video']->thumbnail)
|
||||
<div class="email-thumb-wrap">
|
||||
<a href="{{ route('videos.show', $stats['top_video']) }}">
|
||||
<img src="{{ route('media.thumbnail', $stats['top_video']->thumbnail) }}" alt="{{ $stats['top_video']->title }}">
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="email-infobox">
|
||||
<div class="email-inforow">
|
||||
<span class="email-inforow-key">Title</span>
|
||||
<span class="email-inforow-val">{{ $stats['top_video']->title }}</span>
|
||||
</div>
|
||||
<div class="email-inforow">
|
||||
<span class="email-inforow-key">Views this week</span>
|
||||
<span class="email-inforow-val">{{ number_format($stats['top_video']->week_views ?? 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="email-btn-wrap">
|
||||
<a href="{{ route('videos.show', $stats['top_video']) }}" class="email-btn">▶ Watch Top Video</a>
|
||||
</div>
|
||||
@else
|
||||
<p class="email-text" style="text-align:center;color:var(--text-secondary);">
|
||||
Upload your first video to start tracking performance!
|
||||
</p>
|
||||
<div class="email-btn-wrap">
|
||||
<a href="{{ route('home') }}" class="email-btn">🎬 Go to TAKEONE</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<hr class="email-divider">
|
||||
<p class="email-note">You can manage your notification preferences in your <a href="{{ route('settings') }}">account settings</a>.</p>
|
||||
|
||||
</x-emails.layout>
|
||||
@ -1226,20 +1226,27 @@
|
||||
case 'new_comment': return actor + ' commented on your video ' + title + ':' + preview;
|
||||
case 'new_reply': return actor + ' replied to your comment:' + preview;
|
||||
case 'comment_like':return actor + ' liked your comment on ' + title;
|
||||
case 'new_user': return '🎉 ' + actor + ' just joined TAKEONE!';
|
||||
default: return actor + ' uploaded a new video: ' + title;
|
||||
case 'new_user': return '🎉 ' + actor + ' just joined TAKEONE!';
|
||||
case 'new_subscriber': return '🔔 ' + actor + ' subscribed to your channel';
|
||||
case 'video_like': return '❤️ ' + actor + ' liked your video ' + title;
|
||||
case 'new_post': return '📝 ' + actor + ' posted something new';
|
||||
default: return actor + ' uploaded a new video: ' + title;
|
||||
}
|
||||
}
|
||||
|
||||
function notifHref(d) {
|
||||
if (d.type === 'new_user') return '/channel/' + encodeURIComponent(d.user_channel || '');
|
||||
if (d.type === 'new_user') return '/channel/' + encodeURIComponent(d.user_channel || '');
|
||||
if (d.type === 'new_subscriber') return '/channel/' + encodeURIComponent(d.actor_channel || '');
|
||||
if (d.type === 'new_post') return '/channel/' + encodeURIComponent(d.author_channel || '');
|
||||
return '/videos/' + escHtml(d.video_route_key || '');
|
||||
}
|
||||
|
||||
function notifThumb(d) {
|
||||
if (d.type === 'new_user') {
|
||||
return d.user_avatar
|
||||
? '<img class="yt-notif-thumb" src="' + escHtml(d.user_avatar) + '" alt="" loading="lazy" onerror="notifThumbFallback(this)" style="border-radius:50%">'
|
||||
var avatarTypes = ['new_user', 'new_subscriber', 'new_post'];
|
||||
if (avatarTypes.indexOf(d.type) !== -1) {
|
||||
var av = d.user_avatar || d.actor_avatar || d.author_avatar || '';
|
||||
return av
|
||||
? '<img class="yt-notif-thumb" src="' + escHtml(av) + '" alt="" loading="lazy" onerror="notifThumbFallback(this)" style="border-radius:50%">'
|
||||
: '<div class="yt-notif-thumb-placeholder"><i class="bi bi-person-circle"></i></div>';
|
||||
}
|
||||
return d.video_thumbnail
|
||||
|
||||
@ -1076,6 +1076,42 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.ch-settings-card:last-child { margin-bottom: 0; }
|
||||
|
||||
/* ── Notification preference rows ── */
|
||||
.notif-section-label {
|
||||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: .07em; color: var(--text-secondary);
|
||||
margin: 0 0 12px; display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.notif-pref-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,.04);
|
||||
}
|
||||
.notif-pref-row:last-of-type { border-bottom: none; }
|
||||
.notif-pref-label { font-size: 14px; color: var(--text-primary); }
|
||||
.notif-pref-note {
|
||||
font-size: 12px; color: var(--text-secondary);
|
||||
margin-top: 16px; line-height: 1.5;
|
||||
}
|
||||
.notif-toggle-wrap {
|
||||
position: relative; width: 42px; height: 24px;
|
||||
flex-shrink: 0; cursor: pointer;
|
||||
}
|
||||
.notif-toggle-input { opacity: 0; width: 0; height: 0; position: absolute; }
|
||||
.notif-toggle-slider {
|
||||
position: absolute; inset: 0;
|
||||
background: rgba(255,255,255,.15); border-radius: 24px;
|
||||
transition: background .2s;
|
||||
}
|
||||
.notif-toggle-slider::before {
|
||||
content: ''; position: absolute;
|
||||
width: 18px; height: 18px; border-radius: 50%;
|
||||
background: #fff; top: 3px; left: 3px;
|
||||
transition: transform .2s;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.4);
|
||||
}
|
||||
.notif-toggle-input:checked + .notif-toggle-slider { background: var(--brand-red); }
|
||||
.notif-toggle-input:checked + .notif-toggle-slider::before { transform: translateX(18px); }
|
||||
.ch-settings-card-title {
|
||||
font-size: 13px; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: .06em;
|
||||
@ -2396,6 +2432,85 @@ if ($oldLinks) {
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($isOwner)
|
||||
@php
|
||||
$notifPrefs = $user->notification_preferences ?? [];
|
||||
$notifDefs = \App\Models\User::notifDefaults();
|
||||
function notifVal(array $prefs, array $defs, string $key): bool {
|
||||
return isset($prefs[$key]) ? (bool)$prefs[$key] : ($defs[$key] ?? true);
|
||||
}
|
||||
@endphp
|
||||
<div class="ch-settings-card" id="notifPrefsCard">
|
||||
<div class="ch-settings-card-title"><i class="bi bi-bell-fill"></i> Notification Preferences</div>
|
||||
|
||||
{{-- Bell (In-App) --}}
|
||||
<div class="notif-section-label"><i class="bi bi-bell"></i> In-App Notifications</div>
|
||||
|
||||
@php
|
||||
$bellPrefs = [
|
||||
['notif_new_comment', 'New comment on my video'],
|
||||
['notif_new_reply', 'Reply to my comment'],
|
||||
['notif_comment_like', 'Someone liked my comment'],
|
||||
['notif_video_like', 'Someone liked my video'],
|
||||
['notif_new_subscriber', 'New subscriber'],
|
||||
['notif_new_video_from_sub', 'New video from channels I follow'],
|
||||
['notif_new_post_from_sub', 'New post from channels I follow'],
|
||||
];
|
||||
if ($user->isSuperAdmin()) {
|
||||
$bellPrefs[] = ['notif_new_user_reg', 'New user registration'];
|
||||
}
|
||||
@endphp
|
||||
|
||||
@foreach($bellPrefs as [$key, $label])
|
||||
<div class="notif-pref-row">
|
||||
<span class="notif-pref-label">{{ $label }}</span>
|
||||
<label class="notif-toggle-wrap">
|
||||
<input type="checkbox" class="notif-toggle-input"
|
||||
data-key="{{ $key }}"
|
||||
{{ notifVal($notifPrefs, $notifDefs, $key) ? 'checked' : '' }}
|
||||
onchange="saveNotifPref('{{ $key }}', this.checked)">
|
||||
<span class="notif-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="notif-section-label" style="margin-top:20px;"><i class="bi bi-envelope"></i> Email Notifications</div>
|
||||
|
||||
@php
|
||||
$emailPrefs = [
|
||||
['email_new_comment', 'New comment on my video'],
|
||||
['email_new_reply', 'Reply to my comment'],
|
||||
['email_comment_like', 'Someone liked my comment'],
|
||||
['email_video_like', 'Someone liked my video'],
|
||||
['email_new_subscriber', 'New subscriber'],
|
||||
['email_new_video_from_sub', 'New video from channels I follow'],
|
||||
['email_new_post_from_sub', 'New post from channels I follow'],
|
||||
['email_video_processed', 'My video finished processing'],
|
||||
['email_weekly_digest', 'Weekly activity digest (every Monday)'],
|
||||
];
|
||||
if ($user->isSuperAdmin()) {
|
||||
$emailPrefs[] = ['email_new_user_reg', 'New user registration'];
|
||||
}
|
||||
@endphp
|
||||
|
||||
@foreach($emailPrefs as [$key, $label])
|
||||
<div class="notif-pref-row">
|
||||
<span class="notif-pref-label">{{ $label }}</span>
|
||||
<label class="notif-toggle-wrap">
|
||||
<input type="checkbox" class="notif-toggle-input"
|
||||
data-key="{{ $key }}"
|
||||
{{ notifVal($notifPrefs, $notifDefs, $key) ? 'checked' : '' }}
|
||||
onchange="saveNotifPref('{{ $key }}', this.checked)">
|
||||
<span class="notif-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<p class="notif-pref-note">Security emails (verification, password reset) are always sent and cannot be disabled.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="ch-settings-card">
|
||||
<div class="ch-settings-card-title"><i class="bi bi-info-circle-fill"></i> Account</div>
|
||||
<div class="ch-settings-field">
|
||||
@ -2957,6 +3072,27 @@ function onBannerSaved(url) {
|
||||
if (banner) banner.insertBefore(newImg, banner.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Notification preference toggle ────────────────────────────────────────
|
||||
function saveNotifPref(key, value) {
|
||||
var csrf = document.querySelector('meta[name="csrf-token"]');
|
||||
fetch('{{ route("settings.notifications") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrf ? csrf.getAttribute('content') : '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ key: key, value: value }),
|
||||
}).then(function(r) {
|
||||
if (!r.ok) throw new Error('save failed');
|
||||
}).catch(function() {
|
||||
// Revert toggle on failure
|
||||
var el = document.querySelector('[data-key="' + key + '"]');
|
||||
if (el) el.checked = !value;
|
||||
if (window.showToast) showToast('Could not save preference.', 'error');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@if($isOwner && !$preview)
|
||||
|
||||
@ -95,6 +95,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
// Settings
|
||||
Route::get('/settings', [UserController::class, 'settings'])->name('settings');
|
||||
Route::put('/settings', [UserController::class, 'updateSettings'])->name('settings.update');
|
||||
Route::post('/settings/notifications', [UserController::class, 'updateNotificationPreferences'])->name('settings.notifications');
|
||||
|
||||
// History & Liked
|
||||
Route::get('/history', [UserController::class, 'history'])->name('history');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user