Browse Source

Visit analytics

imwald
Nuša Pukšič 5 months ago
parent
commit
e771b2e874
  1. 33
      src/Controller/Api/VisitController.php
  2. 49
      src/Entity/Visit.php
  3. 37
      src/Repository/VisitRepository.php
  4. 81
      templates/admin/analytics.html.twig
  5. 2
      templates/base.html.twig

33
src/Controller/Api/VisitController.php

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api;
use App\Entity\Visit;
use App\Repository\VisitRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class VisitController extends AbstractController
{
#[Route('/api/visit', name: 'api_record_visit', methods: ['POST'])]
public function recordVisit(Request $request, VisitRepository $visitRepository): JsonResponse
{
$data = json_decode($request->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]);
}
}

49
src/Entity/Visit.php

@ -0,0 +1,49 @@
<?php
namespace App\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use App\Repository\VisitRepository;
#[ORM\Entity(repositoryClass: VisitRepository::class)]
class Visit
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $route;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private \DateTimeImmutable $visitedAt;
public function __construct(string $route)
{
$this->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;
}
}

37
src/Repository/VisitRepository.php

@ -0,0 +1,37 @@
<?php
namespace App\Repository;
use App\Entity\Visit;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Visit>
*/
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();
}
}

81
templates/admin/analytics.html.twig

@ -0,0 +1,81 @@
{% extends 'base.html.twig' %}
{% block title %}Visitor Analytics{% endblock %}
{% block body %}
<div class="analytics-container">
<h1>Page Visit Analytics</h1>
<div class="analytics-card">
<h2>Visit Count by Route</h2>
{% if visitStats|length > 0 %}
<table class="analytics-table">
<thead>
<tr>
<th>Route</th>
<th>Visit Count</th>
</tr>
</thead>
<tbody>
{% for stat in visitStats %}
<tr>
<td>{{ stat.route }}</td>
<td class="text-right">{{ stat.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No visit data recorded yet.</p>
{% endif %}
</div>
<div class="analytics-info">
<p>This data shows the number of page visits per route. No personal user data is collected.</p>
</div>
</div>
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.analytics-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.analytics-card {
background: gray;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.analytics-table {
width: 100%;
border-collapse: collapse;
}
.analytics-table th, .analytics-table td {
padding: 10px;
border-bottom: 1px solid var(--color-border);
}
.analytics-table th {
text-align: left;
font-weight: 600;
}
.text-right {
text-align: right;
}
.analytics-info {
font-size: 0.9rem;
color: var(--color-border);
}
</style>
{% endblock %}

2
templates/base.html.twig

@ -25,7 +25,7 @@
{% block importmap %}{{ importmap('app') }}{% endblock %} {% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %} {% endblock %}
</head> </head>
<body data-controller="service-worker"> <body data-controller="service-worker visit-analytics" data-visit-analytics-path-value="{{ app.request.pathInfo }}">
<twig:Header /> <twig:Header />

Loading…
Cancel
Save