This commit is contained in:
Stefanescu Mihai
2026-03-19 19:47:06 +02:00
parent e8d2728d57
commit eb735a9a98
19 changed files with 860 additions and 9 deletions
@@ -21,6 +21,9 @@
<x-nav-link :href="route('projects.issues.index', $currentProject)" :active="request()->routeIs('projects.issues.*')">
Issues
</x-nav-link>
<x-nav-link :href="route('projects.alerts.edit', $currentProject)" :active="request()->routeIs('projects.alerts.*')">
Alerts
</x-nav-link>
<x-nav-link :href="route('projects.settings', $currentProject)" :active="request()->routeIs('projects.settings')">
Settings
</x-nav-link>
@@ -55,6 +58,19 @@
</x-dropdown>
@endif
{{-- Notification Bell --}}
@php($unreadCount = Auth::user()->unreadNotifications()->count())
<a href="{{ route('notifications.index') }}" class="relative p-2 text-gray-400 hover:text-gray-600 transition" title="Notifications">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
</svg>
@if($unreadCount > 0)
<span class="absolute -top-0.5 -right-0.5 inline-flex items-center justify-center px-1.5 py-0.5 text-[10px] font-bold leading-none text-white bg-red-500 rounded-full">
{{ $unreadCount > 99 ? '99+' : $unreadCount }}
</span>
@endif
</a>
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
@@ -0,0 +1,75 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Notifications</h2>
@if(Auth::user()->unreadNotifications->count() > 0)
<form method="POST" action="{{ route('notifications.mark-all-read') }}">
@csrf
<x-secondary-button type="submit">Mark All as Read</x-secondary-button>
</form>
@endif
</div>
</x-slot>
<div class="py-8">
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
@if(session('status'))
<div class="mb-4 bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-md text-sm">
{{ session('status') }}
</div>
@endif
@if($notifications->isEmpty())
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-12 text-center text-gray-500">
No notifications yet.
</div>
@else
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg divide-y divide-gray-200">
@foreach($notifications as $notification)
@php
$data = $notification->data;
$isUnread = $notification->read_at === null;
$typeLabel = match($data['type'] ?? '') {
'new_issue' => 'New Issue',
'regression' => 'Regression',
'spike' => 'Spike',
default => 'Notification',
};
$typeColor = match($data['type'] ?? '') {
'new_issue' => 'bg-blue-100 text-blue-800',
'regression' => 'bg-orange-100 text-orange-800',
'spike' => 'bg-red-100 text-red-800',
default => 'bg-gray-100 text-gray-600',
};
@endphp
<a href="{{ route('notifications.read', $notification->id) }}"
class="block px-6 py-4 hover:bg-gray-50 transition {{ $isUnread ? 'bg-indigo-50/30' : '' }}">
<div class="flex items-start gap-3">
@if($isUnread)
<span class="mt-2 w-2 h-2 rounded-full bg-indigo-500 shrink-0"></span>
@else
<span class="mt-2 w-2 h-2 shrink-0"></span>
@endif
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="inline-flex px-2 py-0.5 rounded-full text-[10px] font-semibold {{ $typeColor }}">{{ $typeLabel }}</span>
<span class="text-xs text-gray-400">{{ $data['project_name'] ?? '' }}</span>
<span class="text-xs text-gray-400 ml-auto">{{ $notification->created_at->diffForHumans() }}</span>
</div>
<p class="text-sm text-gray-900 truncate">{{ $data['issue_title'] ?? '' }}</p>
@if(($data['type'] ?? '') === 'spike')
<p class="text-xs text-gray-500 mt-0.5">{{ $data['event_count'] ?? 0 }} events in {{ $data['window_minutes'] ?? 0 }}m</p>
@endif
</div>
</div>
</a>
@endforeach
</div>
<div class="mt-4">
{{ $notifications->links() }}
</div>
@endif
</div>
</div>
</x-app-layout>
+135
View File
@@ -0,0 +1,135 @@
<x-app-layout>
@php($currentProject = $project)
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">{{ $project->name }} &mdash; Alert Rules</h2>
</x-slot>
<div class="py-8">
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8 space-y-6">
@if(session('status'))
<div class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-md text-sm">
{{ session('status') }}
</div>
@endif
{{-- Alert Rules --}}
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-1">Alert Rules</h3>
<p class="text-sm text-gray-500 mb-6">Configure when notifications are sent for this project.</p>
<form method="POST" action="{{ route('projects.alerts.update', $project) }}" class="space-y-6">
@csrf
@method('PATCH')
{{-- New Issue --}}
<div class="flex items-center justify-between py-3 border-b border-gray-100">
<div>
<div class="font-medium text-gray-900">New Issue</div>
<div class="text-sm text-gray-500">Notify when a new issue is first seen.</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="hidden" name="rules[new_issue][enabled]" value="0">
<input type="checkbox" name="rules[new_issue][enabled]" value="1" class="sr-only peer" {{ ($rules['new_issue']->enabled ?? true) ? 'checked' : '' }}>
<div class="w-11 h-6 bg-gray-200 peer-focus:ring-2 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
{{-- Regression --}}
<div class="flex items-center justify-between py-3 border-b border-gray-100">
<div>
<div class="font-medium text-gray-900">Regression</div>
<div class="text-sm text-gray-500">Notify when a resolved issue reappears.</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="hidden" name="rules[regression][enabled]" value="0">
<input type="checkbox" name="rules[regression][enabled]" value="1" class="sr-only peer" {{ ($rules['regression']->enabled ?? true) ? 'checked' : '' }}>
<div class="w-11 h-6 bg-gray-200 peer-focus:ring-2 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
{{-- Spike --}}
<div class="py-3 border-b border-gray-100">
<div class="flex items-center justify-between">
<div>
<div class="font-medium text-gray-900">Event Spike</div>
<div class="text-sm text-gray-500">Notify when an issue receives many events in a short window.</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="hidden" name="rules[spike][enabled]" value="0">
<input type="checkbox" name="rules[spike][enabled]" value="1" class="sr-only peer" {{ ($rules['spike']->enabled ?? true) ? 'checked' : '' }}>
<div class="w-11 h-6 bg-gray-200 peer-focus:ring-2 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div class="mt-4 flex items-center gap-4 text-sm">
<div class="flex items-center gap-2">
<x-input-label for="spike_threshold" value="Threshold" />
<x-text-input id="spike_threshold" name="rules[spike][threshold]" type="number" min="1" class="w-20"
:value="$rules['spike']->config['threshold'] ?? 10" />
<span class="text-gray-500">events in</span>
</div>
<div class="flex items-center gap-2">
<x-text-input id="spike_window" name="rules[spike][window_minutes]" type="number" min="1" class="w-20"
:value="$rules['spike']->config['window_minutes'] ?? 5" />
<span class="text-gray-500">minutes</span>
</div>
</div>
</div>
<x-primary-button>Save Alert Rules</x-primary-button>
</form>
</div>
{{-- Personal Notification Preferences --}}
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-1">Your Notification Preferences</h3>
<p class="text-sm text-gray-500 mb-6">Control how you receive notifications for this project.</p>
<form method="POST" action="{{ route('projects.alerts.preference', $project) }}" class="space-y-6">
@csrf
@method('PATCH')
<div class="flex items-center justify-between py-3 border-b border-gray-100">
<div>
<div class="font-medium text-gray-900">Email Notifications</div>
<div class="text-sm text-gray-500">Receive alert emails for this project.</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="hidden" name="email_enabled" value="0">
<input type="checkbox" name="email_enabled" value="1" class="sr-only peer" {{ $preference->email_enabled ? 'checked' : '' }}>
<div class="w-11 h-6 bg-gray-200 peer-focus:ring-2 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div class="flex items-center justify-between py-3 border-b border-gray-100">
<div>
<div class="font-medium text-gray-900">In-App Notifications</div>
<div class="text-sm text-gray-500">Show notifications in the bell icon.</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="hidden" name="database_enabled" value="0">
<input type="checkbox" name="database_enabled" value="1" class="sr-only peer" {{ $preference->database_enabled ? 'checked' : '' }}>
<div class="w-11 h-6 bg-gray-200 peer-focus:ring-2 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div class="flex items-center justify-between py-3 border-b border-gray-100">
<div>
<div class="font-medium text-red-600">Mute All</div>
<div class="text-sm text-gray-500">Completely suppress all notifications from this project.</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="hidden" name="muted" value="0">
<input type="checkbox" name="muted" value="1" class="sr-only peer" {{ $preference->muted ? 'checked' : '' }}>
<div class="w-11 h-6 bg-gray-200 peer-focus:ring-2 peer-focus:ring-red-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-red-600"></div>
</label>
</div>
<x-primary-button>Save Preferences</x-primary-button>
</form>
</div>
</div>
</div>
</x-app-layout>