phase 8 and 9

This commit is contained in:
Stefanescu Mihai
2026-03-19 18:42:19 +02:00
parent d75cabfeec
commit 82ebb697f3
94 changed files with 4498 additions and 749 deletions
@@ -0,0 +1,85 @@
<x-analytics-layout :website="$website" title="Manage Widgets">
<x-slot name="headerActions">
<a href="{{ route('custom-dashboard.show', $website) }}" class="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-300 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
View Dashboard
</a>
</x-slot>
@if (session('success'))
<div class="mb-6 rounded-xl bg-emerald-50 border border-emerald-200 p-4">
<p class="text-sm font-medium text-emerald-800">{{ session('success') }}</p>
</div>
@endif
<div class="grid gap-6 lg:grid-cols-2">
{{-- Add widget form --}}
<div class="bg-white rounded-xl border border-gray-200 p-6">
<h3 class="text-base font-semibold text-gray-900 mb-4">Add Widget</h3>
<form method="POST" action="{{ route('custom-dashboard.store', $website) }}" x-data="{ type: 'metric' }" class="space-y-4">
@csrf
<div>
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input type="text" name="title" id="title" required placeholder="e.g. Daily Visitors"
class="block w-full rounded-lg border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div>
<label for="widget_type" class="block text-sm font-medium text-gray-700 mb-1">Type</label>
<select name="widget_type" id="widget_type" x-model="type"
class="block w-full rounded-lg border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@foreach ($types as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div x-show="type === 'metric'" x-cloak>
<label for="config_metric" class="block text-sm font-medium text-gray-700 mb-1">Metric</label>
<select name="config[metric]" id="config_metric"
class="block w-full rounded-lg border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@foreach ($metrics as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label for="size" class="block text-sm font-medium text-gray-700 mb-1">Size</label>
<select name="size" id="size"
class="block w-full rounded-lg border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="third">Small (1/3)</option>
<option value="half">Medium (1/2)</option>
<option value="full">Full Width</option>
</select>
</div>
<button type="submit" class="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 rounded-lg text-sm font-medium text-white hover:bg-indigo-500 transition">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
Add Widget
</button>
</form>
</div>
{{-- Current widgets --}}
<div class="bg-white rounded-xl border border-gray-200 p-6">
<h3 class="text-base font-semibold text-gray-900 mb-4">Current Widgets</h3>
@if ($widgets->isEmpty())
<p class="text-sm text-gray-400">No widgets yet. Add one using the form.</p>
@else
<div class="space-y-2">
@foreach ($widgets as $widget)
<div class="flex items-center justify-between rounded-lg border border-gray-200 px-4 py-3">
<div>
<p class="text-sm font-medium text-gray-900">{{ $widget->title }}</p>
<p class="text-xs text-gray-500">{{ \App\Models\DashboardWidget::$types[$widget->widget_type] ?? $widget->widget_type }} · {{ $widget->size }}</p>
</div>
<form method="POST" action="{{ route('custom-dashboard.destroy', [$website, $widget]) }}" onsubmit="return confirm('Remove this widget?')">
@csrf
@method('DELETE')
<button type="submit" class="text-sm text-red-600 hover:text-red-800 font-medium transition">Remove</button>
</form>
</div>
@endforeach
</div>
@endif
</div>
</div>
</x-analytics-layout>
@@ -0,0 +1,120 @@
<x-analytics-layout :website="$website" :from="$from" :to="$to" title="Custom Dashboard">
<x-slot name="headerActions">
<a href="{{ route('custom-dashboard.manage', $website) }}" class="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-300 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
Manage Widgets
</a>
</x-slot>
@if ($widgets->isEmpty())
<x-empty-state
icon='<svg class="w-10 h-10" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25a2.25 2.25 0 0 1-2.25-2.25v-2.25Z" /></svg>'
title="No widgets yet"
message="Add widgets to build your custom dashboard."
>
<a href="{{ route('custom-dashboard.manage', $website) }}" class="mt-4 inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 rounded-lg text-sm font-medium text-white hover:bg-indigo-500 transition">
Manage Widgets
</a>
</x-empty-state>
@else
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($widgets as $widget)
@php
$colSpan = match($widget->size) {
'full' => 'sm:col-span-2 lg:col-span-3',
'half' => 'sm:col-span-1 lg:col-span-2' ,
default => '',
};
$data = $widgetData[$widget->id] ?? null;
@endphp
<div class="bg-white rounded-xl border border-gray-200 p-5 {{ $colSpan }}">
<h3 class="text-sm font-semibold text-gray-900 mb-3">{{ $widget->title }}</h3>
@if ($widget->widget_type === 'metric')
@php $metricLabel = \App\Models\DashboardWidget::$metrics[$widget->config['metric'] ?? 'page_views'] ?? 'Metric'; @endphp
<p class="text-3xl font-bold text-gray-900 tabular-nums">
@if (($widget->config['metric'] ?? '') === 'bounce_rate')
{{ $data }}%
@elseif (($widget->config['metric'] ?? '') === 'avg_duration')
{{ gmdate('i:s', (int) $data) }}
@else
{{ number_format($data) }}
@endif
</p>
<p class="text-xs text-gray-500 mt-1">{{ $metricLabel }}</p>
@elseif ($widget->widget_type === 'chart')
<div style="height: 200px">
<canvas id="widget-chart-{{ $widget->id }}"></canvas>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const ctx = document.getElementById('widget-chart-{{ $widget->id }}');
new Chart(ctx, {
type: 'line',
data: {
labels: @json($data->pluck('date')),
datasets: [{
label: 'Visitors',
data: @json($data->pluck('visitors')),
borderColor: '#6366f1',
backgroundColor: 'rgba(99,102,241,0.1)',
fill: true,
tension: 0.3,
pointRadius: 0,
}]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { beginAtZero: true } } }
});
});
</script>
@elseif (in_array($widget->widget_type, ['top_pages', 'top_sources', 'browsers', 'countries', 'events']))
@if ($data && $data->isNotEmpty())
<div class="space-y-2">
@foreach ($data->take(5) as $row)
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600 truncate mr-2">
{{ $row->path ?? $row->source ?? $row->browser ?? $row->country ?? $row->name ?? '—' }}
</span>
<span class="font-medium text-gray-900 tabular-nums shrink-0">
{{ number_format($row->views ?? $row->sessions ?? $row->visitors ?? $row->total ?? 0) }}
</span>
</div>
@endforeach
</div>
@else
<p class="text-sm text-gray-400">No data for this period.</p>
@endif
@elseif ($widget->widget_type === 'new_vs_returning')
<div class="flex items-center gap-6">
<div>
<p class="text-2xl font-bold text-indigo-600 tabular-nums">{{ $data['new_pct'] ?? 0 }}%</p>
<p class="text-xs text-gray-500">New</p>
</div>
<div>
<p class="text-2xl font-bold text-emerald-600 tabular-nums">{{ $data['returning_pct'] ?? 0 }}%</p>
<p class="text-xs text-gray-500">Returning</p>
</div>
</div>
@elseif ($widget->widget_type === 'conversions')
@if ($data && $data->isNotEmpty())
<div class="space-y-2">
@foreach ($data->take(5) as $goal)
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600 truncate mr-2">{{ $goal->name }}</span>
<span class="font-medium text-gray-900 tabular-nums shrink-0">{{ number_format($goal->conversions) }}</span>
</div>
@endforeach
</div>
@else
<p class="text-sm text-gray-400">No conversions for this period.</p>
@endif
@endif
</div>
@endforeach
</div>
@endif
</x-analytics-layout>