diff --git a/.gitignore b/.gitignore
index 18ffc85..c0aa6b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,5 @@ yarn-error.log
/public/uploads/
!/public/uploads/.gitkeep
/public/media/
+/migrations/
+!/migrations/.gitkeep
diff --git a/assets/css/admin.scss b/assets/css/admin.scss
index 668eeef..40bfaf3 100644
--- a/assets/css/admin.scss
+++ b/assets/css/admin.scss
@@ -511,6 +511,8 @@ form {
.modal-dialog-large {
max-width: 80%;
+ margin-left: auto;
+ margin-right: auto;
}
.output {
diff --git a/assets/js/admin/admin.js b/assets/js/admin/admin.js
index 0bf8794..4430472 100644
--- a/assets/js/admin/admin.js
+++ b/assets/js/admin/admin.js
@@ -23,3 +23,4 @@ require('./modules/sortable.js')()
require('./modules/batch.js')()
require('./modules/file-manager.js')()
require('./modules/file-picker.js')()
+require('./modules/analytics.js')()
diff --git a/assets/js/admin/modules/analytics.js b/assets/js/admin/modules/analytics.js
new file mode 100644
index 0000000..dcb6d1a
--- /dev/null
+++ b/assets/js/admin/modules/analytics.js
@@ -0,0 +1,53 @@
+const $ = require('jquery')
+const Chart = require('chart.js/auto').default
+
+const drawChart = () => {
+ const ctx = document.getElementById('analytic-chart')
+ const options = {
+ type: 'bar',
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ y: {
+ beginAtZero: true
+ }
+ }
+ },
+ data: {
+ labels: JSON.parse(ctx.getAttribute('data-labels')),
+ datasets: [{
+ label: ctx.getAttribute('data-label'),
+ data: JSON.parse(ctx.getAttribute('data-values')),
+ backgroundColor: 'rgba(54, 162, 235, 0.2)',
+ borderColor: 'rgb(54, 162, 235)',
+ borderWidth: 1
+ }]
+ }
+ }
+
+ const chart = new Chart(ctx, options)
+
+ const resize = () => {
+ const width = ctx.parentNode.parentNode.offsetWidth
+ const height = 250
+
+ chart.resize(width, height)
+ }
+
+ resize()
+
+ window.addEventListener('resize', resize)
+}
+
+module.exports = () => {
+ const body = $('body')
+
+ body.on('shown.bs.modal', '.modal', (e) => {
+ window.setTimeout(() => {
+ if (document.getElementById('analytic-chart')) {
+ drawChart()
+ }
+ }, 500)
+ })
+}
diff --git a/assets/js/admin/modules/modal.js b/assets/js/admin/modules/modal.js
index 8afb28a..13cecba 100644
--- a/assets/js/admin/modules/modal.js
+++ b/assets/js/admin/modules/modal.js
@@ -3,8 +3,10 @@ const $ = require('jquery')
const openModal = function (url) {
let container = $('#modal-container')
const body = $('body')
+ let doTrigger = true
if (!container.length) {
+ let doTrigger = false
container = $('
')
body.append(container)
@@ -20,6 +22,10 @@ const openModal = function (url) {
container.load(url, function () {
loader.remove()
+
+ if (doTrigger) {
+ container.trigger('shown.bs.modal')
+ }
})
}
diff --git a/composer.json b/composer.json
index 266ff76..dc97ae6 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,7 @@
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.8",
"friendsofsymfony/jsrouting-bundle": "^2.7",
+ "jaybizzle/crawler-detect": "^1.2",
"knplabs/doctrine-behaviors": "^2.2",
"knplabs/knp-paginator-bundle": "^5.8",
"liip/imagine-bundle": "^2.6",
diff --git a/config/services.yaml b/config/services.yaml
index 88fe2f6..4d04c70 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -23,6 +23,10 @@ services:
tags:
- { name: kernel.event_listener, event: kernel.exception }
+ App\Core\EventListener\AnalyticListener:
+ tags:
+ - { name: kernel.event_listener, event: kernel.request }
+
App\:
resource: '../src/'
exclude:
diff --git a/core/Analytic/DateRangeAnalytic.php b/core/Analytic/DateRangeAnalytic.php
new file mode 100644
index 0000000..ce4a60f
--- /dev/null
+++ b/core/Analytic/DateRangeAnalytic.php
@@ -0,0 +1,186 @@
+
+ */
+class DateRangeAnalytic
+{
+ protected ViewRepositoryQuery $viewQuery;
+ protected RefererRepositoryQuery $refererQuery;
+ protected ?Node $node;
+ protected ?\DateTime $from;
+ protected ?\DateTime $to;
+ protected bool $reload = true;
+ protected array $cache = [];
+
+ public function __construct(ViewRepositoryQuery $viewQuery, RefererRepositoryQuery $refererQuery)
+ {
+ $this->viewQuery = $viewQuery;
+ $this->refererQuery = $refererQuery;
+ }
+
+ public function getViews(): array
+ {
+ $entities = $this->getEntities('view');
+ $this->reload = false;
+
+ if ($entities) {
+ $first = $entities[0];
+ $last = $entities[count($entities) - 1];
+
+ $diff = $first->getDate()->diff($last->getDate());
+
+ if ($diff->days >= 90) {
+ $format = 'Y-m';
+ } else {
+ $format = 'Y-m-d';
+ }
+ }
+
+ $datas = [];
+
+ foreach ($entities as $entity) {
+ $index = $entity->getDate()->format($format);
+
+ if (!isset($datas[$index])) {
+ $datas[$index] = 0;
+ }
+
+ $datas[$index] += $entity->getViews();
+ }
+
+ arsort($datas, SORT_NUMERIC);
+
+ return $datas;
+ }
+
+ public function getPathViews(): array
+ {
+ $entities = $this->getEntities('view');
+ $this->reload = false;
+
+ $datas = [];
+
+ foreach ($entities as $entity) {
+ $index = $entity->getPath();
+
+ if (!isset($datas[$index])) {
+ $datas[$index] = 0;
+ }
+
+ $datas[$index] += $entity->getViews();
+ }
+
+ arsort($datas, SORT_NUMERIC);
+
+ return $datas;
+ }
+
+ public function getReferers(): array
+ {
+ $entities = $this->getEntities('referer');
+ $this->reload = false;
+
+ $datas = [];
+
+ foreach ($entities as $entity) {
+ $index = parse_url($entity->getUri(), PHP_URL_HOST);
+
+ if (!isset($datas[$index])) {
+ $datas[$index] = [
+ 'views' => 0,
+ 'uris' => [],
+ ];
+ }
+
+ $datas[$index]['views'] += $entity->getViews();
+
+ $path = parse_url($entity->getUri(), PHP_URL_PATH);
+
+ if (!isset($datas[$index]['uris'][$path])) {
+ $datas[$index]['uris'][$path] = 0;
+ }
+
+ $datas[$index]['uris'][$path] += $entity->getViews();
+ }
+
+ uasort($datas, function($a, $b) {
+ if ($a['views'] > $b['views']) {
+ return -1;
+ }
+
+ if ($a['views'] < $b['views']) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ return $datas;
+ }
+
+ public function setDateRange(?\DateTime $from, ?\DateTime $to): self
+ {
+ $this->from = $from;
+ $this->to = $to;
+ $this->reload = true;
+
+ return $this;
+ }
+
+ public function setNode(?Node $node): self
+ {
+ $this->node = $node;
+ $this->reload = true;
+
+ return $this;
+ }
+
+ protected function getEntities(string $type): array
+ {
+ if ('view' === $type) {
+ $query = $this->viewQuery->create();
+ } elseif ('referer' === $type) {
+ $query = $this->refererQuery->create();
+ } else {
+ throw new \InvalidArgumentException('Invalid type');
+ }
+
+ if (!$this->reload && isset($this->cache[$type])) {
+ return $this->cache[$type];
+ }
+
+ if (null !== $this->from) {
+ $query
+ ->andWhere('.date >= :from')
+ ->setParameter(':from', $this->from)
+ ;
+ }
+
+ if (null !== $this->to) {
+ $query
+ ->andWhere('.date <= :to')
+ ->setParameter(':to', $this->to)
+ ;
+ }
+
+ if (null !== $this->node) {
+ $query
+ ->andWhere('.node = :node')
+ ->setParameter(':node', $this->node->getId())
+ ;
+ }
+
+ $this->cache[$type] = $query->orderBy('.date')->find();
+
+ return $this->cache[$type];
+ }
+}
diff --git a/core/Controller/Analytic/AnalyticController.php b/core/Controller/Analytic/AnalyticController.php
new file mode 100644
index 0000000..6d2fd88
--- /dev/null
+++ b/core/Controller/Analytic/AnalyticController.php
@@ -0,0 +1,38 @@
+createNotFoundException();
+ }
+
+ $analytic
+ ->setDateRange(new \DateTime('now - '.$range), new \DateTime())
+ ->setNode($node)
+ ;
+
+ return $this->render('@Core/analytic/stats.html.twig', [
+ 'range' => $range,
+ 'views' => $analytic->getViews(),
+ 'pathViews' => $analytic->getPathViews(),
+ 'referers' => $analytic->getReferers(),
+ 'node' => $node,
+ ]);
+ }
+}
diff --git a/core/Controller/Site/NodeAdminController.php b/core/Controller/Site/NodeAdminController.php
index 267cf68..327bd34 100644
--- a/core/Controller/Site/NodeAdminController.php
+++ b/core/Controller/Site/NodeAdminController.php
@@ -21,11 +21,12 @@ use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
/**
* @Route("/admin/site/node")
*/
-class NodeAdminController extends AdminController
+class NodeAdminController extends AbstractController
{
/**
* @Route("/new/{node}", name="admin_site_node_new")
@@ -268,11 +269,6 @@ class NodeAdminController extends AdminController
]);
}
- public function getSection(): string
- {
- return '';
- }
-
protected function handlePageAssociation(
string $pageAction,
?Page $pageEntity,
diff --git a/core/Entity/Analytic/Referer.php b/core/Entity/Analytic/Referer.php
new file mode 100644
index 0000000..1c8b94d
--- /dev/null
+++ b/core/Entity/Analytic/Referer.php
@@ -0,0 +1,103 @@
+id;
+ }
+
+ public function getNode(): ?Node
+ {
+ return $this->node;
+ }
+
+ public function setNode(?Node $node): self
+ {
+ $this->node = $node;
+
+ return $this;
+ }
+
+ public function getUri(): ?string
+ {
+ return $this->uri;
+ }
+
+ public function setUri(string $uri): self
+ {
+ $this->uri = $uri;
+
+ return $this;
+ }
+
+ public function getViews(): ?int
+ {
+ return $this->views;
+ }
+
+ public function setViews(int $views): self
+ {
+ $this->views = $views;
+
+ return $this;
+ }
+
+ public function addView(): self
+ {
+ ++$this->views;
+
+ return $this;
+ }
+
+ public function getDate(): ?\DateTimeInterface
+ {
+ return $this->date;
+ }
+
+ public function setDate(\DateTimeInterface $date): self
+ {
+ $this->date = $date;
+
+ return $this;
+ }
+}
diff --git a/core/Entity/Analytic/View.php b/core/Entity/Analytic/View.php
new file mode 100644
index 0000000..0bd3338
--- /dev/null
+++ b/core/Entity/Analytic/View.php
@@ -0,0 +1,103 @@
+id;
+ }
+
+ public function getNode(): ?Node
+ {
+ return $this->node;
+ }
+
+ public function setNode(?Node $node): self
+ {
+ $this->node = $node;
+
+ return $this;
+ }
+
+ public function getPath(): ?string
+ {
+ return $this->path;
+ }
+
+ public function setPath(string $path): self
+ {
+ $this->path = $path;
+
+ return $this;
+ }
+
+ public function getViews(): ?int
+ {
+ return $this->views;
+ }
+
+ public function setViews(int $views): self
+ {
+ $this->views = $views;
+
+ return $this;
+ }
+
+ public function addView(): self
+ {
+ ++$this->views;
+
+ return $this;
+ }
+
+ public function getDate(): ?\DateTimeInterface
+ {
+ return $this->date;
+ }
+
+ public function setDate(\DateTimeInterface $date): self
+ {
+ $this->date = $date;
+
+ return $this;
+ }
+}
diff --git a/core/Entity/Site/Node.php b/core/Entity/Site/Node.php
index ab7d6ef..4196966 100644
--- a/core/Entity/Site/Node.php
+++ b/core/Entity/Site/Node.php
@@ -4,6 +4,7 @@ namespace App\Core\Entity\Site;
use App\Core\Doctrine\Timestampable;
use App\Core\Entity\EntityInterface;
+use App\Core\Entity\NodeView;
use App\Core\Entity\Site\Page\Page;
use App\Core\Repository\Site\NodeRepository;
use Doctrine\Common\Collections\ArrayCollection;
@@ -11,6 +12,8 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use function Symfony\Component\String\u;
+use App\Core\Entity\Analytic\View;
+use App\Core\Entity\Analytic\Referer;
/**
* @Gedmo\Tree(type="nested")
@@ -139,10 +142,27 @@ class Node implements EntityInterface
*/
protected $contentType;
+ /**
+ * @ORM\Column(type="boolean", options={"default"=0})
+ */
+ protected $enableAnalytics = false;
+
+ /**
+ * @ORM\OneToMany(targetEntity=View::class, mappedBy="node")
+ */
+ protected $analyticViews;
+
+ /**
+ * @ORM\OneToMany(targetEntity=Referer::class, mappedBy="node")
+ */
+ protected $analyticReferers;
+
public function __construct()
{
$this->children = new ArrayCollection();
$this->aliasNodes = new ArrayCollection();
+ $this->analyticViews = new ArrayCollection();
+ $this->analyticReferers = new ArrayCollection();
}
public function getId(): ?int
@@ -548,4 +568,76 @@ class Node implements EntityInterface
return $this;
}
+
+ public function getEnableAnalytics(): ?bool
+ {
+ return $this->enableAnalytics;
+ }
+
+ public function setEnableAnalytics(bool $enableAnalytics): self
+ {
+ $this->enableAnalytics = $enableAnalytics;
+
+ return $this;
+ }
+
+ /**
+ * @return Collection|View[]
+ */
+ public function getAnalyticViews(): Collection
+ {
+ return $this->analyticViews;
+ }
+
+ public function addAnalyticView(View $view): self
+ {
+ if (!$this->analyticViews->contains($view)) {
+ $this->analyticViews[] = $view;
+ $view->setNode($this);
+ }
+
+ return $this;
+ }
+
+ public function removeAnalyticView(View $view): self
+ {
+ if ($this->analyticViews->removeElement($view)) {
+ // set the owning side to null (unless already changed)
+ if ($view->getNode() === $this) {
+ $view->setNode(null);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return Collection|Referer[]
+ */
+ public function getAnalyticReferers(): Collection
+ {
+ return $this->analyticReferers;
+ }
+
+ public function addAnalyticReferer(Referer $referer): self
+ {
+ if (!$this->analyticReferers->contains($referer)) {
+ $this->analyticReferers[] = $referer;
+ $referer->setNode($this);
+ }
+
+ return $this;
+ }
+
+ public function removeAnalyticReferer(Referer $referer): self
+ {
+ if ($this->analyticReferers->removeElement($referer)) {
+ // set the owning side to null (unless already changed)
+ if ($referer->getNode() === $this) {
+ $referer->setNode(null);
+ }
+ }
+
+ return $this;
+ }
}
diff --git a/core/EventListener/AnalyticListener.php b/core/EventListener/AnalyticListener.php
new file mode 100644
index 0000000..515e39d
--- /dev/null
+++ b/core/EventListener/AnalyticListener.php
@@ -0,0 +1,134 @@
+
+ */
+class AnalyticListener
+{
+ protected NodeRepository $nodeRepository;
+ protected ViewRepositoryQuery $viewRepositoryQuery;
+ protected ViewFactory $viewFactory;
+ protected RefererRepositoryQuery $refererRepositoryQuery;
+ protected RefererFactory $refererFactory;
+ protected EntityManager $manager;
+ protected CrawlerDetect $crawlerDetect;
+ protected Request $request;
+ protected Node $node;
+
+ public function __construct(
+ NodeRepository $nodeRepository,
+ ViewRepositoryQuery $viewRepositoryQuery,
+ ViewFactory $viewFactory,
+ RefererRepositoryQuery $refererRepositoryQuery,
+ RefererFactory $refererFactory,
+ EntityManager $manager
+ ) {
+ $this->nodeRepository = $nodeRepository;
+ $this->viewRepositoryQuery = $viewRepositoryQuery;
+ $this->viewFactory = $viewFactory;
+ $this->refererRepositoryQuery = $refererRepositoryQuery;
+ $this->refererFactory = $refererFactory;
+ $this->manager = $manager;
+ $this->crawlerDetect = new CrawlerDetect();
+ }
+
+ public function onKernelRequest(RequestEvent $event)
+ {
+ $request = $event->getRequest();
+
+ if (!$request->attributes->has('_node')) {
+ return;
+ }
+
+ if ($this->crawlerDetect->isCrawler($request->headers->get('user-agent'))) {
+ return;
+ }
+
+ $node = $this->nodeRepository->findOneBy([
+ 'id' => $request->attributes->get('_node'),
+ 'enableAnalytics' => true,
+ ]);
+
+ if (!$node) {
+ return;
+ }
+
+ $this->node = $node;
+ $this->request = $request;
+
+ $this->createView();
+ $this->createReferer();
+ }
+
+ protected function createView()
+ {
+ $entity = $this->viewRepositoryQuery->create()
+ ->filterByRequest($this->request)
+ ->andWhere('.date=CURRENT_DATE()')
+ ->findOne()
+ ;
+
+ if (!$entity) {
+ $entity = $this->viewFactory->create($this->node, $this->request->getPathInfo());
+ }
+
+ $entity->addView();
+ $this->save($entity);
+ }
+
+ protected function createReferer()
+ {
+ if (!$this->request->headers->has('referer')) {
+ return;
+ }
+
+ $referer = $this->request->headers->get('referer');
+
+ if (!filter_var($referer, FILTER_VALIDATE_URL) || parse_url($url, PHP_URL_SCHEME)) {
+ return;
+ }
+
+ if (!in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'])) {
+ return;
+ }
+
+ $entity = $this->refererRepositoryQuery->create()
+ ->filterByRequest($this->request)
+ ->andWhere('.date=CURRENT_DATE()')
+ ->findOne()
+ ;
+
+ if (!$entity) {
+ $entity = $this->refererFactory->create($this->node, $referer);
+ }
+
+ $entity->addView();
+ $this->save($entity);
+ }
+
+ protected function save(EntityInterface $entity)
+ {
+ if ($entity->getId()) {
+ $this->manager->update($entity);
+ } else {
+ $this->manager->create($entity);
+ }
+ }
+}
diff --git a/core/Factory/Analytic/RefererFactory.php b/core/Factory/Analytic/RefererFactory.php
new file mode 100644
index 0000000..8884338
--- /dev/null
+++ b/core/Factory/Analytic/RefererFactory.php
@@ -0,0 +1,22 @@
+setNode($node)
+ ->setUri($uri)
+ ->setDate(new \DateTime())
+ ;
+
+ return $entity;
+ }
+}
diff --git a/core/Factory/Analytic/ViewFactory.php b/core/Factory/Analytic/ViewFactory.php
new file mode 100644
index 0000000..840c7ad
--- /dev/null
+++ b/core/Factory/Analytic/ViewFactory.php
@@ -0,0 +1,22 @@
+setNode($node)
+ ->setPath($path)
+ ->setDate(new \DateTime())
+ ;
+
+ return $entity;
+ }
+}
diff --git a/core/Form/Site/NodeType.php b/core/Form/Site/NodeType.php
index 5c9b314..284d0f9 100644
--- a/core/Form/Site/NodeType.php
+++ b/core/Form/Site/NodeType.php
@@ -60,6 +60,19 @@ class NodeType extends AbstractType
]
);
+ $builder->add(
+ 'enableAnalytics',
+ CheckboxType::class,
+ [
+ 'label' => 'Enable analytics',
+ 'required' => false,
+ 'attr' => [
+ ],
+ 'constraints' => [
+ ],
+ ]
+ );
+
$builder->add(
'code',
TextType::class,
diff --git a/core/Repository/Analytic/RefererRepository.php b/core/Repository/Analytic/RefererRepository.php
new file mode 100644
index 0000000..2bd6b73
--- /dev/null
+++ b/core/Repository/Analytic/RefererRepository.php
@@ -0,0 +1,21 @@
+andWhere('.node = :node')
+ ->andWhere('.uri = :uri')
+ ->setParameters([
+ ':node' => $request->attributes->get('_node'),
+ ':uri' => $request->headers->get('referer'),
+ ])
+ ;
+ }
+}
diff --git a/core/Repository/Analytic/ViewRepository.php b/core/Repository/Analytic/ViewRepository.php
new file mode 100644
index 0000000..4ddf0a3
--- /dev/null
+++ b/core/Repository/Analytic/ViewRepository.php
@@ -0,0 +1,21 @@
+andWhere('.node = :node')
+ ->andWhere('.path = :path')
+ ->setParameters([
+ ':node' => $request->attributes->get('_node'),
+ ':path' => $request->getPathInfo(),
+ ])
+ ;
+ }
+}
diff --git a/core/Resources/translations/messages.fr.yaml b/core/Resources/translations/messages.fr.yaml
index 0204b3e..83e5f44 100644
--- a/core/Resources/translations/messages.fr.yaml
+++ b/core/Resources/translations/messages.fr.yaml
@@ -39,6 +39,14 @@
"Never": "Jamais"
"URL": "URL"
"Disable URL": "Désactiver l'URL"
+"Enable analytics": "Activer les analyses"
+"Analytics": "Analyses"
+"Referers": "Référents"
+"Views": "Vues"
+"Last 7 days": "Les 7 derniers jours"
+"Last 30 days": "Les 30 derniers jours"
+"Last 90 days": "Les 90 derniers jours"
+"Last year": "L'année passée"
"Controller": "Contrôleur"
"Leave blank for automatic generation": "Laisser vide pour une génération automatique"
"Leave blank to use the default one. Example: App\\Controller\\FooController::barAction": "Laisser vide pour utiliser celui par défaut. Exemple : App\\Controller\\FooController::barAction"
diff --git a/core/Resources/views/analytic/stats.html.twig b/core/Resources/views/analytic/stats.html.twig
new file mode 100644
index 0000000..8886d4a
--- /dev/null
+++ b/core/Resources/views/analytic/stats.html.twig
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+ {% set values = [] %}
+ {% for item in views %}{% set values = values|merge([item]) %}{% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'Path'|trans }} |
+ {{ 'Views'|trans }} |
+
+
+
+ {% for path, views in pathViews %}
+
+ {{ path }} |
+ {{ views }} |
+
+ {% else %}
+
+
+
+
+
+
+ {{ 'No result'|trans }}
+
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+ {{ 'Referers'|trans }} |
+ {{ 'Views'|trans }} |
+
+
+
+ {% for referer, info in referers %}
+
+
+
+
+
+ {{ info.views }}
+
+
+ {{ referer }}
+
+
+ {% for path, views in info.uris %}
+
+
+ {{ info.views }}
+
+
+ {{ path }}
+
+ {% endfor %}
+
+ |
+
+ {% else %}
+
+
+
+
+
+
+ {{ 'No result'|trans }}
+
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
diff --git a/core/Resources/views/site/node_admin/_form.html.twig b/core/Resources/views/site/node_admin/_form.html.twig
index 9fb460f..42079fd 100644
--- a/core/Resources/views/site/node_admin/_form.html.twig
+++ b/core/Resources/views/site/node_admin/_form.html.twig
@@ -205,6 +205,7 @@
{{ form_row(form.disableUrl) }}
+ {{ form_row(form.enableAnalytics) }}
{{ form_row(form.code) }}
{{ form_row(form.contentType) }}
{{ form_row(form.controller) }}
diff --git a/core/Resources/views/site/tree_admin/navigation.html.twig b/core/Resources/views/site/tree_admin/navigation.html.twig
index b077da6..31efad1 100644
--- a/core/Resources/views/site/tree_admin/navigation.html.twig
+++ b/core/Resources/views/site/tree_admin/navigation.html.twig
@@ -72,6 +72,7 @@
{% set move = path('admin_site_node_move', {entity: node.id}) %}
{% set edit = path('admin_site_node_edit', {entity: node.id}) %}
{% set new = path('admin_site_node_new', {node: node.id}) %}
+ {% set analytics = path('admin_analytic_stats', {node: node.id}) %}
@@ -147,6 +148,12 @@
{% endif %}
+ {% if node.enableAnalytics %}
+
+ {% endif %}
+
@@ -181,6 +188,12 @@
{% endif %}
+ {% if node.enableAnalytics %}
+
+ {% endif %}
+
diff --git a/core/Site/Node.php b/core/Site/Node.php
deleted file mode 100644
index ec63344..0000000
--- a/core/Site/Node.php
+++ /dev/null
@@ -1,24 +0,0 @@
-id;
- }
-}
diff --git a/migrations/.gitignore b/migrations/.gitignore
deleted file mode 100644
index e69de29..0000000
diff --git a/package.json b/package.json
index 65c9408..25ed802 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"@fortawesome/fontawesome-free": "^5.11.2",
"axios": "^0.21.1",
"bootstrap": "^4.3.1",
+ "chart.js": "^3.7.1",
"choices.js": "^9.0.1",
"flag-icon-css": "^3.5.0",
"jquery": "^3.6.0",
diff --git a/symfony.lock b/symfony.lock
index 7e97f5e..58d5c8b 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -129,6 +129,9 @@
"imagine/imagine": {
"version": "1.2.4"
},
+ "jaybizzle/crawler-detect": {
+ "version": "v1.2.110"
+ },
"khanamiryan/qrcode-detector-decoder": {
"version": "1.0.4"
},
diff --git a/yarn.lock b/yarn.lock
index fae2b28..6faa9fb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1819,6 +1819,11 @@ chalk@^4.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
+chart.js@^3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.7.1.tgz#0516f690c6a8680c6c707e31a4c1807a6f400ada"
+ integrity sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA==
+
choices.js@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-9.0.1.tgz#745fb29af8670428fdc0bf1cc9dfaa404e9d0510"
@@ -2048,9 +2053,9 @@ core-js-compat@^3.8.1, core-js-compat@^3.9.0:
semver "7.0.0"
core-js@^3.0.0:
- version "3.9.1"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae"
- integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==
+ version "3.21.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94"
+ integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==
core-util-is@~1.0.0:
version "1.0.2"