lots of progress

This commit is contained in:
Doar Mihai
2026-01-17 13:46:20 +02:00
parent ab5d3cae98
commit 030a105992
77 changed files with 8507 additions and 20 deletions
+30
View File
@@ -1,4 +1,34 @@
import 'bootstrap';
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// import Pusher from 'pusher-js';
// window.Pusher = Pusher;
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: import.meta.env.VITE_PUSHER_APP_KEY,
// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
// wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
// enabledTransports: ['ws', 'wss'],
// });
+7
View File
@@ -0,0 +1,7 @@
// Body
$body-bg: #f8fafc;
// Typography
$font-family-sans-serif: 'Nunito', sans-serif;
$font-size-base: 0.9rem;
$line-height-base: 1.6;
+8
View File
@@ -0,0 +1,8 @@
// Fonts
@import url('https://fonts.bunny.net/css?family=Nunito');
// Variables
@import 'variables';
// Bootstrap
@import 'bootstrap/scss/bootstrap';
+115
View File
@@ -0,0 +1,115 @@
@extends('layouts.app')
@section('title', 'Alerts')
@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1><i class="bi bi-bell"></i> Alerts</h1>
<p class="text-muted mb-0">Notification history for all your sites</p>
</div>
</div>
<!-- Filter Tabs -->
<ul class="nav nav-tabs mb-4" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="all-tab" data-bs-toggle="tab" data-bs-target="#all" type="button" role="tab">
All ({{ $notifications->total() }})
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="unread-tab" data-bs-toggle="tab" data-bs-target="#unread" type="button" role="tab">
Unread ({{ $unreadCount }})
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="read-tab" data-bs-toggle="tab" data-bs-target="#read" type="button" role="tab">
Read ({{ $readCount }})
</button>
</li>
</ul>
<!-- Notifications List -->
<div class="card">
<div class="card-body">
@if($notifications->isEmpty())
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i> No alerts yet. You'll receive notifications when issues are detected with your sites.
</div>
@else
<div class="list-group list-group-flush">
@foreach($notifications as $notification)
<div class="list-group-item {{ !$notification->is_read ? 'bg-light' : '' }} border-start border-{{ $notification->type === 'uptime' ? 'danger' : ($notification->type === 'ssl' ? 'warning' : 'info') }} border-3">
<div class="d-flex w-100 justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-1">
@if($notification->type === 'uptime')
<i class="bi bi-exclamation-triangle-fill text-danger me-2"></i>
<span class="badge bg-danger me-2">Uptime Alert</span>
@elseif($notification->type === 'ssl')
<i class="bi bi-shield-exclamation text-warning me-2"></i>
<span class="badge bg-warning me-2">SSL Alert</span>
@else
<i class="bi bi-link-45deg text-info me-2"></i>
<span class="badge bg-info me-2">Broken Links</span>
@endif
@if(!$notification->is_read)
<span class="badge bg-primary">New</span>
@endif
</div>
<p class="mb-1">{{ $notification->message }}</p>
<small class="text-muted">
<i class="bi bi-clock"></i> {{ $notification->created_at->diffForHumans() }}
</small>
</div>
<div class="ms-3">
@if(!$notification->is_read)
<form action="{{ route('alerts.markAsRead', $notification) }}" method="POST" class="d-inline">
@csrf
@method('PATCH')
<button type="submit" class="btn btn-sm btn-outline-primary" title="Mark as read">
<i class="bi bi-check"></i>
</button>
</form>
@endif
<form action="{{ route('alerts.destroy', $notification) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete"
onclick="return confirm('Are you sure you want to delete this alert?')">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
</div>
@endforeach
</div>
<div class="mt-3">
{{ $notifications->links() }}
</div>
@endif
</div>
@if($notifications->isNotEmpty())
<div class="card-footer">
<div class="d-flex gap-2">
<form action="{{ route('alerts.markAllAsRead') }}" method="POST">
@csrf
@method('PATCH')
<button type="submit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-check-all"></i> Mark All as Read
</button>
</form>
<form action="{{ route('alerts.deleteRead') }}" method="POST" onsubmit="return confirm('Are you sure you want to delete all read alerts?')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i> Delete All Read
</button>
</form>
</div>
</div>
@endif
</div>
@endsection
+73
View File
@@ -0,0 +1,73 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Login') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('login') }}">
@csrf
<div class="row mb-3">
<label for="email" class="col-md-4 col-form-label text-md-end">{{ __('Email Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="password" class="col-md-4 col-form-label text-md-end">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<div class="col-md-6 offset-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
<label class="form-check-label" for="remember">
{{ __('Remember Me') }}
</label>
</div>
</div>
</div>
<div class="row mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Login') }}
</button>
@if (Route::has('password.request'))
<a class="btn btn-link" href="{{ route('password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
@endif
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,49 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Confirm Password') }}</div>
<div class="card-body">
{{ __('Please confirm your password before continuing.') }}
<form method="POST" action="{{ route('password.confirm') }}">
@csrf
<div class="row mb-3">
<label for="password" class="col-md-4 col-form-label text-md-end">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Confirm Password') }}
</button>
@if (Route::has('password.request'))
<a class="btn btn-link" href="{{ route('password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
@endif
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,47 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Reset Password') }}</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('password.email') }}">
@csrf
<div class="row mb-3">
<label for="email" class="col-md-4 col-form-label text-md-end">{{ __('Email Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Send Password Reset Link') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,65 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Reset Password') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('password.update') }}">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<div class="row mb-3">
<label for="email" class="col-md-4 col-form-label text-md-end">{{ __('Email Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ $email ?? old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="password" class="col-md-4 col-form-label text-md-end">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="password-confirm" class="col-md-4 col-form-label text-md-end">{{ __('Confirm Password') }}</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
</div>
</div>
<div class="row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Reset Password') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
+77
View File
@@ -0,0 +1,77 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Register') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('register') }}">
@csrf
<div class="row mb-3">
<label for="name" class="col-md-4 col-form-label text-md-end">{{ __('Name') }}</label>
<div class="col-md-6">
<input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name') }}" required autocomplete="name" autofocus>
@error('name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="email" class="col-md-4 col-form-label text-md-end">{{ __('Email Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email">
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="password" class="col-md-4 col-form-label text-md-end">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="password-confirm" class="col-md-4 col-form-label text-md-end">{{ __('Confirm Password') }}</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
</div>
</div>
<div class="row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Register') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
+28
View File
@@ -0,0 +1,28 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Verify Your Email Address') }}</div>
<div class="card-body">
@if (session('resent'))
<div class="alert alert-success" role="alert">
{{ __('A fresh verification link has been sent to your email address.') }}
</div>
@endif
{{ __('Before proceeding, please check your email for a verification link.') }}
{{ __('If you did not receive the email') }},
<form class="d-inline" method="POST" action="{{ route('verification.resend') }}">
@csrf
<button type="submit" class="btn btn-link p-0 m-0 align-baseline">{{ __('click here to request another') }}</button>.
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,20 @@
@props(['type' => 'info', 'dismissible' => false])
@php
$classes = [
'success' => 'alert-success',
'danger' => 'alert-danger',
'warning' => 'alert-warning',
'info' => 'alert-info',
'primary' => 'alert-primary',
'secondary' => 'alert-secondary',
];
$alertClass = $classes[$type] ?? $classes['info'];
@endphp
<div {{ $attributes->merge(['class' => 'alert ' . $alertClass . ($dismissible ? ' alert-dismissible fade show' : '')]) }} role="alert">
{{ $slot }}
@if($dismissible)
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
@endif
</div>
@@ -0,0 +1,5 @@
@props(['color' => 'primary', 'pill' => false])
<span {{ $attributes->merge(['class' => 'badge bg-' . $color . ($pill ? ' rounded-pill' : '')]) }}>
{{ $slot }}
</span>
+19
View File
@@ -0,0 +1,19 @@
@props(['header' => null, 'footer' => null])
<div {{ $attributes->merge(['class' => 'card']) }}>
@if($header)
<div class="card-header">
{{ $header }}
</div>
@endif
<div class="card-body">
{{ $slot }}
</div>
@if($footer)
<div class="card-footer">
{{ $footer }}
</div>
@endif
</div>
@@ -0,0 +1,20 @@
@props(['id', 'title', 'size' => 'md'])
<div class="modal fade" id="{{ $id }}" tabindex="-1" aria-labelledby="{{ $id }}Label" aria-hidden="true">
<div class="modal-dialog modal-{{ $size }}">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="{{ $id }}Label">{{ $title }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ $slot }}
</div>
@isset($footer)
<div class="modal-footer">
{{ $footer }}
</div>
@endisset
</div>
</div>
</div>
@@ -0,0 +1,15 @@
@props(['title', 'value', 'icon', 'color' => 'primary'])
<div {{ $attributes->merge(['class' => 'card']) }}>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">{{ $title }}</h6>
<h2 class="mb-0 text-{{ $color }}">{{ $value }}</h2>
</div>
<div class="text-{{ $color }}" style="font-size: 2rem;">
<i class="bi bi-{{ $icon }}"></i>
</div>
</div>
</div>
</div>
+508
View File
@@ -0,0 +1,508 @@
@extends('layouts.app')
@section('title', 'Dashboard')
@push('styles')
<style>
.stat-card {
border: none;
border-radius: 16px;
padding: 1.5rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--card-gradient-start), var(--card-gradient-end));
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}
.stat-card.primary {
--card-gradient-start: #667eea;
--card-gradient-end: #764ba2;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(118, 75, 162, 0.05));
}
.stat-card.success {
--card-gradient-start: #10b981;
--card-gradient-end: #059669;
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05), rgba(5, 150, 105, 0.05));
}
.stat-card.danger {
--card-gradient-start: #ef4444;
--card-gradient-end: #dc2626;
background: linear-gradient(135deg, rgba(239, 68, 68, 0.05), rgba(220, 38, 38, 0.05));
}
.stat-card.info {
--card-gradient-start: #3b82f6;
--card-gradient-end: #2563eb;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.05), rgba(37, 99, 235, 0.05));
}
.dark .stat-card.primary {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
}
.dark .stat-card.success {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(5, 150, 105, 0.1));
}
.dark .stat-card.danger {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(220, 38, 38, 0.1));
}
.dark .stat-card.info {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(37, 99, 235, 0.1));
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.stat-icon.primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.stat-icon.success {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
.stat-icon.danger {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
}
.stat-icon.info {
background: linear-gradient(135deg, #3b82f6, #2563eb);
color: white;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
line-height: 1;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.875rem;
font-weight: 500;
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.chart-card {
border: none;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.chart-card .card-header {
background: transparent;
border-bottom: 1px solid var(--border-color);
padding: 1.5rem;
border-radius: 16px 16px 0 0;
}
.chart-card .card-body {
padding: 2rem;
}
.table-modern {
border-collapse: separate;
border-spacing: 0;
}
.table-modern thead th {
background: transparent;
border-bottom: 2px solid var(--border-color);
font-weight: 600;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 1rem;
color: var(--text-secondary);
}
.table-modern tbody tr {
transition: all 0.2s ease;
}
.table-modern tbody tr:hover {
background: var(--bg-secondary);
transform: scale(1.01);
}
.table-modern tbody td {
padding: 1.25rem 1rem;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
}
.site-name {
font-weight: 600;
color: var(--text-primary);
font-size: 0.95rem;
}
.site-url {
font-size: 0.8rem;
color: var(--text-secondary);
margin-top: 0.25rem;
}
.badge-modern {
padding: 0.4rem 0.8rem;
border-radius: 8px;
font-weight: 600;
font-size: 0.75rem;
letter-spacing: 0.3px;
}
.btn-action {
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-action:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.empty-state {
padding: 4rem 2rem;
text-align: center;
}
.empty-state-icon {
font-size: 4rem;
opacity: 0.3;
margin-bottom: 1.5rem;
}
.page-header {
margin-bottom: 2rem;
}
.page-title {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.page-subtitle {
color: var(--text-secondary);
font-size: 1rem;
}
</style>
@endpush
@section('content')
<!-- Page Header -->
<div class="page-header">
<h1 class="page-title">Dashboard</h1>
<p class="page-subtitle">Welcome back! Here's an overview of all your monitored sites.</p>
</div>
<!-- Summary Stats -->
<div class="row g-4 mb-4">
<div class="col-md-3 col-sm-6">
<div class="stat-card primary">
<div class="d-flex justify-content-between align-items-center">
<div class="flex-grow-1">
<div class="stat-label">Total Sites</div>
<div class="stat-value">{{ $totalSites }}</div>
</div>
<div class="stat-icon primary">
<i class="bi bi-globe"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="stat-card success">
<div class="d-flex justify-content-between align-items-center">
<div class="flex-grow-1">
<div class="stat-label">Sites Up</div>
<div class="stat-value text-success">{{ $sitesUp }}</div>
</div>
<div class="stat-icon success">
<i class="bi bi-check-circle-fill"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="stat-card danger">
<div class="d-flex justify-content-between align-items-center">
<div class="flex-grow-1">
<div class="stat-label">Sites Down</div>
<div class="stat-value text-danger">{{ $sitesDown }}</div>
</div>
<div class="stat-icon danger">
<i class="bi bi-exclamation-circle-fill"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="stat-card info">
<div class="d-flex justify-content-between align-items-center">
<div class="flex-grow-1">
<div class="stat-label">Avg Uptime</div>
<div class="stat-value text-info">{{ number_format($avgUptime, 1) }}%</div>
</div>
<div class="stat-icon info">
<i class="bi bi-graph-up-arrow"></i>
</div>
</div>
</div>
</div>
</div>
<!-- 7-Day Uptime Chart -->
<div class="card chart-card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0 d-flex align-items-center">
<i class="bi bi-graph-up me-2"></i>
7-Day Uptime Overview
</h5>
</div>
<div class="text-muted small">
<i class="bi bi-clock"></i> Last 7 days
</div>
</div>
<div class="card-body">
@if($sites->isNotEmpty())
<canvas id="weeklyUptimeChart" height="80"></canvas>
@else
<div class="empty-state">
<div class="empty-state-icon">
<i class="bi bi-graph-up"></i>
</div>
<h5>No Data Available</h5>
<p class="text-muted">Add your first site to start monitoring and viewing analytics.</p>
<a href="{{ route('sites.create') }}" class="btn btn-primary mt-3">
<i class="bi bi-plus-circle"></i> Add Your First Site
</a>
</div>
@endif
</div>
</div>
<!-- Sites Table -->
<div class="card chart-card">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0 d-flex align-items-center">
<i class="bi bi-globe me-2"></i>
Your Sites
</h5>
</div>
<a href="{{ route('sites.create') }}" class="btn btn-primary btn-sm btn-action">
<i class="bi bi-plus-circle"></i> Add Site
</a>
</div>
<div class="card-body p-0">
@if($sites->isNotEmpty())
<div class="table-responsive">
<table class="table table-modern mb-0">
<thead>
<tr>
<th>Site</th>
<th>Status</th>
<th>Uptime</th>
<th>Response</th>
<th>SSL</th>
<th>Links</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach($sites as $site)
@php
$latestCheck = $site->uptimeChecks->first();
$latestSsl = $site->sslCertificates->first();
$daysUntilExpiry = $latestSsl ? now()->diffInDays($latestSsl->expires_at, false) : null;
@endphp
<tr>
<td>
<div class="site-name">{{ $site->name }}</div>
<div class="site-url">
<i class="bi bi-link-45deg"></i> {{ Str::limit($site->url, 40) }}
</div>
</td>
<td>
@if(!$latestCheck)
<span class="badge badge-modern bg-secondary">
<i class="bi bi-question-circle"></i> Not Checked
</span>
@elseif($latestCheck->status_code >= 200 && $latestCheck->status_code < 400)
<span class="badge badge-modern bg-success">
<i class="bi bi-check-circle-fill"></i> Online
</span>
@else
<span class="badge badge-modern bg-danger">
<i class="bi bi-x-circle-fill"></i> Down
</span>
@endif
</td>
<td>
@php
$uptime = $site->uptime_percentage ?? 0;
@endphp
<div class="d-flex align-items-center gap-2">
<div class="progress" style="width: 60px; height: 6px;">
<div class="progress-bar bg-{{ $uptime >= 99 ? 'success' : ($uptime >= 95 ? 'warning' : 'danger') }}"
role="progressbar"
style="width: {{ $uptime }}%"></div>
</div>
<span class="small fw-semibold">{{ number_format($uptime, 1) }}%</span>
</div>
</td>
<td>
@if($site->avg_response_time)
<span class="fw-semibold">{{ round($site->avg_response_time) }}</span>
<span class="text-muted small">ms</span>
@else
<span class="text-muted">—</span>
@endif
</td>
<td>
@if($latestSsl)
@if($daysUntilExpiry < 0)
<span class="badge badge-modern bg-danger">
<i class="bi bi-shield-x"></i> Expired
</span>
@elseif($daysUntilExpiry < 30)
<span class="badge badge-modern bg-warning text-dark">
<i class="bi bi-clock-history"></i> {{ $daysUntilExpiry }}d
</span>
@else
<span class="badge badge-modern bg-success">
<i class="bi bi-shield-check"></i> Valid
</span>
@endif
@else
<span class="text-muted">—</span>
@endif
</td>
<td>
@if($site->broken_links_count > 0)
<span class="badge badge-modern bg-danger">
<i class="bi bi-link-45deg"></i> {{ $site->broken_links_count }}
</span>
@else
<span class="badge badge-modern bg-success">
<i class="bi bi-check-circle"></i> 0
</span>
@endif
</td>
<td class="text-end">
<a href="{{ route('dashboard.show', $site) }}" class="btn btn-sm btn-outline-primary btn-action">
<i class="bi bi-arrow-right-circle"></i> Details
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="empty-state">
<div class="empty-state-icon">
<i class="bi bi-globe"></i>
</div>
<h5>No Sites Yet</h5>
<p class="text-muted">Start monitoring your websites by adding your first site.</p>
<a href="{{ route('sites.create') }}" class="btn btn-primary mt-3">
<i class="bi bi-plus-circle"></i> Add Your First Site
</a>
</div>
@endif
</div>
</div>
@endsection
@push('scripts')
@if($sites->isNotEmpty() && isset($chartData))
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('weeklyUptimeChart').getContext('2d');
const chartData = @json($chartData);
new Chart(ctx, {
type: 'line',
data: {
labels: chartData.labels,
datasets: chartData.datasets.map((dataset, index) => ({
label: dataset.label,
data: dataset.data,
borderColor: dataset.color,
backgroundColor: dataset.color + '33',
borderWidth: 2,
fill: false,
tension: 0.1
}))
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: 'Uptime %'
}
},
x: {
title: {
display: true,
text: 'Date'
}
}
}
}
});
});
</script>
@endif
@endpush
+230
View File
@@ -0,0 +1,230 @@
@extends('layouts.app')
@section('title', $site->name . ' Dashboard')
@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1><i class="bi bi-bar-chart"></i> {{ $site->name }}</h1>
<p class="text-muted mb-0">
<i class="bi bi-link-45deg"></i> <a href="{{ $site->url }}" target="_blank">{{ $site->url }}</a>
</p>
</div>
<a href="{{ route('sites.index') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Sites
</a>
</div>
<!-- Stats Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card text-white bg-primary">
<div class="card-body">
<h6 class="card-title">Uptime</h6>
<h2 class="mb-0">{{ $uptimePercentage }}%</h2>
<small>Last 30 days</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white bg-success">
<div class="card-body">
<h6 class="card-title">Avg Response Time</h6>
<h2 class="mb-0">{{ $avgResponseTime ? round($avgResponseTime) : 'N/A' }}</h2>
<small>{{ $avgResponseTime ? 'milliseconds' : 'No data' }}</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white {{ $brokenLinks->total() > 0 ? 'bg-danger' : 'bg-success' }}">
<div class="card-body">
<h6 class="card-title">Broken Links</h6>
<h2 class="mb-0">{{ $brokenLinks->total() }}</h2>
<small>{{ $brokenLinks->total() == 1 ? 'link' : 'links' }} detected</small>
</div>
</div>
</div>
</div>
<!-- Uptime Chart -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Response Time History (Last 30 Days)</h5>
</div>
<div class="card-body">
@if($uptimeChecks->isNotEmpty())
<canvas id="uptimeChart" height="80"></canvas>
@else
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> No uptime data available yet. The site will be checked automatically.
</div>
@endif
</div>
</div>
<!-- SSL Certificate -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-shield-lock"></i> SSL Certificate</h5>
</div>
<div class="card-body">
@if($latestSslCert)
@php
$daysUntilExpiry = now()->diffInDays($latestSslCert->expires_at, false);
$isExpired = $daysUntilExpiry < 0;
$isExpiringSoon = $daysUntilExpiry < 30 && !$isExpired;
@endphp
<div class="row">
<div class="col-md-3">
<strong>Issuer:</strong><br>
{{ $latestSslCert->issuer }}
</div>
<div class="col-md-3">
<strong>Expires:</strong><br>
{{ $latestSslCert->expires_at->format('M d, Y') }}
</div>
<div class="col-md-3">
<strong>Days Remaining:</strong><br>
<span class="badge bg-{{ $isExpired ? 'danger' : ($isExpiringSoon ? 'warning' : 'success') }}">
{{ $isExpired ? 'EXPIRED' : $daysUntilExpiry . ' days' }}
</span>
</div>
<div class="col-md-3">
<strong>Last Checked:</strong><br>
{{ $latestSslCert->checked_at->diffForHumans() }}
</div>
</div>
@else
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i> No SSL certificate data available yet.
</div>
@endif
</div>
</div>
<!-- Broken Links -->
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-link-45deg"></i> Broken Links</h5>
</div>
<div class="card-body">
@if($brokenLinks->isNotEmpty())
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>URL</th>
<th>Status Code</th>
<th>Last Checked</th>
</tr>
</thead>
<tbody>
@foreach($brokenLinks as $link)
<tr>
<td>
<a href="{{ $link->url }}" target="_blank" class="text-decoration-none">
{{ $link->url }}
</a>
</td>
<td>
<span class="badge bg-danger">{{ $link->status_code ?? 'Failed' }}</span>
</td>
<td>{{ $link->last_checked_at->diffForHumans() }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="mt-3">
{{ $brokenLinks->links() }}
</div>
@else
<div class="alert alert-success mb-0">
<i class="bi bi-check-circle"></i> No broken links detected! Your site is looking good.
</div>
@endif
</div>
</div>
@endsection
@push('scripts')
@if($uptimeChecks->isNotEmpty())
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('uptimeChart').getContext('2d');
const labels = @json($chartLabels);
const responseData = @json($chartData);
const statusCodes = @json($chartStatusCodes);
// Create background colors based on status codes
const backgroundColors = statusCodes.map(code => {
if (!code) return 'rgba(220, 53, 69, 0.5)'; // Red for null/failed
if (code >= 200 && code < 400) return 'rgba(25, 135, 84, 0.5)'; // Green for success
return 'rgba(220, 53, 69, 0.5)'; // Red for errors
});
const borderColors = statusCodes.map(code => {
if (!code) return 'rgb(220, 53, 69)';
if (code >= 200 && code < 400) return 'rgb(25, 135, 84)';
return 'rgb(220, 53, 69)';
});
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Response Time (ms)',
data: responseData,
borderColor: 'rgb(13, 110, 253)',
backgroundColor: backgroundColors,
borderWidth: 2,
fill: false,
tension: 0.1,
pointBackgroundColor: backgroundColors,
pointBorderColor: borderColors,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
callbacks: {
afterLabel: function(context) {
const statusCode = statusCodes[context.dataIndex];
return statusCode ? 'HTTP ' + statusCode : 'Connection Failed';
}
}
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Response Time (ms)'
}
},
x: {
title: {
display: true,
text: 'Check Time'
}
}
}
}
});
});
</script>
@endif
@endpush
+23
View File
@@ -0,0 +1,23 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Dashboard') }}</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
{{ __('You are logged in!') }}
</div>
</div>
</div>
</div>
</div>
@endsection
+267
View File
@@ -0,0 +1,267 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" x-data="{ darkMode: localStorage.getItem('darkMode') === 'true' }" :class="{ 'dark': darkMode }">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', 'Dashboard') - {{ config('app.name') }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-sidebar: #1f2937;
--text-primary: #111827;
--text-secondary: #6b7280;
--border-color: #e5e7eb;
--indigo-600: #4f46e5;
--indigo-700: #4338ca;
}
.dark {
--bg-primary: #111827;
--bg-secondary: #1f2937;
--text-primary: #f9fafb;
--text-secondary: #9ca3af;
--border-color: #374151;
}
* {
font-family: 'Inter', sans-serif;
}
body {
background-color: var(--bg-secondary);
color: var(--text-primary);
min-height: 100vh;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 260px;
background-color: var(--bg-sidebar);
padding: 1.5rem 0;
z-index: 1000;
overflow-y: auto;
}
.sidebar-logo {
padding: 0 1.5rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 1rem;
}
.sidebar-nav {
padding: 0;
list-style: none;
}
.sidebar-nav-item {
margin-bottom: 0.25rem;
}
.sidebar-nav-link {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
color: #9ca3af;
text-decoration: none;
transition: all 0.2s;
}
.sidebar-nav-link:hover,
.sidebar-nav-link.active {
background-color: rgba(79, 70, 229, 0.1);
color: var(--indigo-600);
}
.sidebar-nav-link i {
margin-right: 0.75rem;
font-size: 1.25rem;
}
.main-content {
margin-left: 260px;
min-height: 100vh;
}
.topbar {
background-color: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.card {
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-primary);
}
.btn-primary {
background-color: var(--indigo-600);
border-color: var(--indigo-600);
}
.btn-primary:hover {
background-color: var(--indigo-700);
border-color: var(--indigo-700);
}
.badge-success {
background-color: #10b981;
}
.badge-danger {
background-color: #ef4444;
}
.badge-warning {
background-color: #f59e0b;
}
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s;
}
.sidebar.show {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
}
</style>
@stack('styles')
</head>
<body>
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-logo">
<h4 class="text-white mb-0">
<i class="bi bi-shield-check"></i>
Site Monitor
</h4>
</div>
<ul class="sidebar-nav">
<li class="sidebar-nav-item">
<a href="{{ route('dashboard') }}" class="sidebar-nav-link {{ request()->routeIs('dashboard') ? 'active' : '' }}">
<i class="bi bi-speedometer2"></i>
Dashboard
</a>
</li>
<li class="sidebar-nav-item">
<a href="{{ route('sites.index') }}" class="sidebar-nav-link {{ request()->routeIs('sites.*') ? 'active' : '' }}">
<i class="bi bi-globe"></i>
Sites
</a>
</li>
<li class="sidebar-nav-item">
<a href="{{ route('alerts.index') }}" class="sidebar-nav-link {{ request()->routeIs('alerts.*') ? 'active' : '' }}">
<i class="bi bi-bell"></i>
Alerts
</a>
</li>
<li class="sidebar-nav-item">
<a href="{{ route('settings') }}" class="sidebar-nav-link {{ request()->routeIs('settings') ? 'active' : '' }}">
<i class="bi bi-gear"></i>
Settings
</a>
</li>
</ul>
</aside>
<!-- Main Content -->
<div class="main-content">
<!-- Topbar -->
<div class="topbar">
<button class="btn btn-outline-secondary d-md-none" onclick="document.getElementById('sidebar').classList.toggle('show')">
<i class="bi bi-list"></i>
</button>
<div></div>
<div class="d-flex align-items-center gap-3">
<button class="btn btn-outline-secondary" @click="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode)">
<i class="bi" :class="darkMode ? 'bi-sun' : 'bi-moon'"></i>
</button>
@auth
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i>
{{ auth()->user()->name }}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="dropdown-item">
<i class="bi bi-box-arrow-right"></i> Logout
</button>
</form>
</li>
</ul>
</div>
@endauth
</div>
</div>
<!-- Content -->
<div class="p-4">
@if (session('success'))
<x-alert type="success" dismissible>
{{ session('success') }}
</x-alert>
@endif
@if (session('error'))
<x-alert type="danger" dismissible>
{{ session('error') }}
</x-alert>
@endif
@if ($errors->any())
<x-alert type="danger" dismissible>
<strong>Validation Error:</strong>
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</x-alert>
@endif
@yield('content')
</div>
</div>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
function confirmDelete(message) {
return confirm(message || 'Are you sure you want to delete this?');
}
</script>
@stack('scripts')
</body>
</html>
+144
View File
@@ -0,0 +1,144 @@
@extends('layouts.app')
@section('title', 'Settings')
@section('content')
<div class="mb-4">
<h1><i class="bi bi-gear"></i> Settings</h1>
<p class="text-muted">Manage your account and notification preferences</p>
</div>
<div class="row">
<!-- Notification Preferences -->
<div class="col-lg-8 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-bell"></i> Notification Preferences</h5>
</div>
<div class="card-body">
<form action="{{ route('settings.notifications') }}" method="POST">
@csrf
@method('PATCH')
<p class="text-muted mb-4">Choose how you want to be notified about issues with your sites.</p>
<!-- Uptime Alerts -->
<div class="mb-4">
<h6 class="mb-3">Uptime Alerts</h6>
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input" id="uptime_email" name="uptime[]" value="mail"
{{ in_array('mail', $preferences['uptime'] ?? []) ? 'checked' : '' }}>
<label class="form-check-label" for="uptime_email">
<i class="bi bi-envelope"></i> Email Notifications
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="uptime_slack" name="uptime[]" value="slack"
{{ in_array('slack', $preferences['uptime'] ?? []) ? 'checked' : '' }}>
<label class="form-check-label" for="uptime_slack">
<i class="bi bi-slack"></i> Slack Notifications
</label>
</div>
</div>
<!-- SSL Expiry Alerts -->
<div class="mb-4">
<h6 class="mb-3">SSL Certificate Alerts</h6>
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input" id="ssl_email" name="ssl[]" value="mail"
{{ in_array('mail', $preferences['ssl'] ?? []) ? 'checked' : '' }}>
<label class="form-check-label" for="ssl_email">
<i class="bi bi-envelope"></i> Email Notifications
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="ssl_slack" name="ssl[]" value="slack"
{{ in_array('slack', $preferences['ssl'] ?? []) ? 'checked' : '' }}>
<label class="form-check-label" for="ssl_slack">
<i class="bi bi-slack"></i> Slack Notifications
</label>
</div>
</div>
<!-- Broken Links Alerts -->
<div class="mb-4">
<h6 class="mb-3">Broken Links Alerts</h6>
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input" id="broken_links_email" name="broken_links[]" value="mail"
{{ in_array('mail', $preferences['broken_links'] ?? []) ? 'checked' : '' }}>
<label class="form-check-label" for="broken_links_email">
<i class="bi bi-envelope"></i> Email Notifications
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="broken_links_slack" name="broken_links[]" value="slack"
{{ in_array('slack', $preferences['broken_links'] ?? []) ? 'checked' : '' }}>
<label class="form-check-label" for="broken_links_slack">
<i class="bi bi-slack"></i> Slack Notifications
</label>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Save Preferences
</button>
</form>
</div>
</div>
</div>
<!-- Profile Settings -->
<div class="col-lg-4 mb-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-person"></i> Profile</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="text-muted small">Name</label>
<p class="mb-0">{{ auth()->user()->name }}</p>
</div>
<div class="mb-3">
<label class="text-muted small">Email</label>
<p class="mb-0">{{ auth()->user()->email }}</p>
</div>
<div class="mb-3">
<label class="text-muted small">Member Since</label>
<p class="mb-0">{{ auth()->user()->created_at->format('M d, Y') }}</p>
</div>
</div>
</div>
<!-- Slack Integration -->
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-slack"></i> Slack Integration</h5>
</div>
<div class="card-body">
<form action="{{ route('settings.slack') }}" method="POST">
@csrf
@method('PATCH')
<div class="mb-3">
<label for="slack_webhook" class="form-label">Slack Webhook URL</label>
<input type="url" class="form-control @error('slack_webhook') is-invalid @enderror"
id="slack_webhook" name="slack_webhook"
value="{{ old('slack_webhook', $slackWebhook ?? '') }}"
placeholder="https://hooks.slack.com/...">
@error('slack_webhook')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<div class="form-text">
<a href="https://api.slack.com/messaging/webhooks" target="_blank">Get your webhook URL</a>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Save Webhook
</button>
</form>
</div>
</div>
</div>
</div>
@endsection
+61
View File
@@ -0,0 +1,61 @@
@extends('layouts.app')
@section('title', 'Add New Site')
@section('content')
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0"><i class="bi bi-plus-circle"></i> Add New Site</h4>
</div>
<div class="card-body">
<form action="{{ route('sites.store') }}" method="POST">
@csrf
<div class="mb-3">
<label for="name" class="form-label">Site Name <span class="text-danger">*</span></label>
<input type="text" class="form-control @error('name') is-invalid @enderror"
id="name" name="name" value="{{ old('name') }}" required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<div class="form-text">A friendly name to identify your site</div>
</div>
<div class="mb-3">
<label for="url" class="form-label">Site URL <span class="text-danger">*</span></label>
<input type="url" class="form-control @error('url') is-invalid @enderror"
id="url" name="url" value="{{ old('url') }}" required
placeholder="https://example.com">
@error('url')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<div class="form-text">The full URL of your site (must start with http:// or https://)</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="monitoring_enabled"
name="monitoring_enabled" {{ old('monitoring_enabled', true) ? 'checked' : '' }}>
<label class="form-check-label" for="monitoring_enabled">
Enable Monitoring
</label>
</div>
<div class="form-text">Start monitoring this site immediately</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Add Site
</button>
<a href="{{ route('sites.index') }}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
+61
View File
@@ -0,0 +1,61 @@
@extends('layouts.app')
@section('title', 'Edit Site')
@section('content')
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0"><i class="bi bi-pencil"></i> Edit Site</h4>
</div>
<div class="card-body">
<form action="{{ route('sites.update', $site) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label for="name" class="form-label">Site Name <span class="text-danger">*</span></label>
<input type="text" class="form-control @error('name') is-invalid @enderror"
id="name" name="name" value="{{ old('name', $site->name) }}" required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<div class="form-text">A friendly name to identify your site</div>
</div>
<div class="mb-3">
<label for="url" class="form-label">Site URL <span class="text-danger">*</span></label>
<input type="url" class="form-control @error('url') is-invalid @enderror"
id="url" name="url" value="{{ old('url', $site->url) }}" required>
@error('url')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<div class="form-text">The full URL of your site (must start with http:// or https://)</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="monitoring_enabled"
name="monitoring_enabled" {{ old('monitoring_enabled', $site->monitoring_enabled) ? 'checked' : '' }}>
<label class="form-check-label" for="monitoring_enabled">
Enable Monitoring
</label>
</div>
<div class="form-text">Enable or disable monitoring for this site</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Update Site
</button>
<a href="{{ route('sites.index') }}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
+93
View File
@@ -0,0 +1,93 @@
@extends('layouts.app')
@section('title', 'My Sites')
@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-list-ul"></i> My Sites</h1>
<a href="{{ route('sites.create') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add New Site
</a>
</div>
@if($sites->isEmpty())
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> You haven't added any sites yet. Click "Add New Site" to get started!
</div>
@else
<div class="row">
@foreach($sites as $site)
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-0">{{ $site->name }}</h5>
@if($site->latestUptimeCheck)
@if($site->latestUptimeCheck->status_code >= 200 && $site->latestUptimeCheck->status_code < 400)
<span class="badge bg-success">Online</span>
@else
<span class="badge bg-danger">Down</span>
@endif
@else
<span class="badge bg-secondary">Not Checked</span>
@endif
</div>
<p class="card-text text-muted small mb-3">
<i class="bi bi-link-45deg"></i> {{ $site->url }}
</p>
<div class="row g-2 mb-3">
<div class="col-6">
<div class="border rounded p-2 text-center">
<div class="small text-muted">Uptime Checks</div>
<strong>{{ $site->uptime_checks_count }}</strong>
</div>
</div>
<div class="col-6">
<div class="border rounded p-2 text-center">
<div class="small text-muted">Broken Links</div>
<strong class="{{ $site->broken_links_count > 0 ? 'text-danger' : '' }}">
{{ $site->broken_links_count }}
</strong>
</div>
</div>
</div>
@if($site->latestSslCertificate)
@php
$daysUntilExpiry = now()->diffInDays($site->latestSslCertificate->expires_at, false);
@endphp
<div class="alert alert-{{ $daysUntilExpiry < 30 ? 'warning' : 'success' }} alert-sm py-1 px-2 mb-3">
<small>
<i class="bi bi-shield-lock"></i> SSL expires in {{ $daysUntilExpiry }} days
</small>
</div>
@endif
<div class="d-flex gap-2">
<a href="{{ route('dashboard.show', $site) }}" class="btn btn-sm btn-outline-primary flex-fill">
<i class="bi bi-bar-chart"></i> Dashboard
</a>
<a href="{{ route('sites.edit', $site) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
<form action="{{ route('sites.destroy', $site) }}" method="POST" class="d-inline"
onsubmit="return confirm('Are you sure you want to delete this site?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
<div class="card-footer small text-muted">
<i class="bi bi-clock"></i> Added {{ $site->created_at->diffForHumans() }}
</div>
</div>
</div>
@endforeach
</div>
@endif
@endsection
+494
View File
@@ -0,0 +1,494 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name') }} - Website Monitoring Made Simple</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
* {
font-family: 'Inter', sans-serif;
}
body {
overflow-x: hidden;
}
.hero-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 100px 0 80px;
position: relative;
overflow: hidden;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
opacity: 0.1;
}
.hero-section h1 {
font-size: 3.5rem;
font-weight: 800;
margin-bottom: 1.5rem;
}
.hero-section p.lead {
font-size: 1.5rem;
margin-bottom: 2rem;
opacity: 0.95;
}
.btn-hero {
padding: 1rem 2.5rem;
font-size: 1.1rem;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
display: inline-block;
transition: all 0.3s;
}
.btn-hero-primary {
background: white;
color: #667eea;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.btn-hero-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
color: #667eea;
}
.btn-hero-secondary {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid white;
margin-left: 1rem;
}
.btn-hero-secondary:hover {
background: rgba(255, 255, 255, 0.3);
color: white;
}
.feature-card {
padding: 2.5rem;
border-radius: 12px;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
height: 100%;
transition: all 0.3s;
border: 1px solid #e5e7eb;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.feature-icon {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
color: white;
margin-bottom: 1.5rem;
}
.feature-card h3 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 1rem;
color: #111827;
}
.feature-card p {
color: #6b7280;
line-height: 1.7;
margin-bottom: 0;
}
.stats-section {
background: #f9fafb;
padding: 60px 0;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 3rem;
font-weight: 800;
color: #667eea;
display: block;
}
.stat-label {
color: #6b7280;
font-size: 1.1rem;
margin-top: 0.5rem;
}
.cta-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 80px 0;
text-align: center;
}
.cta-section h2 {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 1rem;
}
.navbar-landing {
padding: 1.5rem 0;
background: transparent;
position: absolute;
width: 100%;
z-index: 10;
}
.navbar-landing .navbar-brand {
font-size: 1.5rem;
font-weight: 800;
color: white;
}
.navbar-landing .nav-link {
color: white;
font-weight: 500;
margin: 0 1rem;
}
.navbar-landing .btn-login {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid white;
padding: 0.5rem 1.5rem;
border-radius: 6px;
font-weight: 600;
}
.navbar-landing .btn-login:hover {
background: white;
color: #667eea;
}
.pricing-badge {
background: #10b981;
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
}
@media (max-width: 768px) {
.hero-section h1 {
font-size: 2.5rem;
}
.hero-section p.lead {
font-size: 1.2rem;
}
.btn-hero-secondary {
margin-left: 0;
margin-top: 1rem;
}
}
</style>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-landing">
<div class="container">
<a class="navbar-brand" href="/">
<i class="bi bi-shield-check"></i> {{ config('app.name', 'Site Monitor') }}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbarNav">
<ul class="navbar-nav align-items-center">
<li class="nav-item">
<a class="nav-link" href="#features">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#pricing">Pricing</a>
</li>
@guest
<li class="nav-item">
<a class="btn btn-login" href="{{ route('login') }}">Login</a>
</li>
@else
<li class="nav-item">
<a class="btn btn-login" href="{{ route('dashboard') }}">Dashboard</a>
</li>
@endguest
</ul>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="hero-section">
<div class="container position-relative">
<div class="row align-items-center">
<div class="col-lg-7">
<h1>Monitor Your Websites With Confidence</h1>
<p class="lead">Get instant alerts when your site goes down, SSL certificates expire, or broken links appear.</p>
<div class="mt-4">
@guest
<a href="{{ route('register') }}" class="btn-hero btn-hero-primary">Get Started Free</a>
<a href="#features" class="btn-hero btn-hero-secondary">Learn More</a>
@else
<a href="{{ route('dashboard') }}" class="btn-hero btn-hero-primary">Go to Dashboard</a>
<a href="{{ route('sites.create') }}" class="btn-hero btn-hero-secondary">Add Your First Site</a>
@endguest
</div>
</div>
<div class="col-lg-5 d-none d-lg-block text-center">
<i class="bi bi-graph-up-arrow" style="font-size: 15rem; opacity: 0.2;"></i>
</div>
</div>
</div>
</section>
<!-- Stats Section -->
<section class="stats-section">
<div class="container">
<div class="row">
<div class="col-md-4 mb-4 mb-md-0">
<div class="stat-item">
<span class="stat-number">99.9%</span>
<div class="stat-label">Uptime Guaranteed</div>
</div>
</div>
<div class="col-md-4 mb-4 mb-md-0">
<div class="stat-item">
<span class="stat-number">5 min</span>
<div class="stat-label">Check Intervals</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-item">
<span class="stat-number">24/7</span>
<div class="stat-label">Monitoring</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="py-5" style="padding: 80px 0;">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-4 fw-bold mb-3">Everything You Need to Monitor Your Sites</h2>
<p class="lead text-muted">Comprehensive monitoring tools to keep your websites running smoothly</p>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="feature-card">
<div class="feature-icon">
<i class="bi bi-activity"></i>
</div>
<h3>Uptime Monitoring</h3>
<p>Check your website's availability every 5 minutes. Get instant alerts via email or Slack when your site goes down.</p>
</div>
</div>
<div class="col-md-4">
<div class="feature-card">
<div class="feature-icon">
<i class="bi bi-shield-lock"></i>
</div>
<h3>SSL Certificate Checks</h3>
<p>Never let your SSL certificates expire. Receive notifications 30 days before expiry to renew on time.</p>
</div>
</div>
<div class="col-md-4">
<div class="feature-card">
<div class="feature-icon">
<i class="bi bi-link-45deg"></i>
</div>
<h3>Broken Link Detection</h3>
<p>Automatically scan your website for broken links. Keep your SEO strong and user experience smooth.</p>
</div>
</div>
<div class="col-md-4">
<div class="feature-card">
<div class="feature-icon">
<i class="bi bi-graph-up"></i>
</div>
<h3>Performance Tracking</h3>
<p>Track response times and view historical performance data with beautiful charts and analytics.</p>
</div>
</div>
<div class="col-md-4">
<div class="feature-card">
<div class="feature-icon">
<i class="bi bi-bell"></i>
</div>
<h3>Smart Notifications</h3>
<p>Customize notification preferences for each check type. Get alerts via email or Slack integration.</p>
</div>
</div>
<div class="col-md-4">
<div class="feature-card">
<div class="feature-icon">
<i class="bi bi-speedometer2"></i>
</div>
<h3>Beautiful Dashboard</h3>
<p>View all your sites at a glance with an intuitive dashboard. Dark mode included for late-night monitoring.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Pricing Section -->
<section id="pricing" class="py-5 bg-light" style="padding: 80px 0;">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-4 fw-bold mb-3">Simple, Transparent Pricing</h2>
<p class="lead text-muted">Start monitoring for free, upgrade as you grow</p>
</div>
<div class="row justify-content-center">
<div class="col-md-5 mb-4">
<div class="feature-card text-center">
<div class="mb-3">
<span class="pricing-badge">FREE FOREVER</span>
</div>
<h3>Starter</h3>
<div class="my-4">
<span class="display-3 fw-bold">$0</span>
<span class="text-muted">/month</span>
</div>
<ul class="list-unstyled text-start mb-4">
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Up to 5 sites</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> 5-minute checks</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Email notifications</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> SSL monitoring</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Broken link detection</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> 30 days history</li>
</ul>
<a href="{{ route('register') }}" class="btn btn-lg btn-outline-primary w-100">Get Started</a>
</div>
</div>
<div class="col-md-5 mb-4">
<div class="feature-card text-center" style="border: 2px solid #667eea;">
<div class="mb-3">
<span class="pricing-badge" style="background: #667eea;">COMING SOON</span>
</div>
<h3>Professional</h3>
<div class="my-4">
<span class="display-3 fw-bold">$19</span>
<span class="text-muted">/month</span>
</div>
<ul class="list-unstyled text-start mb-4">
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Unlimited sites</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> 1-minute checks</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Email + Slack notifications</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Advanced SSL monitoring</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Deep link crawling</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Unlimited history</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Priority support</li>
</ul>
<button class="btn btn-lg w-100" style="background: #667eea; color: white;" disabled>Coming Soon</button>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="cta-section">
<div class="container">
<h2>Ready to Monitor Your Websites?</h2>
<p class="lead mb-4">Join thousands of developers who trust us to keep their sites online.</p>
@guest
<a href="{{ route('register') }}" class="btn-hero btn-hero-primary">Start Monitoring for Free</a>
@else
<a href="{{ route('sites.create') }}" class="btn-hero btn-hero-primary">Add Your First Site</a>
@endguest
</div>
</section>
<!-- Footer -->
<footer class="bg-dark text-white py-5">
<div class="container">
<div class="row">
<div class="col-md-4 mb-4 mb-md-0">
<h5 class="mb-3"><i class="bi bi-shield-check"></i> {{ config('app.name', 'Site Monitor') }}</h5>
<p class="text-muted">Professional website monitoring made simple. Keep your sites online 24/7.</p>
</div>
<div class="col-md-4 mb-4 mb-md-0">
<h6 class="mb-3">Product</h6>
<ul class="list-unstyled">
<li class="mb-2"><a href="#features" class="text-muted text-decoration-none">Features</a></li>
<li class="mb-2"><a href="#pricing" class="text-muted text-decoration-none">Pricing</a></li>
@guest
<li class="mb-2"><a href="{{ route('login') }}" class="text-muted text-decoration-none">Login</a></li>
<li class="mb-2"><a href="{{ route('register') }}" class="text-muted text-decoration-none">Sign Up</a></li>
@endguest
</ul>
</div>
<div class="col-md-4">
<h6 class="mb-3">Built With</h6>
<p class="text-muted">Laravel " MySQL " Bootstrap " Alpine.js " Chart.js</p>
<p class="text-muted small mt-3">&copy; {{ date('Y') }} {{ config('app.name') }}. All rights reserved.</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
</script>
</body>
</html>
File diff suppressed because one or more lines are too long