Merge remote-tracking branch 'origin/mathis' into Jade-Front

Jade-Front
Jade VAN BRABANDT 2 weeks ago
commit f0bc442188

@ -9,10 +9,12 @@
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^3.2",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.13",
"doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.3",
"nelmio/cors-bundle": "^2.5",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^1.14",
"symfony/asset": "6.1.*",

@ -0,0 +1,18 @@
api_platform:
title: Hello API Platform
version: 1.0.0
formats:
jsonld: ['application/ld+json']
docs_formats:
jsonld: ['application/ld+json']
jsonopenapi: ['application/vnd.openapi+json']
html: ['text/html']
defaults:
stateless: true
cache_headers:
vary: ['Content-Type', 'Authorization', 'Origin']
extra_properties:
standard_put: true
rfc_7807_compliant_errors: true
event_listeners_backward_compatibility_layer: false
keep_legacy_inflector: false

@ -0,0 +1,10 @@
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
expose_headers: ['Link']
max_age: 3600
paths:
'^/': null

@ -4,14 +4,30 @@ security:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
provider: app_user_provider
custom_authenticator: App\Security\LoginFormAuthenticator
logout:
path: app_logout
target: app_login
entry_point: App\Security\LoginFormAuthenticator
remember_me:
secret: '%kernel.secret%'
lifetime: 604800
path: /
always_remember_me: true
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
@ -22,8 +38,13 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/register, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER }
when@test:
security:

@ -0,0 +1,4 @@
api_platform:
resource: .
type: api_platform
prefix: /api

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250529203532 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE TABLE emoji (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, nom VARCHAR(255) NOT NULL, code VARCHAR(255) NOT NULL, force DOUBLE PRECISION NOT NULL, robustesse DOUBLE PRECISION NOT NULL, intelligence DOUBLE PRECISION NOT NULL, vitesse DOUBLE PRECISION NOT NULL, nb_combat_gagne INTEGER NOT NULL, rarete INTEGER NOT NULL)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE rarity (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(50) NOT NULL, drop_rate DOUBLE PRECISION NOT NULL)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_B7C0BE465E237E06 ON rarity (name)
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
DROP TABLE emoji
SQL);
$this->addSql(<<<'SQL'
DROP TABLE rarity
SQL);
}
}

