$this->viewData->searches = $this->_processSearches();
$this->viewData->outlinks = $this->_processOutgoingLinks();
$this->viewData->countries = $this->_processCountries();
+ $this->viewData->shareDetails = $this->_processShareDetails();
//=== MAIN PERIOD STATISTICS
// These are the main statistics used to build the chart and the table below it.
}
+ protected function _processShareDetails()
+ {
+ $icons = [
+ 'email' => 'las la-envelope',
+ 'facebook' => 'lab la-facebook-f',
+ 'twitter' => 'lab la-twitter',
+ 'whatsapp' => 'lab la-whatsapp',
+ 'linkedin' => 'lab la-linkedin-in',
+ 'pinterest' => 'lab la-pinterest-p',
+ 'googleplus' => 'lab la-google-plus',
+ 'viadeo' => 'lab la-viadeo',
+ ];
+
+// $res = [];
+// foreach ($icons as $social => $icon) {
+// $res[$social] = ['nb' => rand(1, 100), 'icon' => $icon];
+// }
+// return $res;
+
+ $events = collect($this->_eventsByPage['share']['subtable'] ?? [])->sortByDesc('nb_events');
+
+ foreach ($events as $event) {
+ $res[$event['label']] = ['nb' => $event['nb_events'], 'icon' => $icons[$event['label']]];
+ }
+ return $res;
+ }
+
protected function getTooltipLabels()
{
'nb_downloads' => __('Téléchargements'),
'nb_prints' => __('Impressions'),
'nb_zooms' => __('Zooms'),
+ 'nb_shares' => __('Partages'),
],
// Per-page detail table
'per-page' => [
'nb_pageviews' => __('Vues'),
'nb_zooms' => __('Zooms'),
'nb_bookmarks' => __('Pages marquées'),
- 'nb_shares' => __('Partages'),
],
];
->keyBy('label')
->map(function ($item, $key) {
// Make subtable data easier to lookup by keying them with the labels (ie. page numbers) for certain events
- if (in_array($item['label'], ['zoom', 'bookmark', 'share']) && isset($item['subtable'])) {
+ if (in_array($item['label'], ['zoom', 'bookmark', 'share', 'video', 'slideshow']) && isset($item['subtable'])) {
$item['subtable'] = collect($item['subtable'])->keyBy(function ($item, $key) {
// Since there's some inconsistency in the way labels are stored (some have "page x" instead
// of just the number), we strip out any non-numeric values when making the keys
$data = $stats->processData($period_override);
$data['locale'] = app()->getLocale();
$data['fluidbook'] = $fluidbook;
- return view('fluidbook_stats.loader', $data);
+ return view('fluidbook_stats.stats', $data);
}
"jquery-form": "^4.3.0",
"jquery.scrollto": "^2.1.3",
"keymaster-reloaded": "^1.7.2",
+ "line-awesome": "^1.3.0",
"lz4js": "^0.2.0",
"noty": "^3.2.0-beta-deprecated",
"pako": "^2.1.0",
"node": ">=10"
}
},
+ "node_modules/line-awesome": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/line-awesome/-/line-awesome-1.3.0.tgz",
+ "integrity": "sha512-Y0YHksL37ixDsHz+ihCwOtF5jwJgCDxQ3q+zOVgaSW8VugHGTsZZXMacPYZB1/JULBi6BAuTCTek+4ZY/UIwcw=="
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"jquery-form": "^4.3.0",
"jquery.scrollto": "^2.1.3",
"keymaster-reloaded": "^1.7.2",
+ "line-awesome": "^1.3.0",
"lz4js": "^0.2.0",
"noty": "^3.2.0-beta-deprecated",
"pako": "^2.1.0",
--- /dev/null
+.chart-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+table.stats-details {
+ width: 100%;
+ table-layout: fixed;
+}
+table.stats-details th,
+table.stats-details td {
+ padding: 0.5em 0.75em;
+}
+table.stats-details td,
+table.stats-details .with-aliases {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+table.stats-details .with-aliases .number {
+ font-size: 75%;
+ display: block;
+ font-style: italic;
+ margin-top: -2px;
+}
+table.stats-details .with-aliases .alias:after {
+ content: ', ';
+}
+table.stats-details .with-aliases .alias:last-of-type:after {
+ content: '';
+}
+table.stats-details thead tr {
+ position: sticky;
+ top: 0;
+ background-color: #fafafa;
+}
+table.stats-details thead th {
+ border-bottom: medium solid rgba(0, 40, 100, 0.12);
+}
+[data-name="formatted_date"] {
+ white-space: nowrap;
+}
+.table-columns {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2em;
+}
+.table-columns > div {
+ flex: 1;
+}
+.no-statistics {
+ background-color: #fefce9;
+ color: #854e18;
+ padding: 1.5em;
+ margin-top: 1.5em;
+}
+.heading-subtitle {
+ opacity: 0.6;
+}
+.heading-subtitle:before {
+ content: ' — ';
+}
+/*=== Date Range Picker ===*/
+[data-daterangepicker]:hover {
+ color: #467fcf;
+}
+/*=== Table Column Sorter ===*/
+#sorttable_sortfwdind,
+#sorttable_sortrevind {
+ display: none !important;
+}
+.sortable th:not(.sorttable_nosort) {
+ cursor: pointer;
+ white-space: nowrap;
+ position: relative;
+ font-size: 100% !important;
+}
+.sortable th:not(.sorttable_nosort):after,
+.sortable th:not(.sorttable_nosort):before {
+ position: absolute;
+ content: "\f0d7";
+ font-family: Line Awesome Free;
+ font-weight: 900;
+ right: 0.4em;
+ top: 50%;
+ font-size: 14px !important;
+ line-height: 1;
+ opacity: 0.125;
+}
+.sortable th:not(.sorttable_nosort):before {
+ content: "\f0d8";
+ top: auto;
+ bottom: 50%;
+}
+.sortable th:not(.sorttable_nosort).sorttable_sorted_reverse:after {
+ opacity: 1;
+}
+.sortable th:not(.sorttable_nosort).sorttable_sorted:before {
+ opacity: 1;
+}
+.whitespace-nowrap {
+ white-space: nowrap;
+}
+.summary-details,
+.share-details {
+ opacity: 0.6;
+ display: inline-block;
+ margin-left: 0.5em;
+}
+.share-details:before {
+ content: '(';
+}
+.share-details:after {
+ content: ')';
+}
+.share-details span {
+ margin-right: 10px;
+}
+.share-details span:last-of-type {
+ margin-right: 0;
+}
+/*# sourceMappingURL=stats.css.map */
\ No newline at end of file
--- /dev/null
+{"version":3,"sources":["stats.less"],"names":[],"mappings":"AAAA;EACI,aAAA;EACA,mBAAA;EACA,8BAAA;;AAGJ,KAAK;EACD,WAAA;EACA,mBAAA;;AAFJ,KAAK,cAID;AAJJ,KAAK,cAIG;EACA,qBAAA;;AALR,KAAK,cASD;AATJ,KAAK,cASG;EACA,gBAAA;EACA,uBAAA;;AAXR,KAAK,cAcD,cACI;EACI,cAAA;EACA,cAAA;EACA,kBAAA;EACA,gBAAA;;AAnBZ,KAAK,cAcD,cAQI,OAAM;EACF,SAAS,IAAT;;AAvBZ,KAAK,cAcD,cAYI,OAAM,aAAa;EACf,SAAS,EAAT;;AA3BZ,KAAK,cA+BD,MACI;EACI,gBAAA;EACA,MAAA;EACA,yBAAA;;AAnCZ,KAAK,cA+BD,MAOI;EACI,kDAAA;;AAOZ;EACI,mBAAA;;AAGJ;EACI,aAAA;EACA,eAAA;EACA,QAAA;;AAHJ,cAKI;EACI,OAAA;;AAKR;EACI,yBAAA;EACA,cAAA;EACA,cAAA;EACA,iBAAA;;AAGJ;EACI,YAAA;;AAEA,iBAAC;EACG,SAAS,KAAT;;;AAKR,sBAAsB;EAClB,cAAA;;;AAIJ;AAAuB;EACnB,wBAAA;;AAGJ,SAAU,GAAE,IAAI;EACZ,eAAA;EACA,mBAAA;EACA,kBAAA;EACA,0BAAA;;AAEA,SANM,GAAE,IAAI,mBAMX;AAAQ,SANH,GAAE,IAAI,mBAMF;EACN,kBAAA;EACA,SAAS,OAAT;EACA,8BAAA;EACA,gBAAA;EACA,YAAA;EACA,QAAA;EACA,0BAAA;EACA,cAAA;EACA,cAAA;;AAGJ,SAlBM,GAAE,IAAI,mBAkBX;EACG,SAAS,OAAT;EACA,SAAA;EACA,WAAA;;AAGJ,SAxBM,GAAE,IAAI,mBAwBX,yBAAyB;EACtB,UAAA;;AAGJ,SA5BM,GAAE,IAAI,mBA4BX,iBAAiB;EACd,UAAA;;AAIR;EACI,mBAAA;;AAIJ;AAAkB;EACd,YAAA;EACA,qBAAA;EACA,kBAAA;;AAIA,cAAC;EACG,SAAS,GAAT;;AAGJ,cAAC;EACG,SAAS,GAAT;;AANR,cASI;EACI,kBAAA;;AACA,cAFJ,KAEK;EACG,eAAA","file":"stats.css"}
\ No newline at end of file
--- /dev/null
+.chart-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+table.stats-details {
+ width: 100%;
+ table-layout: fixed;
+
+ th, td {
+ padding: 0.5em 0.75em;
+ }
+
+
+ td, .with-aliases {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .with-aliases {
+ .number {
+ font-size: 75%;
+ display: block;
+ font-style: italic;
+ margin-top: -2px;
+ }
+
+ .alias:after {
+ content: ', ';
+ }
+
+ .alias:last-of-type:after {
+ content: '';
+ }
+ }
+
+ thead {
+ tr {
+ position: sticky;
+ top: 0;
+ background-color: #fafafa;
+ }
+
+ th {
+ border-bottom: medium solid rgba(0, 40, 100, .12);
+ }
+ }
+
+}
+
+
+[data-name="formatted_date"] {
+ white-space: nowrap;
+}
+
+.table-columns {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2em;
+
+ > div {
+ flex: 1;
+ }
+}
+
+
+.no-statistics {
+ background-color: #fefce9;
+ color: #854e18;
+ padding: 1.5em;
+ margin-top: 1.5em;
+}
+
+.heading-subtitle {
+ opacity: 0.6;
+
+ &:before {
+ content: ' — ';
+ }
+}
+
+/*=== Date Range Picker ===*/
+[data-daterangepicker]:hover {
+ color: #467fcf;
+}
+
+/*=== Table Column Sorter ===*/
+#sorttable_sortfwdind, #sorttable_sortrevind {
+ display: none !important;
+}
+
+.sortable th:not(.sorttable_nosort) {
+ cursor: pointer;
+ white-space: nowrap;
+ position: relative;
+ font-size: 100% !important;
+
+ &:after, &:before {
+ position: absolute;
+ content: "\f0d7";
+ font-family: Line Awesome Free;
+ font-weight: 900;
+ right: .4em;
+ top: 50%;
+ font-size: 14px !important;
+ line-height: 1;
+ opacity: 0.125;
+ }
+
+ &:before {
+ content: "\f0d8";
+ top: auto;
+ bottom: 50%;
+ }
+
+ &.sorttable_sorted_reverse:after {
+ opacity: 1;
+ }
+
+ &.sorttable_sorted:before {
+ opacity: 1;
+ }
+}
+
+.whitespace-nowrap {
+ white-space: nowrap;
+}
+
+
+.summary-details, .share-details {
+ opacity: 0.6;
+ display: inline-block;
+ margin-left: 0.5em;
+}
+
+.share-details {
+ &:before {
+ content: '(';
+ }
+
+ &:after {
+ content: ')';
+ }
+
+ span {
+ margin-right: 10px;
+ &:last-of-type{
+ margin-right: 0;
+ }
+ }
+}
+++ /dev/null
-{{-- __('!! Statistiques') --}}
-@extends(backpack_view('blank'))
-@push('after_scripts')
- {{-- Include the base libraries for the chart and the date picker here so that they're ready
- when the report HTML and extra scripts are injected --}}
-
- {{-- Charting library --}}
- <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"
- integrity="sha512-ElRFoEQdI5Ht6kZvyzXhYG9NqjtkmlkfYk0wr6wHxU9JEHakS7UJZNeml5ALk+8IKlU6jDgMabC3vkumRokgJA=="
- crossorigin="anonymous" referrerpolicy="no-referrer"></script>
-
- {{-- Date Range picker: https://www.daterangepicker.com/ --}}
- <script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment-with-locales.min.js"></script>
- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
-
- {{-- Simple Table Sorter --}}
- <script type="text/javascript" src="{{ asset('packages/sorttable/sorttable.js') }}"></script>
- {{-- This script works on any tables with the "sortable" class. There's no extra setup needed here. --}}
-@endpush
-
-@section('after_styles')
- <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css"/>
- <style>
- .chart-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
-
- table.stats-details {
- width: 100%;
- table-layout: fixed;
- }
-
- table.stats-details th, table.stats-details td {
- padding: 0.5em 0.75em;
- }
-
- table.stats-details td, .with-aliases {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- table.stats-details thead tr {
- position: sticky;
- top: 0;
- background-color: #fafafa;
- }
-
- table.stats-details thead th {
- border-bottom: medium solid rgba(0, 40, 100, .12);
- }
-
- [data-name="formatted_date"] {
- white-space: nowrap;
- }
-
- .table-columns {
- display: flex;
- flex-wrap: wrap;
- gap: 2em;
- }
-
- .table-columns > div {
- flex: 1;
- }
-
- .no-statistics {
- background-color: #fefce9;
- color: #854e18;
- padding: 1.5em;
- margin-top: 1.5em;
- }
-
- .heading-subtitle {
- opacity: 0.6;
- }
-
- .heading-subtitle:before {
- content: ' — ';
- }
-
- /*=== Date Range Picker ===*/
- [data-daterangepicker]:hover {
- color: #467fcf;
- }
-
- /*=== Table Column Sorter ===*/
- #sorttable_sortfwdind, #sorttable_sortrevind {
- display: none !important;
- }
-
- .sortable th:not(.sorttable_nosort) {
- cursor: pointer;
- white-space: nowrap;
- position: relative;
- font-size: 100% !important;
- }
-
- .sortable th:not(.sorttable_nosort):after, .sortable th:not(.sorttable_nosort):before {
- position: absolute;
- content: "\f0d7";
- font-family: Line Awesome Free;
- font-weight: normal;
- font-weight: 900;
- right: .4em;
- top: 50%;
- font-size: 14px !important;
- line-height: 1;
- opacity: 0.125;
- }
-
- .sortable th:not(.sorttable_nosort):before {
- content: "\f0d8";
- top: auto;
- bottom: 50%;
- }
-
- .sortable th:not(.sorttable_nosort).sorttable_sorted_reverse:after {
- opacity: 1;
- }
-
- .sortable th:not(.sorttable_nosort).sorttable_sorted:before {
- opacity: 1;
- }
-
- .whitespace-nowrap {
- white-space: nowrap;
- }
-
- .with-aliases .number {
- font-size: 75%;
- display: block;
- font-style: italic;
- margin-top: -2px;
- }
-
- .with-aliases .alias:after {
- content: ', ';
- }
-
- .with-aliases .alias:last-of-type:after {
- content: '';
- }
- </style>
-@endsection
-
-@section('content')
- <div id="stats_wrapper">
- @include('fluidbook_stats.summary')
- </div>
-@endsection
--- /dev/null
+{{-- __('!! Statistiques') --}}
+@extends(backpack_view('blank'))
+@push('after_scripts')
+ {{-- Include the base libraries for the chart and the date picker here so that they're ready
+ when the report HTML and extra scripts are injected --}}
+
+ {{-- Charting library --}}
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"
+ integrity="sha512-ElRFoEQdI5Ht6kZvyzXhYG9NqjtkmlkfYk0wr6wHxU9JEHakS7UJZNeml5ALk+8IKlU6jDgMabC3vkumRokgJA=="
+ crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+
+ {{-- Date Range picker: https://www.daterangepicker.com/ --}}
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment-with-locales.min.js"></script>
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
+
+ {{-- Simple Table Sorter --}}
+ <script type="text/javascript" src="{{ asset('packages/sorttable/sorttable.js') }}"></script>
+ {{-- This script works on any tables with the "sortable" class. There's no extra setup needed here. --}}
+@endpush
+
+@section('after_styles')
+ <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css"/>
+ <link rel="stylesheet" type="text/css" href="{{asset('packages/fluidbook/toolbox/css/stats.css')}}">
+@endsection
+
+@section('content')
+ <div id="stats_wrapper">
+ {{-- __('!! Statistiques') --}}
+ {{-- Statistics Report --}}
+ {{-- This doesn't extend any templates because it is fetched from loader.blade.php and injected via JS --}}
+
+ @php
+ $tableClasses='stats-details sortable bg-white table table-striped table-hover nowrap rounded shadow-xs border-xs mt-2 dataTable dtr-inline';
+ $fluidbookBaseURL=$fluidbook->getPreviewURL();
+ @endphp
+
+ <h2 class="mt-4">
+ {{ __('Statistiques') }}
+ </h2>
+
+ <div data-daterangepicker class="mb-4" style="cursor: pointer"
+ title="{{ __('Période').' - '.__('Cliquer pour changer l\'intervalle') }}">
+ <i class="las la-calendar-week align-middle mr-1" style="font-size: 32px;"></i>
+ <span class="date-range-text">
+ {!! $formatted_date_range !!}
+ </span>
+ </div>
+
+
+ <table class="{!! $tableClasses !!}">
+ <thead style="display: none">
+ </thead>
+ <tr>
+ <td>#</td>
+ <td>{{ $fluidbook->id }}</td>
+ </tr>
+ <tr>
+ <td>{{ __('Titre de la publication') }}</td>
+ <td>{{ $title }}</td>
+ </tr>
+ <tr>
+ <td>{{ __('Crée le') }}</td>
+ <td>
+ {{ $fluidbook->created_at->isoFormat('Do MMMM YYYY') }}
+ <span class="summary-details">
+ ({{ $fluidbook->created_at->diffForHumans([
+ 'parts' => 2, // How many levels of detail to go into (ie. years, months, days)
+ 'join' => true, // Join string with natural language separators for the locale
+ ])
+ }})
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td>{{ __('Nombre de pages') }}</td>
+ <td>{{ $page_count }}</td>
+ </tr>
+
+ {{-- Summary of totals --}}
+ @if($period_details->isNotEmpty())
+ @foreach ($table_map['summary'] as $summary_key => $summary_heading)
+ @if($summary_key === 'formatted_date' || $period_details->sum($summary_key)===0)
+ @continue
+ @endif
+ <tr>
+ <td>{{ $summary_heading }}</td>
+ <td>
+ {{ $formatter->format($period_details->sum($summary_key)) }}
+ @if($summary_key==='nb_shares' && $period_details->sum($summary_key)>0)
+ <span class="share-details">
+ @foreach($shareDetails as $type=>$details)
+ <span class="share-details-{{$type}}"><i class="{{$details['icon']}}"></i> {{$details['nb']}}</span>
+ @endforeach
+ </span>
+
+ @endif
+ </td>
+ </tr>
+ @endforeach
+
+ <tr>
+ <td>{{ __('Recherches') }}</td>
+ <td>{{ $formatter->format($searches->sum()) }}</td>
+ </tr>
+ @endif
+ </table>
+ <a id="chart"></a>
+ @if($period_details->isNotEmpty())
+
+ {{-- Period (segmentation) override controls [ Day / Week / Month / Year ] --}}
+ <div class="chart-header mt-5">
+
+ <h2>{!! $chart_heading !!}</h2>
+ <div class="btn-group m-t-10">
+ @foreach($period_map as $period_key => $period_title)
+ <a class="btn @if($period_key === $period)
+ btn-success
+ @else
+ btn-secondary
+@endif"
+ href="{{ route('stats', compact('fluidbook_id', 'hash') + ['date' => $date ?? '-', 'period_override' => $period_key]) }}#chart">
+ {{ $period_title['singular'] }}
+ </a>
+ @endforeach
+ </div>
+ </div>
+
+ {{-- Chart --}}
+ <div style="height: 500px">
+ <canvas id="stats_chart"></canvas>
+ </div>
+
+ {{-- Stats for each period entry (year, month, week or day) --}}
+ <table class="{!! $tableClasses !!}">
+ <thead>
+ <tr>
+ @foreach ($table_map['summary'] as $summary_heading_key => $summary_heading)
+ <th>
+ {{ $summary_heading }}
+ </th>
+ @endforeach
+ </tr>
+ </thead>
+ <tbody>
+ @foreach($period_details as $date_key => $period_data)
+ <tr>
+ @foreach (array_keys($table_map['summary']) as $summary_key)
+ <td data-name="{{ $summary_key }}"
+ @if($summary_key === 'formatted_date')sorttable_customkey="{{ $period_data['raw_date'] }}"@endif>
+ @if(isset($period_data[$summary_key]))
+ {!! is_int($period_data[$summary_key]) ? $formatter->format($period_data[$summary_key]) : $period_data[$summary_key] !!}
+ @else
+ -
+ @endif
+ </td>
+ @endforeach
+ </tr>
+ @endforeach
+ </tbody>
+ </table>
+
+ {{-- Stats segregated by page number --}}
+ <h3 class="mt-5">{{ __('Détails par page') }} <small>({!! $formatted_date_range !!})</small></h3>
+
+ <table class="{!! $tableClasses !!}">
+ <thead>
+ <tr>
+ @foreach ($table_map['per-page'] as $page_data_heading_key => $page_data_heading)
+ {{-- In the case of the "page_group" data, we want it to be sorted in ascending order by default, even though it's a numeric column --}}
+ <th @if($page_data_heading_key === 'page_group')@endif>
+ {{ $page_data_heading }}
+ </th>
+ @endforeach
+ </tr>
+ </thead>
+ <tbody>
+ @foreach($pages as $page_group => $page_data)
+ <tr>
+ @foreach (array_keys($table_map['per-page']) as $summary_key)
+ <td data-name="{{ $summary_key }}"
+ @if($summary_key === 'page_group')sorttable_customkey="{{ $page_data['page_number'] }}"@endif>
+ @if($summary_key === 'page_group')
+ @if(count($page_data['page_aliases'])>0)
+ <div class="with-aliases">
+ @foreach($page_data['page_aliases'] as $alias)
+ <span class="alias"><a target="_blank"
+ href="{!! $fluidbookBaseURL !!}#/page/{{\Fluidbook\Tools\Links\AnchorLink::normalizeAnchor($alias)}}">{{$alias}}</a></span>
+ @endforeach
+ @endif
+ <a class="number" target="_blank"
+ href="{!! $fluidbookBaseURL !!}#/page/{{ $page_data['page_number'] }}">{{$page_data[$summary_key]}}</a>
+ @if(count($page_data['page_aliases'])>0)
+ </div>
+ @endif
+ @else
+ {!! is_int($page_data[$summary_key]) ? $formatter->format($page_data[$summary_key]) : $page_data[$summary_key] !!}
+ @endif
+ </td>
+ @endforeach
+ </tr>
+ @endforeach
+ </tbody>
+ </table>
+
+ {{-- Additional stats tables organised into columns (outgoing links, search keywords and countries) --}}
+ {{-- Sometimes there are no stats for certain categories, so the number of columns adapts accordingly --}}
+ <div class="table-columns mt-5">
+
+ {{-- Outgoing Links --}}
+ @if($outlinks->isNotEmpty())
+ <div>
+ <h3>{{ __('Liens sortants') }}</h3>
+
+ <table class="{!! $tableClasses !!}">
+ <thead>
+ <tr>
+ <th>{{ __('URL') }}</th>
+ <th class="sorttable_sorted_reverse">{{ __('Clics') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach($outlinks as $link)
+ <tr>
+ <td sorttable_customkey="{{$link['label']}}">{!! \Cubist\Util\Url::linkIfisURL($link['label']) !!}</td>
+ <td>{{ $formatter->format($link['nb_events']) }}</td>
+ </tr>
+ @endforeach
+ </tbody>
+ </table>
+ </div>
+ @endif
+
+ {{-- Search Keywords --}}
+ @if($searches->isNotEmpty())
+ <div>
+ <h3>{{ __('Mots recherchés') }}</h3>
+
+ <table class="{!! $tableClasses !!}">
+ <thead>
+ <tr>
+ <th>{{ __('Requêtes') }}</th>
+ <th class="sorttable_sorted_reverse">{{ __('Nombre') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach($searches as $search_query => $search_count)
+ <tr>
+ <td class="whitespace-nowrap">{{ $search_query }}</td>
+ <td>{{ $formatter->format($search_count) }}</td>
+ </tr>
+ @endforeach
+ </tbody>
+ </table>
+ </div>
+ @endif
+
+ {{-- Visitor Countries --}}
+ @if($countries->isNotEmpty())
+ <div>
+ <h3>{{ __('Origine des visiteurs') }}</h3>
+
+ <table class="{!! $tableClasses !!}">
+ <thead>
+ <tr>
+ <th>{{ __('Pays') }}</th>
+ <th class="sorttable_sorted_reverse">{{ __('Nombre de visites') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach($countries as $country)
+ <tr>
+ <td class="whitespace-nowrap" data-sort-value="{{ $country['label'] }}">
+ <img src="{{ $country['flag'] }}" alt="{{ $country['label'] }}"
+ style="width: 1.5em; margin-right: 0.75em;">
+ {{ $country['label'] }}
+ </td>
+ <td>{{ $formatter->format($country['nb_visits']) }}</td>
+ </tr>
+ @endforeach
+ </tbody>
+ </table>
+ </div>
+ @endif
+
+ </div>
+
+
+ {{-- It's possible for there to be no statistics returned by the API --}}
+ @else
+ <div class="no-statistics">
+ <span style="vertical-align: middle; margin-right: 0.5em;">⚠</span>️
+ {{ __('Aucune visite ne correspond à cette période') }}
+ </div>
+ @endif
+
+ @can('fluidbook-publication:admin')
+ <div style="text-align: right;margin-top: 20px;">
+ <a target="_blank"
+ href="{{route('statsmatomo',['fluidbook_id'=>$fluidbook->id,'hash'=>$fluidbook->hash,'period'=>'range','date'=>$start_date.','.$end_date])}}">{{__('Voir les données brutes sur Matomo')}}</a>
+ @can('superadmin')
+ | <a target="_blank"
+ href="/fluidbook-publication/stats/API/{{$fluidbook->id}}">{{__('API Matomo')}}</a>
+ @endcan
+ </div>
+ @endcan
+
+ @push('after_scripts')
+ {{--================== SCRIPTS ==================--}}
+
+ {{-- Date Range picker setup: https://sensortower.github.io/daterangepicker/docs --}}
+ <script>
+ $(function () {
+ moment.locale('{{ $locale }}');
+ @if ($locale === 'en')
+ moment.updateLocale('{{ $locale }}', {
+ longDateFormat: {
+ // Date range picker uses the 'L' format for displaying dates
+ L: 'DD/MM/YYYY', // We don't like the default, backwards US date format
+ }
+ });
+ @endif
+
+ let baseURL = '{!! route('stats', compact('fluidbook_id', 'hash') + ['date' => '--range--', 'period_override' => $period]) !!}';
+
+ let creationDate = moment('{{ $fluidbook->created_at->isoFormat('YYYY-MM-DD') }}');
+ let now = moment();
+
+ $('[data-daterangepicker]').daterangepicker({
+ "locale": {
+ "format": "DD/MM/YYYY",
+ "separator": " - ",
+ "applyLabel": "{{__('Appliquer')}}",
+ "cancelLabel": "{{__('Annuler')}}",
+ "fromLabel": "{{__('De')}}",
+ "toLabel": "{{__('à')}}",
+ "customRangeLabel": "{{__('Personnalisé')}}",
+ "weekLabel": "W",
+ "daysOfWeek": [
+ "{{__('Lu')}}",
+ "{{__('Ma')}}",
+ "{{__('Me')}}",
+ "{{__('Je')}}",
+ "{{__('Ve')}}",
+ "{{__('Sa')}}",
+ "{{__('Di')}}"
+ ],
+ "monthNames": [
+ "{{__('Janvier')}}",
+ "{{__('Février')}}",
+ "{{__('Mars')}}",
+ "{{__('Avril')}}",
+ "{{__('Mai')}}",
+ "{{__('Juin')}}",
+ "{{__('Juillet')}}",
+ "{{__('Août')}}",
+ "{{__('Septembre')}}",
+ "{{__('Octobre')}}",
+ "{{__('Novembre')}}",
+ "{{__('Décembre')}}",
+ ],
+
+ "firstDay": 0
+ },
+ minDate: creationDate, // Creation date of the Fluidbook
+ maxDate: now,
+ startDate: moment('{{ $start_date }}'),
+ endDate: moment('{{ $end_date }}'),
+ ranges: {
+ '{{__('Ce mois')}}': [moment().startOf('month'), now],
+ '{{__('Les 30 derniers jours')}}': [moment().subtract(29, 'days'), moment()],
+ '{{__('Cette année')}}': [moment().startOf('year'), now],
+ '{{__('Depuis la création')}}': [creationDate, now],
+ },
+ "alwaysShowCalendars": true,
+ }, function (start, end) {
+ $('[data-daterangepicker] span').html(start.format('MMMM D, YYYY') + ' - ' + end.format('MMMM D, YYYY'));
+ window.location = baseURL.replace('--range--', start.format('YYYY-MM-DD') + "," + end.format('YYYY-MM-DD'));
+ });
+
+ {{--============================================================================================================--}}
+
+ {{-- Charting library --}}
+ //=== Chart Setup
+ const labels = {!! json_encode(array_keys($tooltip_labels)) !!};
+ const tooltip_labels = {!! json_encode($tooltip_labels) !!};
+ const data = {
+ labels: labels,
+ datasets: {!! $chart_datasets !!}
+ };
+
+ let maxvisits = 0;
+ $.each(data.datasets[1].data, function (k, v) {
+ if (v === null) {
+ return;
+ }
+ maxvisits = Math.max(maxvisits, v);
+ });
+
+ //=== Plugins
+ //== Offset Bars Plugin: creates a small offset between stacked bars
+ const offsetBars = {
+ id: 'offsetBars',
+ beforeDatasetsDraw(chart, args, options) {
+
+ // Create an offset between the stacked bars
+ chart.config.data.datasets.forEach(function (dataset, datasetIndex) {
+
+ const bar_offset_percentage = dataset.bar_offset || 0;
+
+ // Go through each data point (bar) and apply the horizontal positioning offset
+ chart.getDatasetMeta(datasetIndex).data.forEach(function (dataPoint, index) {
+
+ let offset = Math.round(dataPoint.width * bar_offset_percentage / 100);
+
+ // Make sure offset amount isn't too small
+ if (offset > 0 && offset < 2) {
+ offset = 2;
+ } else if (offset < 0 && offset > -2) {
+ offset = -2;
+ }
+
+ dataPoint.x = chart.scales.x.getPixelForValue(index) + offset;
+ });
+ });
+ }
+ };
+
+ //=== Chart Configuration
+ const config = {
+ type: 'bar',
+ data: data,
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ maxBarThickness: 20, // Prevent bars being ridiculously wide when there isn't much data
+ scales: {
+ x: {stacked: true},
+ y: {
+ type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
+ position: 'left',
+ stacked: false,
+ },
+ y2: {
+ type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
+ position: 'right',
+ reverse: false,
+ ticks: {
+ color: '#f54d00',
+ },
+ grid: {
+ drawOnChartArea: false // only want the grid lines for one axis to show up
+ },
+ max: maxvisits * 3,
+ stacked: false,
+ },
+ },
+ plugins: {
+ tooltip: {
+ mode: 'index',
+ position: 'nearest',
+ callbacks: {
+ title: function (context) {
+ return tooltip_labels[context[0].label];
+ }
+ },
+ }
+ }
+ },
+ plugins: [offsetBars]
+ };
+
+ //=== Render Chart
+ const statsChart = new Chart(
+ document.getElementById('stats_chart'),
+ config
+ );
+ });
+
+
+ </script>
+ @endpush
+
+ </div>
+@endsection
+++ /dev/null
-{{-- __('!! Statistiques') --}}
-{{-- Statistics Report --}}
-{{-- This doesn't extend any templates because it is fetched from loader.blade.php and injected via JS --}}
-
-@php
- $tableClasses='stats-details sortable bg-white table table-striped table-hover nowrap rounded shadow-xs border-xs mt-2 dataTable dtr-inline';
- $fluidbookBaseURL=$fluidbook->getPreviewURL();
-@endphp
-
-<h2 class="mt-4">
- {{ __('Statistiques') }}
-</h2>
-
-<div data-daterangepicker class="mb-4" style="cursor: pointer"
- title="{{ __('Période').' - '.__('Cliquer pour changer l\'intervalle') }}">
- <i class="las la-calendar-week align-middle mr-1" style="font-size: 32px;"></i>
- <span class="date-range-text">
- {!! $formatted_date_range !!}
- </span>
-</div>
-
-
-<table class="{!! $tableClasses !!}">
- <thead style="display: none">
- </thead>
- <tr>
- <td>#</td>
- <td>{{ $fluidbook->id }}</td>
- </tr>
- <tr>
- <td>{{ __('Titre de la publication') }}</td>
- <td>{{ $title }}</td>
- </tr>
- <tr>
- <td>{{ __('Crée le') }}</td>
- <td>
- {{ $fluidbook->created_at->isoFormat('Do MMMM YYYY') }}
- <span style="opacity: 0.6; display: inline-block; margin-left: 0.5em;">
- ({{ $fluidbook->created_at->diffForHumans([
- 'parts' => 2, // How many levels of detail to go into (ie. years, months, days)
- 'join' => true, // Join string with natural language separators for the locale
- ])
- }})
- </span>
- </td>
- </tr>
- <tr>
- <td>{{ __('Nombre de pages') }}</td>
- <td>{{ $page_count }}</td>
- </tr>
-
- {{-- Summary of totals --}}
- @if($period_details->isNotEmpty())
- @foreach ($table_map['summary'] as $summary_key => $summary_heading)
- @php
- if ($summary_key === 'formatted_date') continue;
- @endphp
- <tr>
- <td>{{ $summary_heading }}</td>
- <td>{{ $formatter->format($period_details->sum($summary_key)) }}</td>
- </tr>
- @endforeach
-
- <tr>
- <td>{{ __('Recherches') }}</td>
- <td>{{ $formatter->format($searches->sum()) }}</td>
- </tr>
- @endif
-</table>
-<a id="chart"></a>
-@if($period_details->isNotEmpty())
-
- {{-- Period (segmentation) override controls [ Day / Week / Month / Year ] --}}
- <div class="chart-header mt-5">
-
- <h2>{!! $chart_heading !!}</h2>
- <div class="btn-group m-t-10">
- @foreach($period_map as $period_key => $period_title)
- <a class="btn @if($period_key === $period)
- btn-success
- @else
- btn-secondary
-@endif"
- href="{{ route('stats', compact('fluidbook_id', 'hash') + ['date' => $date ?? '-', 'period_override' => $period_key]) }}#chart">
- {{ $period_title['singular'] }}
- </a>
- @endforeach
- </div>
- </div>
-
- {{-- Chart --}}
- <div style="height: 500px">
- <canvas id="stats_chart"></canvas>
- </div>
-
- {{-- Stats for each period entry (year, month, week or day) --}}
- <table class="{!! $tableClasses !!}">
- <thead>
- <tr>
- @foreach ($table_map['summary'] as $summary_heading_key => $summary_heading)
- <th>
- {{ $summary_heading }}
- </th>
- @endforeach
- </tr>
- </thead>
- <tbody>
- @foreach($period_details as $date_key => $period_data)
- <tr>
- @foreach (array_keys($table_map['summary']) as $summary_key)
- <td data-name="{{ $summary_key }}"
- @if($summary_key === 'formatted_date')sorttable_customkey="{{ $period_data['raw_date'] }}"@endif>
- @if(isset($period_data[$summary_key]))
- {!! is_int($period_data[$summary_key]) ? $formatter->format($period_data[$summary_key]) : $period_data[$summary_key] !!}
- @else
- -
- @endif
- </td>
- @endforeach
- </tr>
- @endforeach
- </tbody>
- </table>
-
- {{-- Stats segregated by page number --}}
- <h3 class="mt-5">{{ __('Détails par page') }} <small>({!! $formatted_date_range !!})</small></h3>
-
- <table class="{!! $tableClasses !!}">
- <thead>
- <tr>
- @foreach ($table_map['per-page'] as $page_data_heading_key => $page_data_heading)
- {{-- In the case of the "page_group" data, we want it to be sorted in ascending order by default, even though it's a numeric column --}}
- <th @if($page_data_heading_key === 'page_group')@endif>
- {{ $page_data_heading }}
- </th>
- @endforeach
- </tr>
- </thead>
- <tbody>
- @foreach($pages as $page_group => $page_data)
- <tr>
- @foreach (array_keys($table_map['per-page']) as $summary_key)
- <td data-name="{{ $summary_key }}"
- @if($summary_key === 'page_group')sorttable_customkey="{{ $page_data['page_number'] }}"@endif>
- @if($summary_key === 'page_group')
- @if(count($page_data['page_aliases'])>0)
- <div class="with-aliases">
- @foreach($page_data['page_aliases'] as $alias)
- <span class="alias"><a target="_blank"
- href="{!! $fluidbookBaseURL !!}#/page/{{\Fluidbook\Tools\Links\AnchorLink::normalizeAnchor($alias)}}">{{$alias}}</a></span>
- @endforeach
- @endif
- <a class="number" target="_blank"
- href="{!! $fluidbookBaseURL !!}#/page/{{ $page_data['page_number'] }}">{{$page_data[$summary_key]}}</a>
- @if(count($page_data['page_aliases'])>0)
- </div>
- @endif
- @else
- {!! is_int($page_data[$summary_key]) ? $formatter->format($page_data[$summary_key]) : $page_data[$summary_key] !!}
- @endif
- </td>
- @endforeach
- </tr>
- @endforeach
- </tbody>
- </table>
-
- {{-- Additional stats tables organised into columns (outgoing links, search keywords and countries) --}}
- {{-- Sometimes there are no stats for certain categories, so the number of columns adapts accordingly --}}
- <div class="table-columns mt-5">
-
- {{-- Outgoing Links --}}
- @if($outlinks->isNotEmpty())
- <div>
- <h3>{{ __('Liens sortants') }}</h3>
-
- <table class="{!! $tableClasses !!}">
- <thead>
- <tr>
- <th>{{ __('URL') }}</th>
- <th class="sorttable_sorted_reverse">{{ __('Clics') }}</th>
- </tr>
- </thead>
- <tbody>
- @foreach($outlinks as $link)
- <tr>
- <td sorttable_customkey="{{$link['label']}}">{!! \Cubist\Util\Url::linkIfisURL($link['label']) !!}</td>
- <td>{{ $formatter->format($link['nb_events']) }}</td>
- </tr>
- @endforeach
- </tbody>
- </table>
- </div>
- @endif
-
- {{-- Search Keywords --}}
- @if($searches->isNotEmpty())
- <div>
- <h3>{{ __('Mots recherchés') }}</h3>
-
- <table class="{!! $tableClasses !!}">
- <thead>
- <tr>
- <th>{{ __('Requêtes') }}</th>
- <th class="sorttable_sorted_reverse">{{ __('Nombre') }}</th>
- </tr>
- </thead>
- <tbody>
- @foreach($searches as $search_query => $search_count)
- <tr>
- <td class="whitespace-nowrap">{{ $search_query }}</td>
- <td>{{ $formatter->format($search_count) }}</td>
- </tr>
- @endforeach
- </tbody>
- </table>
- </div>
- @endif
-
- {{-- Visitor Countries --}}
- @if($countries->isNotEmpty())
- <div>
- <h3>{{ __('Origine des visiteurs') }}</h3>
-
- <table class="{!! $tableClasses !!}">
- <thead>
- <tr>
- <th>{{ __('Pays') }}</th>
- <th class="sorttable_sorted_reverse">{{ __('Nombre de visites') }}</th>
- </tr>
- </thead>
- <tbody>
- @foreach($countries as $country)
- <tr>
- <td class="whitespace-nowrap" data-sort-value="{{ $country['label'] }}">
- <img src="{{ $country['flag'] }}" alt="{{ $country['label'] }}"
- style="width: 1.5em; margin-right: 0.75em;">
- {{ $country['label'] }}
- </td>
- <td>{{ $formatter->format($country['nb_visits']) }}</td>
- </tr>
- @endforeach
- </tbody>
- </table>
- </div>
- @endif
-
- </div>
-
-
- {{-- It's possible for there to be no statistics returned by the API --}}
-@else
- <div class="no-statistics">
- <span style="vertical-align: middle; margin-right: 0.5em;">⚠</span>️
- {{ __('Aucune visite ne correspond à cette période') }}
- </div>
-@endif
-
-@can('fluidbook-publication:admin')
- <div style="text-align: right;margin-top: 20px;">
- <a target="_blank"
- href="{{route('statsmatomo',['fluidbook_id'=>$fluidbook->id,'hash'=>$fluidbook->hash,'period'=>'range','date'=>$start_date.','.$end_date])}}">{{__('Voir les données brutes sur Matomo')}}</a>
- @can('superadmin')
- | <a target="_blank" href="/fluidbook-publication/stats/API/{{$fluidbook->id}}">{{__('API Matomo')}}</a>
- @endcan
- </div>
-@endcan
-
-@push('after_scripts')
- {{--================== SCRIPTS ==================--}}
-
- {{-- Date Range picker setup: https://sensortower.github.io/daterangepicker/docs --}}
- <script>
- $(function () {
- moment.locale('{{ $locale }}');
- @if ($locale === 'en')
- moment.updateLocale('{{ $locale }}', {
- longDateFormat: {
- // Date range picker uses the 'L' format for displaying dates
- L: 'DD/MM/YYYY', // We don't like the default, backwards US date format
- }
- });
- @endif
-
- let baseURL = '{!! route('stats', compact('fluidbook_id', 'hash') + ['date' => '--range--', 'period_override' => $period]) !!}';
-
- let creationDate = moment('{{ $fluidbook->created_at->isoFormat('YYYY-MM-DD') }}');
- let now = moment();
-
- $('[data-daterangepicker]').daterangepicker({
- "locale": {
- "format": "DD/MM/YYYY",
- "separator": " - ",
- "applyLabel": "{{__('Appliquer')}}",
- "cancelLabel": "{{__('Annuler')}}",
- "fromLabel": "{{__('De')}}",
- "toLabel": "{{__('à')}}",
- "customRangeLabel": "{{__('Personnalisé')}}",
- "weekLabel": "W",
- "daysOfWeek": [
- "{{__('Lu')}}",
- "{{__('Ma')}}",
- "{{__('Me')}}",
- "{{__('Je')}}",
- "{{__('Ve')}}",
- "{{__('Sa')}}",
- "{{__('Di')}}"
- ],
- "monthNames": [
- "{{__('Janvier')}}",
- "{{__('Février')}}",
- "{{__('Mars')}}",
- "{{__('Avril')}}",
- "{{__('Mai')}}",
- "{{__('Juin')}}",
- "{{__('Juillet')}}",
- "{{__('Août')}}",
- "{{__('Septembre')}}",
- "{{__('Octobre')}}",
- "{{__('Novembre')}}",
- "{{__('Décembre')}}",
- ],
-
- "firstDay": 0
- },
- minDate: creationDate, // Creation date of the Fluidbook
- maxDate: now,
- startDate: moment('{{ $start_date }}'),
- endDate: moment('{{ $end_date }}'),
- ranges: {
- '{{__('Ce mois')}}': [moment().startOf('month'), now],
- '{{__('Les 30 derniers jours')}}': [moment().subtract(29, 'days'), moment()],
- '{{__('Cette année')}}': [moment().startOf('year'), now],
- '{{__('Depuis la création')}}': [creationDate, now],
- },
- "alwaysShowCalendars": true,
- }, function (start, end) {
- $('[data-daterangepicker] span').html(start.format('MMMM D, YYYY') + ' - ' + end.format('MMMM D, YYYY'));
- window.location = baseURL.replace('--range--', start.format('YYYY-MM-DD') + "," + end.format('YYYY-MM-DD'));
- });
-
- {{--============================================================================================================--}}
-
- {{-- Charting library --}}
- //=== Chart Setup
- const labels = {!! json_encode(array_keys($tooltip_labels)) !!};
- const tooltip_labels = {!! json_encode($tooltip_labels) !!};
- const data = {
- labels: labels,
- datasets: {!! $chart_datasets !!}
- };
-
- let maxvisits = 0;
- $.each(data.datasets[1].data, function (k, v) {
- if (v === null) {
- return;
- }
- maxvisits = Math.max(maxvisits, v);
- });
-
- //=== Plugins
- //== Offset Bars Plugin: creates a small offset between stacked bars
- const offsetBars = {
- id: 'offsetBars',
- beforeDatasetsDraw(chart, args, options) {
-
- // Create an offset between the stacked bars
- chart.config.data.datasets.forEach(function (dataset, datasetIndex) {
-
- const bar_offset_percentage = dataset.bar_offset || 0;
-
- // Go through each data point (bar) and apply the horizontal positioning offset
- chart.getDatasetMeta(datasetIndex).data.forEach(function (dataPoint, index) {
-
- let offset = Math.round(dataPoint.width * bar_offset_percentage / 100);
-
- // Make sure offset amount isn't too small
- if (offset > 0 && offset < 2) {
- offset = 2;
- } else if (offset < 0 && offset > -2) {
- offset = -2;
- }
-
- dataPoint.x = chart.scales.x.getPixelForValue(index) + offset;
- });
- });
- }
- };
-
- //=== Chart Configuration
- const config = {
- type: 'bar',
- data: data,
- options: {
- responsive: true,
- maintainAspectRatio: false,
- maxBarThickness: 20, // Prevent bars being ridiculously wide when there isn't much data
- scales: {
- x: {stacked: true},
- y: {
- type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
- position: 'left',
- stacked: false,
- },
- y2: {
- type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
- position: 'right',
- reverse: false,
- ticks: {
- color: '#f54d00',
- },
- grid: {
- drawOnChartArea: false // only want the grid lines for one axis to show up
- },
- max: maxvisits * 3,
- stacked: false,
- },
- },
- plugins: {
- tooltip: {
- mode: 'index',
- position: 'nearest',
- callbacks: {
- title: function (context) {
- return tooltip_labels[context[0].label];
- }
- },
- }
- }
- },
- plugins: [offsetBars]
- };
-
- //=== Render Chart
- const statsChart = new Chart(
- document.getElementById('stats_chart'),
- config
- );
- });
-
-
- </script>
-@endpush