```
✨ feat(ansible): Ajoute la mise à jour du journal client et permissions fichier ✨ feat(HomeController): Récupère et affiche le journal de bord client. 📦️ chore: Ajoute gemini-api-php/client et corrige des dépendances. 🐛 fix(docker): Supprime la configuration Xdebug obsolète. ```
This commit is contained in:
@@ -203,7 +203,22 @@
|
||||
args:
|
||||
chdir: "{{ path }}"
|
||||
when: ansible_os_family == "Debian"
|
||||
- name: Exécuter app:git-log-update pour mettre à jour le journal client
|
||||
ansible.builtin.command: php bin/console app:git-log-update
|
||||
become: false
|
||||
args:
|
||||
chdir: "{{ path }}"
|
||||
# On ignore les erreurs pour ne pas bloquer le déploiement si l'IA ou Git échoue
|
||||
ignore_errors: yes
|
||||
|
||||
- name: S'assurer que le fichier update.json a les bonnes permissions
|
||||
ansible.builtin.file:
|
||||
path: "{{ path }}/var/update.json"
|
||||
owner: bot
|
||||
group: www-data
|
||||
mode: '0664'
|
||||
state: file
|
||||
ignore_errors: yes
|
||||
- name: Exécuter liip:imagine:cache:remove dans le répertoire de l application
|
||||
ansible.builtin.command: php bin/console liip:imagine:cache:remove
|
||||
become: false
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"exbil/mailcow-php-api": ">=0.15.0",
|
||||
"fkrzski/robots-txt": "^2.0",
|
||||
"fpdf/fpdf": "^1.86",
|
||||
"gemini-api-php/client": "^1.7",
|
||||
"google/apiclient": "^2.19.0",
|
||||
"google/cloud": "^0.296.0",
|
||||
"healey/robots": "^1.0.1",
|
||||
|
||||
69
composer.lock
generated
69
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d69b2f764686c771554adde51917a972",
|
||||
"content-hash": "04441998f46efc5154a83945a7ca9a46",
|
||||
"packages": [
|
||||
{
|
||||
"name": "async-aws/core",
|
||||
@@ -2536,6 +2536,73 @@
|
||||
},
|
||||
"time": "2025-12-08T14:03:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "gemini-api-php/client",
|
||||
"version": "v1.7.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/gemini-api-php/client.git",
|
||||
"reference": "a48e61285d82b24117a5c8928dd1e504818f908b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/gemini-api-php/client/zipball/a48e61285d82b24117a5c8928dd1e504818f908b",
|
||||
"reference": "a48e61285d82b24117a5c8928dd1e504818f908b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php-http/discovery": "^1.19",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-client-implementation": "*",
|
||||
"psr/http-factory": "^1.0.2",
|
||||
"psr/http-factory-implementation": "*",
|
||||
"psr/http-message": "^1.0.1 || ^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.41",
|
||||
"guzzlehttp/guzzle": "^7.8.0",
|
||||
"guzzlehttp/psr7": "^2.0.0",
|
||||
"phpstan/phpstan": "^1.10.50",
|
||||
"phpunit/phpunit": "^10.5"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-curl": "Required for streaming responses"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GeminiAPI\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Erdem Köse",
|
||||
"email": "erdemkose@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "API client for Google's Gemini API",
|
||||
"keywords": [
|
||||
"Gemini",
|
||||
"ai",
|
||||
"api",
|
||||
"client",
|
||||
"gemini pro",
|
||||
"gemini pro vision",
|
||||
"google",
|
||||
"php",
|
||||
"sdk"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/gemini-api-php/client/issues",
|
||||
"source": "https://github.com/gemini-api-php/client/tree/v1.7.2"
|
||||
},
|
||||
"time": "2025-01-31T15:31:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/apiclient",
|
||||
"version": "v2.19.0",
|
||||
|
||||
@@ -54,25 +54,6 @@ RUN pecl install redis && docker-php-ext-enable redis
|
||||
# Installer Excimer via pecl
|
||||
RUN pecl install excimer && docker-php-ext-enable excimer
|
||||
|
||||
# Configuration et installation de Xdebug
|
||||
# Utilisation de --no-install-recommends pour éviter l'installation de paquets inutiles
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git \
|
||||
&& mkdir -p /usr/src/php/ext/xdebug \
|
||||
&& git clone https://github.com/xdebug/xdebug.git /usr/src/php/ext/xdebug \
|
||||
&& cd /usr/src/php/ext/xdebug \
|
||||
&& phpize \
|
||||
&& ./configure --enable-xdebug \
|
||||
&& make && make install \
|
||||
&& rm -rf /usr/src/php/ext/xdebug \
|
||||
&& apt-get autoremove -y build-essential git \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
COPY ./docker/php/custom.ini /usr/local/etc/php/conf.d/custom.ini
|
||||
|
||||
RUN echo "zend_extension=xdebug" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||
RUN echo "xdebug.mode=develop,debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
|
||||
&& echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
|
||||
&& echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||
RUN mkdir -p /opt/phpstorm-coverage && \
|
||||
chmod -R 777 /opt/phpstorm-coverage
|
||||
|
||||
|
||||
124
src/Command/GitSyncLogCommand.php
Normal file
124
src/Command/GitSyncLogCommand.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Process\Process;
|
||||
// Utilisation des namespaces que tu as fournis
|
||||
use GeminiAPI\Client;
|
||||
use GeminiAPI\Resources\Parts\TextPart;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:git-log-update',
|
||||
description: 'Archive le dernier commit avec reformulation IA pour le client.',
|
||||
)]
|
||||
class GitSyncLogCommand extends Command
|
||||
{
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$filePath = 'var/update.json';
|
||||
$projectDir = '/srv/app';
|
||||
|
||||
// 1. Récupération des infos Git
|
||||
$gitCmd = sprintf(
|
||||
'git config --global --add safe.directory %s && git log -1 --format="%%s|%%ci|%%h"',
|
||||
$projectDir
|
||||
);
|
||||
|
||||
$process = Process::fromShellCommandline($gitCmd);
|
||||
$process->run();
|
||||
|
||||
if (!$process->isSuccessful()) {
|
||||
$io->error("Erreur Git : " . $process->getErrorOutput());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$outputGit = explode('|', trim($process->getOutput()));
|
||||
$rawMessage = $outputGit[0] ?? '';
|
||||
$commitDate = $outputGit[1] ?? date('Y-m-d H:i:s');
|
||||
$commitHash = $outputGit[2] ?? 'unknown';
|
||||
|
||||
// 2. Détermination du TYPE (feature, fix, optimise, new)
|
||||
$type = 'new';
|
||||
$lowerMsg = strtolower($rawMessage);
|
||||
|
||||
if (preg_match('/(fix|bug|patch|correct)/', $lowerMsg)) {
|
||||
$type = 'fix';
|
||||
} elseif (preg_match('/(feat|add|create|nouveau|new)/', $lowerMsg)) {
|
||||
$type = 'feature';
|
||||
} elseif (preg_match('/(perf|opti|refactor|clean|speed)/', $lowerMsg)) {
|
||||
$type = 'optimise';
|
||||
}
|
||||
|
||||
// 3. Vérification anti-doublon (Basée sur le Hash)
|
||||
$data = [];
|
||||
if (file_exists($filePath)) {
|
||||
$data = json_decode(file_get_contents($filePath), true) ?? [];
|
||||
}
|
||||
|
||||
if (!empty($data) && $data[0]['hash'] === $commitHash) {
|
||||
$io->info("Le commit [$commitHash] est déjà dans le journal client.");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// 4. Appel IA Gemini-3-Pro-Preview pour la reformulation
|
||||
$friendlyMessage = $rawMessage;
|
||||
try {
|
||||
// Ta clé API
|
||||
$client = new Client("AIzaSyDTPJERlUC47bcvhZU51Lwpqb1uxXS8SIg");
|
||||
$model = 'gemini-3-pro-preview';
|
||||
|
||||
$prompt = "Tu es un expert en communication web pour Ludik Event. Ta mission est de transformer
|
||||
un message de commit technique en une note de mise à jour élégante pour ton client.
|
||||
|
||||
MESSAGE TECHNIQUE : \"$rawMessage\"
|
||||
|
||||
DIRECTIVES :
|
||||
1. Reformule pour un propriétaire de site non-technique.
|
||||
2. Sois court, positif et rassurant.
|
||||
3. Ne commence JAMAIS par 'Voici la phrase' ou 'Mise à jour'.
|
||||
4. Donne uniquement le texte final prêt à être affiché.
|
||||
|
||||
RÉSULTAT ATTENDU :";
|
||||
|
||||
$response = $client->withV1BetaVersion()->generativeModel($model)->generateContent(
|
||||
new TextPart($prompt)
|
||||
);
|
||||
|
||||
// Adaptation selon la structure de retour du SDK
|
||||
$aiText = $response->text();
|
||||
if ($aiText) {
|
||||
$friendlyMessage = trim($aiText);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$io->warning("L'IA n'a pas pu traiter le message. Utilisation du texte brut.");
|
||||
}
|
||||
|
||||
// 5. Création de l'entrée JSON
|
||||
$newEntry = [
|
||||
'type' => $type,
|
||||
'message' => $friendlyMessage,
|
||||
'date' => $commitDate,
|
||||
'hash' => $commitHash
|
||||
];
|
||||
|
||||
// 6. Sauvegarde et rotation (5 max)
|
||||
array_unshift($data, $newEntry);
|
||||
$data = array_slice($data, 0, 5);
|
||||
|
||||
if (!is_dir('var')) {
|
||||
mkdir('var', 0777, true);
|
||||
}
|
||||
|
||||
file_put_contents($filePath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$io->success("Journal client mis à jour avec succès (Type: $type).");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -29,53 +29,65 @@ class HomeController extends AbstractController
|
||||
// 1. Récupération sécurisée du Token (Cache 2h)
|
||||
$token = $cache->get('umami_token', function (ItemInterface $item) use ($httpClient, $baseUrl) {
|
||||
$item->expiresAfter(7200);
|
||||
|
||||
$response = $httpClient->request('POST', "$baseUrl/auth/login", [
|
||||
'json' => [
|
||||
'username' => $_ENV['UMAMI_USER'],
|
||||
'password' => $_ENV['UMAMI_PASSWORD'],
|
||||
]
|
||||
]);
|
||||
|
||||
return $response->toArray()['token'] ?? null;
|
||||
});
|
||||
|
||||
// 2. Récupération des Stats (Cache 15 min pour la fluidité du CRM)
|
||||
// 2. Récupération des Stats Umami
|
||||
$stats = $cache->get('umami_stats_24h', function (ItemInterface $item) use ($httpClient, $baseUrl, $token, $websiteId) {
|
||||
$item->expiresAfter(900); // 15 minutes
|
||||
|
||||
$item->expiresAfter(900);
|
||||
if (!$token) return ['visitors' => 0, 'views' => 0];
|
||||
|
||||
try {
|
||||
$startAt = (time() - (24 * 3600)) * 1000;
|
||||
$endAt = time() * 1000;
|
||||
|
||||
|
||||
$response = $httpClient->request('GET', "$baseUrl/websites/$websiteId/stats", [
|
||||
'headers' => ['Authorization' => "Bearer $token"],
|
||||
'query' => [
|
||||
'startAt' => $startAt,
|
||||
'endAt' => $endAt,
|
||||
],
|
||||
'query' => ['startAt' => $startAt, 'endAt' => $endAt],
|
||||
]);
|
||||
|
||||
$data = $response->toArray();
|
||||
return [
|
||||
'visitors' => $data['visitors'] ?? 0,
|
||||
'views' => $data['pageviews'] ?? 0
|
||||
'visitors' => $data['visitors']['value'] ?? $data['visitors'] ?? 0,
|
||||
'views' => $data['pageviews']['value'] ?? $data['pageviews'] ?? 0
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return ['visitors' => 0, 'views' => 0];
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Récupération des Updates (Journal de bord client)
|
||||
$updateFile = $this->getParameter('kernel.project_dir') . '/var/update.json';
|
||||
$updates = [];
|
||||
|
||||
if (file_exists($updateFile)) {
|
||||
$rawUpdates = json_decode(file_get_contents($updateFile), true) ?? [];
|
||||
|
||||
// On enrichit les données avec les couleurs Tailwind
|
||||
$updates = array_map(function ($update) {
|
||||
$update['tag_color'] = match ($update['type'] ?? 'new') {
|
||||
'feature' => 'bg-emerald-100 text-emerald-700 border-emerald-200',
|
||||
'fix' => 'bg-rose-100 text-rose-700 border-rose-200',
|
||||
'optimise' => 'bg-amber-100 text-amber-700 border-amber-200',
|
||||
'new' => 'bg-slate-100 text-slate-700 border-slate-200',
|
||||
default => 'bg-gray-100 text-gray-700 border-gray-200',
|
||||
};
|
||||
return $update;
|
||||
}, $rawUpdates);
|
||||
}
|
||||
|
||||
return $this->render('dashboard/home.twig', [
|
||||
'product' => $productRepository->count(),
|
||||
'devis_wait_sign' => $devisRepository->waitSign(),
|
||||
'customers' => $customerRepository->count(),
|
||||
'nbVisitor' => $stats['visitors'],
|
||||
'nbView' => $stats['views'],
|
||||
'statview' => "https://tools-security.esy-web.dev/share/o9j2XMjV4Trnnbfb"
|
||||
'statview' => "https://tools-security.esy-web.dev/share/o9j2XMjV4Trnnbfb",
|
||||
'updates' => $updates // Passage des mises à jour au template
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
|
||||
{# SIDEBAR #}
|
||||
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-800 lg:translate-x-0 transition-all duration-300 ease-in-out">
|
||||
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-800 lg:translate-x-0 transition-all duration-300 ease-in-out">
|
||||
|
||||
<div class="flex items-center px-8 h-20 border-b border-slate-100 dark:border-slate-800">
|
||||
@@ -216,7 +215,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</main> {# Fin du main existant #}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,15 +8,14 @@
|
||||
{# --- HEADER STATS (GLASS PILLS) --- #}
|
||||
<div class="flex flex-wrap gap-4 items-center mb-8">
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-4 w-full md:w-auto">
|
||||
{# Titre / Lien vers le site #}
|
||||
<div class="flex items-center gap-2 px-2">
|
||||
<h1 class="text-xl font-black italic text-slate-900 dark:text-white uppercase tracking-tighter">
|
||||
Analytics
|
||||
</h1>
|
||||
<a href="{{ statview }}" target="_blank" class="group flex items-center gap-2 bg-slate-900/5 dark:bg-white/5 px-4 py-2 rounded-xl border border-slate-200/50 dark:border-white/10 hover:bg-blue-600 transition-all duration-300">
|
||||
<span class="text-[10px] font-bold text-slate-500 group-hover:text-white transition-colors uppercase tracking-widest">
|
||||
reservation.ludikevent.fr
|
||||
</span>
|
||||
<span class="text-[10px] font-bold text-slate-500 group-hover:text-white transition-colors uppercase tracking-widest">
|
||||
reservation.ludikevent.fr
|
||||
</span>
|
||||
<svg class="w-3 h-3 text-slate-400 group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
@@ -24,22 +23,20 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
{# Badge Visiteurs #}
|
||||
<div class="backdrop-blur-md bg-white/40 dark:bg-slate-900/20 border border-white/40 dark:border-white/10 px-6 py-2.5 rounded-full shadow-lg flex items-center gap-3">
|
||||
<div class="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
|
||||
<span class="text-[11px] font-black uppercase tracking-widest text-slate-600 dark:text-slate-300">
|
||||
<span class="text-slate-900 dark:text-white text-sm">{{ nbVisitor }}</span> Visiteurs <span class="hidden md:inline text-slate-400">(-24h)</span>
|
||||
</span>
|
||||
<span class="text-slate-900 dark:text-white text-sm">{{ nbVisitor }}</span> Visiteurs <span class="hidden md:inline text-slate-400">(-24h)</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{# Badge Vues #}
|
||||
<div class="backdrop-blur-md bg-white/40 dark:bg-slate-900/20 border border-white/40 dark:border-white/10 px-6 py-2.5 rounded-full shadow-lg flex items-center gap-3">
|
||||
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
<span class="text-[11px] font-black uppercase tracking-widest text-slate-600 dark:text-slate-300">
|
||||
<span class="text-slate-900 dark:text-white text-sm">{{ nbView }}</span> Vues <span class="hidden md:inline text-slate-400">(-24h)</span>
|
||||
</span>
|
||||
<span class="text-slate-900 dark:text-white text-sm">{{ nbView }}</span> Vues <span class="hidden md:inline text-slate-400">(-24h)</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,15 +79,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Carte Clients #}
|
||||
<div class="group relative">
|
||||
{# Carte Journal de Maintenance #}
|
||||
<div class="group relative md:row-span-1">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-600/20 to-transparent blur-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative h-full overflow-hidden backdrop-blur-2xl bg-white/30 dark:bg-slate-800/40 border border-white/20 dark:border-white/5 rounded-[3rem] p-8 shadow-[0_20px_50px_rgba(0,0,0,0.1)] transition-all duration-500 hover:-translate-y-2">
|
||||
<p class="text-[10px] font-black text-purple-600 dark:text-purple-400 uppercase tracking-[0.4em] mb-6 italic">Maintenance</p>
|
||||
|
||||
<div class="space-y-6">
|
||||
{% for update in updates %}
|
||||
<div class="relative pl-6 border-l-2 border-slate-200 dark:border-slate-700 pb-1">
|
||||
{# Point sur la timeline #}
|
||||
<div class="absolute -left-[9px] top-0 w-4 h-4 rounded-full border-4 border-white dark:border-slate-800 {{ update.tag_color|replace({'text-': 'bg-'})|split(' ')[0] }}"></div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="px-2 py-0.5 rounded-md border text-[8px] font-black uppercase tracking-tighter {{ update.tag_color }}">
|
||||
{{ update.type }}
|
||||
</span>
|
||||
<span class="text-[9px] font-bold text-slate-400 uppercase">{{ update.date|date('d/m/y') }}</span>
|
||||
</div>
|
||||
<p class="text-[11px] font-semibold text-slate-700 dark:text-slate-200 leading-tight">
|
||||
{{ update.message }}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-xs text-slate-400 italic">Aucune mise à jour disponible.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Carte Clients (Déplacée en bas ou sur le côté selon ton envie de grille) #}
|
||||
<div class="group relative md:col-span-2">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-emerald-600/20 to-transparent blur-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative overflow-hidden backdrop-blur-2xl bg-white/30 dark:bg-slate-800/40 border border-white/20 dark:border-white/5 rounded-[3rem] p-8 shadow-[0_20px_50px_rgba(0,0,0,0.1)] transition-all duration-500 hover:-translate-y-2">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 uppercase tracking-[0.4em] mb-4 italic">BDD Clients</p>
|
||||
<p class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 uppercase tracking-[0.4em] mb-4 italic">Client(s)</p>
|
||||
<h2 class="text-7xl font-black text-slate-900 dark:text-white tracking-tighter italic leading-none">{{ customers }}</h2>
|
||||
<p class="text-[11px] font-bold text-slate-500 dark:text-slate-400 mt-4 uppercase tracking-wider">Fiches clients uniques</p>
|
||||
<p class="text-[11px] font-bold text-slate-500 dark:text-slate-400 mt-4 uppercase tracking-wider">Fiches clients uniques dans la base</p>
|
||||
</div>
|
||||
<div class="w-14 h-14 bg-gradient-to-br from-emerald-500 to-teal-600 rounded-2xl flex items-center justify-center text-white shadow-[0_10px_20px_rgba(16,185,129,0.3)]">
|
||||
<svg class="w-7 h-7" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /></svg>
|
||||
|
||||
Reference in New Issue
Block a user