From e771b2e874478b5f012950bef675234d5f01a290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nu=C5=A1a=20Puk=C5=A1i=C4=8D?= Date: Tue, 26 Aug 2025 17:07:30 +0200 Subject: [PATCH] Visit analytics --- src/Controller/Api/VisitController.php | 33 +++++++++++ src/Entity/Visit.php | 49 ++++++++++++++++ src/Repository/VisitRepository.php | 37 ++++++++++++ templates/admin/analytics.html.twig | 81 ++++++++++++++++++++++++++ templates/base.html.twig | 2 +- 5 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 src/Controller/Api/VisitController.php create mode 100644 src/Entity/Visit.php create mode 100644 src/Repository/VisitRepository.php create mode 100644 templates/admin/analytics.html.twig diff --git a/src/Controller/Api/VisitController.php b/src/Controller/Api/VisitController.php new file mode 100644 index 0000000..61f7d80 --- /dev/null +++ b/src/Controller/Api/VisitController.php @@ -0,0 +1,33 @@ +getContent(), true); + + if (!isset($data['route']) || empty($data['route'])) { + return new JsonResponse(['error' => 'Route is required'], Response::HTTP_BAD_REQUEST); + } + + $route = $data['route']; + $visit = new Visit($route); + + $visitRepository->save($visit); + + return new JsonResponse(['success' => true]); + } +} diff --git a/src/Entity/Visit.php b/src/Entity/Visit.php new file mode 100644 index 0000000..e50fb21 --- /dev/null +++ b/src/Entity/Visit.php @@ -0,0 +1,49 @@ +route = $route; + $this->visitedAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getRoute(): string + { + return $this->route; + } + + public function setRoute(string $route): self + { + $this->route = $route; + return $this; + } + + public function getVisitedAt(): \DateTimeImmutable + { + return $this->visitedAt; + } +} diff --git a/src/Repository/VisitRepository.php b/src/Repository/VisitRepository.php new file mode 100644 index 0000000..026a518 --- /dev/null +++ b/src/Repository/VisitRepository.php @@ -0,0 +1,37 @@ + + */ +class VisitRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Visit::class); + } + + public function save(Visit $visit, bool $flush = true): void + { + $this->getEntityManager()->persist($visit); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function getVisitCountByRoute(): array + { + return $this->createQueryBuilder('v') + ->select('v.route, COUNT(v.id) as count') + ->groupBy('v.route') + ->orderBy('count', 'DESC') + ->getQuery() + ->getResult(); + } +} diff --git a/templates/admin/analytics.html.twig b/templates/admin/analytics.html.twig new file mode 100644 index 0000000..862f96f --- /dev/null +++ b/templates/admin/analytics.html.twig @@ -0,0 +1,81 @@ +{% extends 'base.html.twig' %} + +{% block title %}Visitor Analytics{% endblock %} + +{% block body %} +
+

Page Visit Analytics

+ +
+

Visit Count by Route

+ + {% if visitStats|length > 0 %} + + + + + + + + + {% for stat in visitStats %} + + + + + {% endfor %} + +
RouteVisit Count
{{ stat.route }}{{ stat.count }}
+ {% else %} +

No visit data recorded yet.

+ {% endif %} +
+ +
+

This data shows the number of page visits per route. No personal user data is collected.

+
+
+{% endblock %} + +{% block stylesheets %} +{{ parent() }} + +{% endblock %} diff --git a/templates/base.html.twig b/templates/base.html.twig index 161af02..e86f81e 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -25,7 +25,7 @@ {% block importmap %}{{ importmap('app') }}{% endblock %} {% endblock %} - +