@ -11,12 +11,45 @@ h1 {
color: #f8b435;
}
.emoji-container {
.section-divider {
border: none;
border-top: 3px solid #f8b435;
margin: 50px auto 20px;
width: 80%;
}
.section-break-icon {
text-align: center;
font-size: 2rem;
margin: -20px 0;
color: #f8b435;
}
.section-title {
margin: 40px 10px;
font-size: 1.8rem;
color: #f8f5e0;
}
.emoji-container:not(.base) {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 30px;
width: fit-content;
margin: 0 auto;
}
.emoji-container.base {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 30px;
margin-bottom: 10rem;
border: 3px dashed #f8b435;
padding: 20px;
margin: 0 auto 30px auto;
border-radius: 15px;
width: fit-content;
}
.emoji-card {
@ -43,7 +76,7 @@ h1 {
/* Animation brillance pour rareté légendaire */
@keyframes shine {
0% { background-position: 0px; }
100% { background-position: 177px; }
100% { background-position: 168px; }
}
.emoji-card.gold {
@ -54,17 +87,17 @@ h1 {
.emoji-card.gold::before {
content: '';
position: absolute;
top: 0;
top: 1%;
left: 1%;
width: 98%;
height: 100%;
width: 97%;
height: 99%;
background: linear-gradient(
120deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.5) 50%,
rgba(255, 255, 255, 0.8) 50%,
rgba(255, 255, 255, 0) 100%
);
animation: shine 5s infinite;
animation: shine 3.5s infinite;
pointer-events: none;
z-index: -1;
}
@ -219,7 +252,7 @@ color: #555;
#selection-status {
font-size: 1.1rem;
margin-bottom: 20px;
margin-top: 4rem;
font-weight: bold;
color: #f9e8c0;
}
@ -245,4 +278,37 @@ color: #555;
.vs-text {
font-size: 1.5rem;
font-weight: bold;
}
/* Pagination */
.pagination-bar {
margin: 4rem 4rem;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
color: #f8f5e0;
font-family: 'Georgia', serif;
}
.pagination-bar button {
background: none;
border: none;
font-size: 1.1rem;
color: #f8f5e0;
cursor: pointer;
padding: 5px 10px;
border-radius: 50%;
transition: background 0.2s ease;
}
.pagination-bar button.active {
background: #333;
color: white;
font-weight: bold;
}
.pagination-bar button:hover:not(.active) {
color: #f8b435;
}

@ -0,0 +1,118 @@
body {
background-color: #314e57;
font-family: 'Georgia', serif;
text-align: center;
}
.login-container {
background-color: #3b6068;
border: 4px solid #000;
border-radius: 15px;
padding: 40px 30px;
margin: 60px auto;
width: 90%;
max-width: 400px;
box-shadow: 0 0 30px rgba(0,0,0,0.6);
color: #f8f5e0;
font-family: 'Georgia', serif;
}
.login-title {
font-size: 2.5rem;
color: #f8b435;
margin-bottom: 20px;
text-shadow: 2px 2px 4px #000;
}
.login-error {
background-color: #8b0000;
color: #fff8e1;
padding: 10px;
border: 2px solid #000;
border-radius: 8px;
margin-bottom: 20px;
font-weight: bold;
}
.already-logged {
background-color: #f2e6c9;
color: #000;
padding: 12px;
border: 2px dashed #000;
border-radius: 8px;
margin-bottom: 20px;
font-size: 0.95rem;
}
.logout-link {
display: inline-block;
margin-top: 10px;
color: #8b0000;
font-weight: bold;
text-decoration: underline;
}
.login-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.login-form label {
text-align: left;
color: #f8f5e0;
font-weight: bold;
}
.login-form input[type="text"],
.login-form input[type="password"] {
padding: 10px;
border: 2px solid #000;
border-radius: 6px;
background-color: #f2e6c9;
font-family: 'Georgia', serif;
font-size: 1rem;
box-shadow: inset 2px 2px 5px rgba(0,0,0,0.2);
}
.btn-login {
background-color: #f8b435;
color: #000;
font-weight: bold;
border: 3px solid #000;
padding: 10px;
border-radius: 8px;
font-size: 1.2rem;
cursor: pointer;
box-shadow: 3px 3px 0 #000;
transition: transform 0.2s ease;
}
.btn-login:hover {
transform: scale(1.05);
background-color: #e09f30;
}
.no-account {
margin-top: 30px;
color: #f8f5e0;
}
.btn-register {
display: inline-block;
margin-top: 10px;
background-color: #f2e6c9;
color: #000;
border: 2px solid #000;
padding: 8px 20px;
border-radius: 6px;
text-decoration: none;
font-weight: bold;
box-shadow: 2px 2px 0 #000;
transition: transform 0.2s ease;
}
.btn-register:hover {
transform: scale(1.05);
background-color: #e5d6b8;
}

@ -0,0 +1,88 @@
body {
background-color: #314e57 !important;
font-family: 'Georgia', serif;
text-align: center;
}
.register-container {
background-color: #3b6068;
border: 4px solid #000;
border-radius: 15px;
padding: 40px 30px;
margin: 60px auto;
width: 90%;
max-width: 400px;
box-shadow: 0 0 30px rgba(0,0,0,0.6);
color: #f8f5e0;
font-family: 'Georgia', serif;
}
.register-title {
font-size: 2.5rem;
color: #f8b435;
margin-bottom: 20px;
text-shadow: 2px 2px 4px #000;
}
.register-form label {
text-align: left;
color: #f8f5e0;
font-weight: bold;
}
.register-form input[type="text"],
.register-form input[type="password"] {
padding: 10px;
border: 2px solid #000;
border-radius: 6px;
background-color: #f2e6c9;
font-family: 'Georgia', serif;
font-size: 1rem;
box-shadow: inset 2px 2px 5px rgba(0,0,0,0.2);
width: 100%;
margin-bottom: 15px;
}
.btn-register-submit {
background-color: #f8b435;
color: #000;
font-weight: bold;
border: 3px solid #000;
padding: 10px;
border-radius: 8px;
font-size: 1.2rem;
cursor: pointer;
box-shadow: 3px 3px 0 #000;
transition: transform 0.2s ease;
width: 100%;
margin-bottom: 20px;
}
.btn-register-submit:hover {
transform: scale(1.05);
background-color: #e09f30;
}
.already-account {
color: #f8f5e0;
font-size: 0.95rem;
}
.btn-login-link {
display: inline-block;
margin-top: 10px;
background-color: #f2e6c9;
color: #000;
border: 2px solid #000;
padding: 8px 20px;
border-radius: 6px;
text-decoration: none;
font-weight: bold;
box-shadow: 2px 2px 0 #000;
transition: transform 0.2s ease;
}
.btn-login-link:hover {
transform: scale(1.05);
background-color: #e5d6b8;
}

@ -42,22 +42,63 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
function handleAction(type) {
async function handleAction(type) {
if (selectedCards.length !== 2) {
alert("Tu dois sélectionner 2 créatures.");
return;
}
const name1 = selectedCards[0].dataset.name;
const name2 = selectedCards[1].dataset.name;
const id1 = selectedCards[0].dataset.id;
const id2 = selectedCards[1].dataset.id;
if (type === 'reproduction') {
try {
const response = await fetch(`/emojis/fusion/${encodeURIComponent(id1)}/${encodeURIComponent(id2)}`, {
method: 'POST',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error("Erreur serveur : " + response.status);
}
const data = await response.json();
alert(`Succès : ${data.message} (ID : ${data.childId})`);
// Tu peux aussi mettre à jour le DOM ici avec le nouvel enfant
// ex: ajouter une carte, etc.
} catch (error) {
console.error("Erreur lors de la reproduction :", error);
alert("Une erreur est survenue lors de la reproduction.");
}
return; // on quitte ici
}
if (type === 'combat') {
console.log(`Combat : ${name1} contre ${name2}`);
} else if (type === 'reproduction') {
console.log(`Accouplement : ${name1} et ${name2}`);
fetch(`/emojis/fight/${id1}/${id2}`, { method: 'GET',
headers: {
'Accept': 'application/json'
}}).then(response => {
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Données reçues :', data);
})
.catch(error => {
console.error('Erreur lors de la requête :', error);
});
}
// Réinitialiser après l'action
// Réinitialiser si pas redirection
selectedCards.forEach(card => card.classList.remove('selected'));
selectedCards = [];
updateSelectionDisplay();
@ -66,7 +107,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Ouvre / Ferme la popup d'information
function togglePopup(id) {
const popup = document.getElementById('popup-' + id);
popup.style.display = (popup.style.display === 'block') ? 'none' : 'block';
if (popup) {
popup.style.display = (popup.style.display === 'block') ? 'none' : 'block';
}
}
// Fermer les autres popups en cliquant ailleurs
@ -84,7 +127,7 @@ document.addEventListener('DOMContentLoaded', () => {
const sortBy = document.getElementById('sort-select').value;
const searchTerm = document.getElementById('search-input').value.toLowerCase();
const cards = Array.from(document.querySelectorAll('.emoji-card'));
const cards = Array.from(document.querySelectorAll('#collection-container .emoji-card'));
cards.forEach(card => {
const color = card.dataset.color;
@ -101,7 +144,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
if (sortBy !== 'none') {
const container = document.querySelector('.emoji-container');
const container = document.getElementById('collection-container'); // ✅ corrigé
const visibleCards = cards.filter(c => c.style.display !== 'none');
visibleCards.sort((a, b) => {
@ -111,21 +154,29 @@ document.addEventListener('DOMContentLoaded', () => {
});
visibleCards.forEach(card => container.appendChild(card));
paginateCards();
}
}
// Appel Fonctionnalité de popup d'information
document.querySelectorAll('.detail-icon').forEach(icon => {
icon.addEventListener('click', (e) => {
const id = icon.parentElement.dataset.id;
togglePopup(id);
e.stopPropagation(); // empêche le clic daller à la carte
const card = icon.closest('.emoji-card');
const id = card?.dataset.id;
if (id) {
togglePopup(id);
}
e.stopPropagation(); // évite que ça sélectionne la carte
});
});
// Appel Fonctionnalité de sélection des cartes
document.querySelectorAll('.emoji-card').forEach(card => {
card.addEventListener('click', () => toggleSelection(card));
// Appliquer l'écouteur à toutes les cartes (base + collection)
document.querySelectorAll('.emoji-container').forEach(container => {
container.addEventListener('click', (e) => {
const card = e.target.closest('.emoji-card');
if (!card || e.target.closest('.detail-icon')) return; // Ignore si clic sur icône
toggleSelection(card);
});
});
// Appel Fonctionnalité de combat et reproduction
@ -140,4 +191,66 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('search-input').addEventListener('input', applyFilters);
document.getElementById('rarete-filter').addEventListener('change', applyFilters);
document.getElementById('sort-select').addEventListener('change', applyFilters);
/***** Partie Pagination ******/
const ITEMS_PER_PAGE = 10;
let currentPage = 1;
function paginateCards() {
const allCards = Array.from(document.querySelectorAll('#collection-container .emoji-card'));
const container = document.getElementById('collection-container');
const totalPages = Math.ceil(allCards.length / ITEMS_PER_PAGE);
// Masquer toutes les cartes
allCards.forEach(card => card.style.display = 'none');
// Afficher uniquement les cartes de la page courante
const start = (currentPage - 1) * ITEMS_PER_PAGE;
const end = start + ITEMS_PER_PAGE;
allCards.slice(start, end).forEach(card => card.style.display = 'block');
renderPagination(totalPages);
}
function renderPagination(totalPages) {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
const addBtn = (text, page = null, isActive = false, disabled = false) => {
const btn = document.createElement('button');
btn.textContent = text;
if (isActive) btn.classList.add('active');
if (disabled) btn.disabled = true;
if (page !== null) {
btn.addEventListener('click', () => {
currentPage = page;
paginateCards();
});
}
pagination.appendChild(btn);
};
addBtn('Précédent', currentPage - 1, false, currentPage === 1);
for (let i = 1; i <= totalPages; i++) {
if (
i === 1 ||
i === totalPages ||
(i >= currentPage - 1 && i <= currentPage + 1)
) {
addBtn(i, i, i === currentPage);
} else if (
i === 2 && currentPage > 3 ||
i === totalPages - 1 && currentPage < totalPages - 2
) {
addBtn('...');
}
}
addBtn('Suivant', currentPage + 1, false, currentPage === totalPages);
}
paginateCards();
});

@ -0,0 +1,78 @@
<?php
namespace App\Command;
use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class PopulateDBEmojiAvailableCommand extends Command
{
protected static $defaultName = 'app:popDBEmojiAvai';
private Connection $connection;
public function __construct(Connection $connection)
{
parent::__construct();
$this->connection = $connection;
}
protected function configure()
{
$this
->setDescription('Populate the database.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
// On supprime la table si elle existe déjà
$this->connection->executeStatement('DROP TABLE IF EXISTS stock_emoji');
// On crée la table
$this->connection->executeStatement('
CREATE TABLE stock_emoji (
id INT PRIMARY KEY,
code VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL
)
');
// On peuple la table
$this->connection->executeStatement("INSERT INTO stock_emoji (id, code, name) VALUES
(1,'🤖','Rodolph'),
(2,'😺','Bobette'),
(3,'🧠','Diana'),
(4,'👻','Ian'),
(5,'🧟','Alice'),
(6,'🐶','Eric'),
(7,'👽','Hannah'),
(8,'🧛','Fiona'),
(9,'🎃','George'),
(10,'🐸','John'),
(11,'⚡','Charlie'),
(12,'💀','Benoit'),
(13,'🔥','Sophie'),
(14,'🧙','Bob'),
(15,'🌪️','Ethan'),
(16,'😎','Luna'),
(17,'😁','Maxence'),
(18,'🌟','Jasper'),
(19,'😈','Nora')
");
$output->writeln('Base de données peuplée.');
} catch (\Exception $e) {
$output->writeln('<error>Erreur : ' . $e->getMessage() . '</error>');
return Command::FAILURE;
}
return Command::SUCCESS;
}
}

@ -13,7 +13,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use App\Repository\EmojiRepository;
#[Route('/emoji', name: 'emoji')]
#[Route('/emojis', name: 'app_emoji_')]
class EmojiController extends AbstractController
{
private RarityRepository $rarityRepository;
@ -25,15 +25,34 @@ class EmojiController extends AbstractController
$this->httpClient = $httpClient;
}
#[Route('/emoji', name: 'app_emoji')]
#[Route('/', name: 'emojis')]
public function index(): Response
{
$this->testToMove();
return $this->render('emoji/index.html.twig', [
'controller_name' => 'EmojiController',
]);
}
#[Route('/add/{code}', name: 'add')]
#[Route('/count', name: 'count')]
public function count(EmojiRepository $emojiRepository): Response
{
$count = count($emojiRepository->findAll());
return new Response(['count' => $count]);
}
#[Route('/addRarity', name: 'add_rarity')]
public function addRarityDebug(EntityManagerInterface $entityManager) {
$rarity = new Rarity();
$rarity->setName('Bip');
$rarity->setDropRate(42);
$entityManager->persist($rarity);
$entityManager->flush();
return new Response();
}
#[Route('/add/{code}', name: 'add_code')]
public function addEmojiDebug(string $code, EntityManagerInterface $entityManager) {
$emoji = new Emoji();
$emoji->setCode($code);
@ -168,9 +187,71 @@ class EmojiController extends AbstractController
return new JsonResponse([
'message' => 'Child created',
'mommy' => $child->getParent1()->getCode(),
'daddy' => $child->getParent2()->getCode(),
'baby' => $child->getCode()
'childId' => $child->getId()
]);
}
}
#[Route('/fight/{idEmoji1}/{idEmoji2}', name: 'fight_emoji')]
public function fightEmoji(int $idEmoji1, int $idEmoji2, EntityManagerInterface $entityManager, EmojiRepository $emojiRepository): JsonResponse {
$emoji1 = $emojiRepository->find($idEmoji1);
$emoji2 = $emojiRepository->find($idEmoji2);
if (!$emoji1 || !$emoji2) {
return new JsonResponse(['error' => 'Emoji not found'], 404);
}
$aleatoire = random_int(0,3);
$valEmoji1 = [$emoji1->getStrength(),$emoji1->getToughness(),$emoji1->getIntelligence(),$emoji1->getSpeed()];
$valEmoji2 = [$emoji2->getStrength(),$emoji2->getToughness(),$emoji2->getIntelligence(),$emoji2->getSpeed()];
$difference = $valEmoji1[$aleatoire] - $valEmoji2[$aleatoire];
// if($difference > 0){
// $emoji1->wonFight();
// $entityManager->persist($emoji1);
// $entityManager->remove($emoji2);
// } else {
// $emoji2->wonFight();
// $entityManager->persist($emoji2);
// $entityManager->remove($emoji1);
// }
// $entityManager->flush();
if ($difference > 0) {
$winner = $emoji1;
$loser = $emoji2;
$wonFight = 'left';
} else {
$winner = $emoji2;
$loser = $emoji1;
$wonFight = 'right';
}
$winner->wonFight();
$entityManager->persist($winner);
$entityManager->remove($loser);
$entityManager->flush();
return new JsonResponse([
'emoji1' => $emoji1->getCode(),
'emoji2' => $emoji2->getCode(),
'winner' => $wonFight
]);
}
public function testToMove(){
$e = new Emoji();
$e->setName("ROBERT");
$e->setStrength(5);
$e->setIntelligence(2);
$e->setToughness(3);
$e->setSpeed(4);
$e2 = new Emoji();
$e2->setName("BIBOP");
$e2->setStrength(42);
$e2->setIntelligence(1);
$e2->setToughness(1);
$e2->setSpeed(1);
$vic = $this->fightEmoji($e,$e2);
echo $vic->getName();
}
}

@ -8,10 +8,10 @@ use Symfony\Component\Routing\Annotation\Route;
class HomeController extends AbstractController
{
#[Route('/', name: 'home')]
#[Route('/', name: 'app_home')]
public function index(): Response
{
$emojis = [
$emojisDeBase = [
[
'id' => 1,
'nom' => 'Bob',
@ -21,7 +21,7 @@ class HomeController extends AbstractController
'intelligence' => 7.8,
'vitesse' => 10.0,
'nbCombatGagne' => 3,
'rarete' => 2, // épique
'rarete' => 3, // épique
],
[
'id' => 2,
@ -43,24 +43,169 @@ class HomeController extends AbstractController
'intelligence' => 18.0,
'vitesse' => 17.0,
'nbCombatGagne' => 10,
'rarete' => 4, // légendaire
'rarete' => 5, // légendaire
],
[
'id' => 4,
'nom' => 'Sophie',
'code' => '😎',
'force' => 8.0,
'robustesse' => 7.5,
'intelligence' => 9.0,
'vitesse' => 8.5,
'nbCombatGagne' => 2,
'rarete' => 4, // mythique
]
];
// Ajout de la couleur selon la rareté
foreach ($emojis as &$emoji) {
$emojisCrees = [
[
'id' => 6,
'nom' => 'Benoit',
'code' => '🤖',
'force' => 15.0,
'robustesse' => 12.0,
'intelligence' => 14.0,
'vitesse' => 13.0,
'nbCombatGagne' => 5,
'rarete' => 3, // épique
],
[
'id' => 7,
'nom' => 'Eric',
'code' => '🌟',
'force' => 18.0,
'robustesse' => 16.0,
'intelligence' => 17.0,
'vitesse' => 19.0,
'nbCombatGagne' => 8,
'rarete' => 5, // légendaire
],
[
'id' => 5,
'nom' => 'Alice',
'code' => '🥳',
'force' => 6.0,
'robustesse' => 5.0,
'intelligence' => 4.5,
'vitesse' => 6.5,
'nbCombatGagne' => 0,
'rarete' => 2, // rare
],
[
'id' => 8,
'nom' => 'Bobette',
'code' => '🤩',
'force' => 10.0,
'robustesse' => 9.0,
'intelligence' => 11.0,
'vitesse' => 12.0,
'nbCombatGagne' => 4,
'rarete' => 4, // mythique
],
[
'id' => 9,
'nom' => 'Charlie',
'code' => '😇',
'force' => 7.0,
'robustesse' => 6.0,
'intelligence' => 8.0,
'vitesse' => 7.5,
'nbCombatGagne' => 2,
'rarete' => 1, // commun
],
[
'id' => 10,
'nom' => 'Diana',
'code' => '😈',
'force' => 14.0,
'robustesse' => 13.0,
'intelligence' => 15.0,
'vitesse' => 16.0,
'nbCombatGagne' => 6,
'rarete' => 4, // mythique
],
[
'id' => 11,
'nom' => 'Ethan',
'code' => '🤯',
'force' => 9.0,
'robustesse' => 8.0,
'intelligence' => 10.0,
'vitesse' => 11.0,
'nbCombatGagne' => 3,
'rarete' => 3, // épique
],
[
'id' => 12,
'nom' => 'Fiona',
'code' => '🥺',
'force' => 4.0,
'robustesse' => 3.5,
'intelligence' => 5.0,
'vitesse' => 4.5,
'nbCombatGagne' => 1,
'rarete' => 1, // commun
],
[
'id' => 13,
'nom' => 'George',
'code' => '😜',
'force' => 11.0,
'robustesse' => 10.0,
'intelligence' => 12.0,
'vitesse' => 13.5,
'nbCombatGagne' => 2,
'rarete' => 3, // épique
],
[
'id' => 14,
'nom' => 'Hannah',
'code' => '😏',
'force' => 3.0,
'robustesse' => 2.5,
'intelligence' => 4.0,
'vitesse' => 3.5,
'nbCombatGagne' => 0,
'rarete' => 2, // rare
],
[
'id' => 15,
'nom' => 'Ian',
'code' => '😬',
'force' => 17.0,
'robustesse' => 14.0,
'intelligence' => 16.0,
'vitesse' => 18.0,
'nbCombatGagne' => 7,
'rarete' => 5, // légendaire
],
];
foreach ($emojisDeBase as &$emoji) {
$emoji['color'] = match ($emoji['rarete']) {
2 => 'green',
3 => 'purple',
4 => 'red',
5 => 'gold',
default => 'gray',
};
}
foreach ($emojisCrees as &$emoji) {
$emoji['color'] = match ($emoji['rarete']) {
1 => 'green', // commun
2 => 'purple', // épique
3 => 'red', // mythique
4 => 'gold', // légendaire
default => 'gray'
2 => 'green',
3 => 'purple',
4 => 'red',
5 => 'gold',
default => 'gray',
};
}
return $this->render('home/index.html.twig', [
'emojis' => $emojis,
'emojisDeBase' => $emojisDeBase,
'emojisCrees' => $emojisCrees,
]);
}
}

@ -0,0 +1,29 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class LoginController extends AbstractController
{
#[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('app_home');
}
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('auth/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
}
#[Route(path: '/logout', name: 'app_logout')]
public function logout(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
}

@ -0,0 +1,48 @@
<?php
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\LoginFormAuthenticator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
class RegistrationController extends AbstractController
{
#[Route('/register', name: 'app_register')]
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, LoginFormAuthenticator $authenticator, EntityManagerInterface $entityManager): Response
{
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user->setPassword(
$userPasswordHasher->hashPassword(
$user,
$form->get('plainPassword')->getData()
)
);
$user->setRoles(['ROLE_USER']);
$entityManager->persist($user);
$entityManager->flush();
return $userAuthenticator->authenticateUser(
$user,
$authenticator,
$request
);
}
return $this->render('auth/register.html.twig', [
'registrationForm' => $form->createView(),
]);
}
}

@ -0,0 +1,59 @@
<?php
namespace App\Controller;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Doctrine\ORM\EntityManagerInterface;
#[Route('/users', name: 'users_')]
class UserController extends AbstractController
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
#[Route('/{userId}', name: 'get_by_id', methods: ['GET'])]
public function getUserById(int $userId): JsonResponse
{
$user = $this->entityManager->getRepository(User::class)->find($userId);
if (!$user) {
return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
}
$data = [
'id' => $user->getId(),
'username' => $user->getUsername(),
'roles' => $user->getRoles(),
];
return $this->json($data);
}
#[Route('/{userId}/emojis', name: 'get_emojis_by_user', methods: ['GET'])]
public function getEmojisByUserId(int $userId): JsonResponse
{
$user = $this->entityManager->getRepository(User::class)->find($userId);
if (!$user) {
return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
}
$emojis = $user->getEmojis()->map(function ($emoji) {
return [
'id' => $emoji->getId(),
'name' => $emoji->getName(),
'code' => $emoji->getCode(),
];
})->toArray();
return $this->json($emojis);
}
}

@ -4,8 +4,10 @@ namespace App\Entity;
use App\Repository\EmojiRepository;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Metadata\ApiResource;
#[ORM\Entity(repositoryClass: EmojiRepository::class)]
#[ApiResource]
class Emoji
{
#[ORM\Id]
@ -160,4 +162,13 @@ class Emoji
$this->rarity = $rarity;
return $this;
}
public function wonFight() : self{
$this->fightsWon = $this->fightsWon + 1;
$this->strength += 1;
$this->toughness += 1;
$this->intelligence += 1;
$this->speed += 1;
return $this;
}
}

@ -4,8 +4,10 @@ namespace App\Entity;
use App\Repository\RarityRepository;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Metadata\ApiResource;
#[ORM\Entity(repositoryClass: RarityRepository::class)]
#[ApiResource]
class Rarity
{
#[ORM\Id]

@ -0,0 +1,52 @@
<?php
namespace App\Entity;
use App\Repository\StockEmojiRepository;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Metadata\ApiResource;
#[ORM\Entity(repositoryClass: StockEmojiRepository::class)]
#[ApiResource]
class StockEmoji
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $code = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
public function getId(): ?int
{
return $this->id;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): self
{
$this->code = $code;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
}

@ -0,0 +1,118 @@
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use ApiPlatform\Metadata\ApiResource;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[UniqueEntity(fields: ['username'], message: 'Un compte existe déjà avec ce nom d\'utilisateur')]
#[ApiResource]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $username = null;
#[ORM\Column]
private array $roles = [];
/**
* @var string The hashed password
*/
#[ORM\Column]
private ?string $password = null;
#[ORM\ManyToMany(targetEntity: Emoji::class, inversedBy: 'users')]
#[ORM\JoinTable(name: 'posseder')]
private Collection $emojis;
public function __construct() {
$this->emojis = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(string $username): static
{
$this->username = $username;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->username;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
public function getEmojis(): Collection
{
return $this->emojis;
}
/**
* @see UserInterface
*/
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}

@ -0,0 +1,44 @@
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('username')
->add('plainPassword', PasswordType::class, [
'mapped' => false,
'attr' => ['autocomplete' => 'new-password'],
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
'max' => 4096,
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

@ -0,0 +1,48 @@
<?php
namespace App\Repository;
use App\Entity\StockEmoji;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<StockEmoji>
*
* @method StockEmoji|null find($id, $lockMode = null, $lockVersion = null)
* @method StockEmoji|null findOneBy(array $criteria, array $orderBy = null)
* @method StockEmoji[] findAll()
* @method StockEmoji[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class StockEmojiRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, StockEmoji::class);
}
// /**
// * @return StockEmoji[] Returns an array of StockEmoji objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('s')
// ->andWhere('s.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('s.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?StockEmoji
// {
// return $this->createQueryBuilder('s')
// ->andWhere('s.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

@ -0,0 +1,67 @@
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* @extends ServiceEntityRepository<User>
*
* @implements PasswordUpgraderInterface<User>
*
* @method User|null find($id, $lockMode = null, $lockVersion = null)
* @method User|null findOneBy(array $criteria, array $orderBy = null)
* @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class));
}
$user->setPassword($newHashedPassword);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
}
// /**
// * @return User[] Returns an array of User objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('u.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?User
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

@ -0,0 +1,59 @@
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
public function __construct(private UrlGeneratorInterface $urlGenerator)
{
}
public function authenticate(Request $request): Passport
{
$username = $request->request->get('username', '');
$request->getSession()->set(Security::LAST_USERNAME, $username);
return new Passport(
new UserBadge($username),
new PasswordCredentials($request->request->get('password', '')),
[
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
new RememberMeBadge(),
]
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('app_home'));
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}

@ -0,0 +1,43 @@
{% extends 'base.html.twig' %}
{% block title %}Connexion{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/login.css') }}">
{% endblock %}
{% block body %}
<div class="login-container">
<h1 class="login-title">🔐 Connexion</h1>
{% if error %}
<div class="login-error">Mauvais nom d'utilisateur ou mot de passe.</div>
{% endif %}
{% if app.user %}
<div class="already-logged">
Connecté en tant que {{ app.user.userIdentifier }},
<a href="{{ path('app_logout') }}" class="logout-link">Se déconnecter</a>
</div>
{% endif %}
<form method="post" class="login-form">
<label for="inputUsername">Nom d'utilisateur</label>
<input type="text" id="inputUsername" name="username" value="{{ last_username }}" required autofocus>
<label for="inputPassword">Mot de passe</label>
<input type="password" id="inputPassword" name="password" required>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<button class="btn-login" type="submit">🚪 Se connecter</button>
</form>
</div>
<div class="no-account">
<p>Pas encore de compte ?</p>
<a href="{{ path('app_register') }}" class="btn-register">Créer un compte</a>
</div>
{% endblock %}

@ -0,0 +1,30 @@
{% extends 'base.html.twig' %}
{% block title %}Inscription{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/register.css') }}">
{% endblock %}
{% block body %}
<div class="register-container">
<h1 class="register-title">📝 Inscription</h1>
{{ form_start(registrationForm, {'attr': {'class': 'register-form'}}) }}
{{ form_errors(registrationForm) }}
{{ form_row(registrationForm.username) }}
{{ form_row(registrationForm.plainPassword, {
label: 'Mot de passe'
}) }}
<button type="submit" class="btn-register-submit">Créer mon compte</button>
<div class="already-account">
<p>Déjà inscrit ?</p>
<a href="{{ path('app_login') }}" class="btn-login-link">Se connecter</a>
</div>
{{ form_end(registrationForm) }}
</div>
{% endblock %}

@ -7,9 +7,49 @@
{% endblock %}
{% block body %}
{% if app.user %}
<div class="logout-container">
Connecté en tant que <strong>{{ app.user.username }}</strong>
<form action="{{ path('app_logout') }}" method="post" style="display: inline;">
<button type="submit" class="btn btn-logout">🚪 Se déconnecter</button>
</form>
</div>
{% endif %}
<h1>🧬 Ma collection de créatures 🐾</h1>
<h2 class="section-title">🌱 Vos créatures de base</h2>
<div class="emoji-container base">
{% for emoji in emojisDeBase %}
<div class="emoji-card {{ emoji.color }}"
data-id="{{ emoji.id }}"
data-name="{{ emoji.nom }}"
data-color="{{ emoji.color }}"
data-level="{{ emoji.nbCombatGagne }}"
data-force="{{ emoji.force }}"
data-vitesse="{{ emoji.vitesse }}">
<div class="emoji-level">Level {{ emoji.nbCombatGagne }}</div>
<div class="emoji">{{ emoji.code }}</div>
<div class="emoji-name {{ emoji.color }}">{{ emoji.nom }}</div>
<div class="detail-icon" data-id="{{ emoji.id }}"></div>
<div class="popup" id="popup-{{ emoji.id }}">
<strong>Stats :</strong><br>
Force: {{ emoji.force }}<br>
Robustesse: {{ emoji.robustesse }}<br>
Intelligence: {{ emoji.intelligence }}<br>
Vitesse: {{ emoji.vitesse }}
</div>
</div>
{% endfor %}
</div>
<div class="section-break-icon">🔻</div>
<h2 class="section-title">📦 Votre collection personnelle</h2>
<div class="filter-bar">
<label for="search-input">🔍 Rechercher un nom :</label>
<input type="text" id="search-input" placeholder="ex: Bob..." />
@ -32,18 +72,20 @@
</select>
</div>
<div id="selection-status">Sélectionnez 2 créatures...</div>
<div class="emoji-container">
{% for emoji in emojis %}
<div class="emoji-card {{ emoji.color }}" data-id="{{ emoji.id }}" data-name="{{ emoji.nom }}"
data-color="{{ emoji.color }}" data-level="{{ emoji.nbCombatGagne }}"
data-force="{{ emoji.force }}" data-vitesse="{{ emoji.vitesse }}">
<div class="emoji-container" id="collection-container">
{% for emoji in emojisCrees %}
<div class="emoji-card {{ emoji.color }}"
data-id="{{ emoji.id }}"
data-name="{{ emoji.nom }}"
data-color="{{ emoji.color }}"
data-level="{{ emoji.nbCombatGagne }}"
data-force="{{ emoji.force }}"
data-vitesse="{{ emoji.vitesse }}">
<div class="emoji-level">Level {{ emoji.nbCombatGagne }}</div>
<div class="emoji">{{ emoji.code }}</div>
<div class="emoji-name {{ emoji.color }}">{{ emoji.nom }}</div>
<div class="detail-icon"></div>
<div class="detail-icon" data-id="{{ emoji.id }}"></div>
<div class="popup" id="popup-{{ emoji.id }}">
<strong>Stats :</strong><br>
@ -56,14 +98,18 @@
{% endfor %}
</div>
<div id="pagination" class="pagination-bar"></div>
<div id="selection-status">Sélectionnez 2 créatures de votre choix pour commencer ...</div>
<div id="selection-visual" class="selection-visual"></div>
<div class="action-buttons">
<button class="btn" ">⚔️ Combattre</button>
<button class="btn" ">💞 Reproduire</button>
<button class="btn">⚔️ Combattre</button>
<button class="btn">💞 Reproduire</button>
</div>
{% endblock %}
{% block javascripts %}
<script src="{{ asset('js/home.js') }}"></script>
<script src="{{ asset('js/home.js?v=1.3') }}"></script>
{% endblock %}
Loading…
Cancel
Save