From 9d16c0e00a1d3060b4a4fcffb782ca9844ab37df Mon Sep 17 00:00:00 2001 From: bastien ollier Date: Wed, 22 May 2024 17:19:40 +0200 Subject: [PATCH 01/37] add register --- composer.json | 1 + composer.lock | 48 +++++++++- config/bundles.php | 1 + config/packages/security.yaml | 8 +- migrations/Version20240522150738.php | 47 +++++++++ src/Controller/RegistrationController.php | 44 +++++++++ src/Entity/User.php | 110 ++++++++++++++++++++++ src/Form/RegistrationFormType.php | 55 +++++++++++ src/Repository/UserRepository.php | 60 ++++++++++++ symfony.lock | 3 + templates/registration/register.html.twig | 19 ++++ 11 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 migrations/Version20240522150738.php create mode 100644 src/Controller/RegistrationController.php create mode 100644 src/Entity/User.php create mode 100644 src/Form/RegistrationFormType.php create mode 100644 src/Repository/UserRepository.php create mode 100644 templates/registration/register.html.twig diff --git a/composer.json b/composer.json index a582dc8..7d931a3 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,7 @@ "symfony/validator": "7.0.*", "symfony/web-link": "7.0.*", "symfony/yaml": "7.0.*", + "symfonycasts/verify-email-bundle": "^1.17", "twig/extra-bundle": "^2.12|^3.0", "twig/twig": "^2.12|^3.0" }, diff --git a/composer.lock b/composer.lock index bc06fd0..eea328a 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "c8031101044f75c20e905f33336bf7de", + "content-hash": "378620289156d5479d1a4f4a365e060e", "packages": [ { "name": "composer/semver", @@ -7240,6 +7240,52 @@ ], "time": "2024-04-28T11:44:19+00:00" }, + { + "name": "symfonycasts/verify-email-bundle", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/SymfonyCasts/verify-email-bundle.git", + "reference": "f72af149070b39ef82a7095074378d0a98b4d2ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SymfonyCasts/verify-email-bundle/zipball/f72af149070b39ef82a7095074378d0a98b4d2ef", + "reference": "f72af149070b39ef82a7095074378d0a98b4d2ef", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "symfony/config": "^5.4 | ^6.0 | ^7.0", + "symfony/dependency-injection": "^5.4 | ^6.0 | ^7.0", + "symfony/deprecation-contracts": "^2.2 | ^3.0", + "symfony/http-kernel": "^5.4 | ^6.0 | ^7.0", + "symfony/routing": "^5.4 | ^6.0 | ^7.0" + }, + "require-dev": { + "doctrine/orm": "^2.7", + "doctrine/persistence": "^2.0", + "symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0", + "symfony/phpunit-bridge": "^5.4 | ^6.0 | ^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "SymfonyCasts\\Bundle\\VerifyEmail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Simple, stylish Email Verification for Symfony", + "support": { + "issues": "https://github.com/SymfonyCasts/verify-email-bundle/issues", + "source": "https://github.com/SymfonyCasts/verify-email-bundle/tree/v1.17.0" + }, + "time": "2024-03-17T02:29:53+00:00" + }, { "name": "twig/extra-bundle", "version": "v3.10.0", diff --git a/config/bundles.php b/config/bundles.php index 4e3a560..cf72b69 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -13,4 +13,5 @@ return [ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true], ]; diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25..fbfb8ed 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -4,14 +4,18 @@ 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: email firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true - provider: users_in_memory + provider: app_user_provider # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall diff --git a/migrations/Version20240522150738.php b/migrations/Version20240522150738.php new file mode 100644 index 0000000..653ab20 --- /dev/null +++ b/migrations/Version20240522150738.php @@ -0,0 +1,47 @@ +addSql('CREATE TABLE post (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, found_date DATETIME NOT NULL --(DC2Type:datetime_immutable) + , publication_date DATETIME NOT NULL --(DC2Type:datetime_immutable) + , latitude DOUBLE PRECISION DEFAULT NULL, longitude DOUBLE PRECISION DEFAULT NULL, altitude DOUBLE PRECISION DEFAULT NULL, commentary CLOB NOT NULL)'); + $this->addSql('CREATE TABLE species (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, scientific_name VARCHAR(255) NOT NULL, vernacular_name VARCHAR(255) NOT NULL, region VARCHAR(255) NOT NULL)'); + $this->addSql('CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json) + , password VARCHAR(255) NOT NULL)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON user (email)'); + $this->addSql('CREATE TABLE messenger_messages (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, body CLOB NOT NULL, headers CLOB NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable) + , available_at DATETIME NOT NULL --(DC2Type:datetime_immutable) + , delivered_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable) + )'); + $this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)'); + $this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)'); + $this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE post'); + $this->addSql('DROP TABLE species'); + $this->addSql('DROP TABLE user'); + $this->addSql('DROP TABLE messenger_messages'); + } +} diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php new file mode 100644 index 0000000..6bfc237 --- /dev/null +++ b/src/Controller/RegistrationController.php @@ -0,0 +1,44 @@ +createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // encode the plain password + $user->setPassword( + $userPasswordHasher->hashPassword( + $user, + $form->get('plainPassword')->getData() + ) + ); + + $entityManager->persist($user); + $entityManager->flush(); + + // do anything else you need here, like send an email + + return $this->redirectToRoute('_profiler_home'); + } + + return $this->render('registration/register.html.twig', [ + 'registrationForm' => $form, + ]); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..b9e43ef --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,110 @@ + The user roles + */ + #[ORM\Column] + private array $roles = []; + + /** + * @var string The hashed password + */ + #[ORM\Column] + private ?string $password = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + * + * @return list + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + /** + * @param list $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; + } + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } +} diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php new file mode 100644 index 0000000..957e588 --- /dev/null +++ b/src/Form/RegistrationFormType.php @@ -0,0 +1,55 @@ +add('email') + ->add('agreeTerms', CheckboxType::class, [ + 'mapped' => false, + 'constraints' => [ + new IsTrue([ + 'message' => 'You should agree to our terms.', + ]), + ], + ]) + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + '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 length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..4f2804e --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,60 @@ + + */ +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() + // ; + // } +} diff --git a/symfony.lock b/symfony.lock index d4b2e9b..d8bf239 100644 --- a/symfony.lock +++ b/symfony.lock @@ -276,6 +276,9 @@ "config/routes/web_profiler.yaml" ] }, + "symfonycasts/verify-email-bundle": { + "version": "v1.17.0" + }, "twig/extra-bundle": { "version": "v3.10.0" } diff --git a/templates/registration/register.html.twig b/templates/registration/register.html.twig new file mode 100644 index 0000000..97f32c9 --- /dev/null +++ b/templates/registration/register.html.twig @@ -0,0 +1,19 @@ +{% extends 'base.html.twig' %} + +{% block title %}Register{% endblock %} + +{% block body %} +

Register

+ + {{ form_errors(registrationForm) }} + + {{ form_start(registrationForm) }} + {{ form_row(registrationForm.email) }} + {{ form_row(registrationForm.plainPassword, { + label: 'Password' + }) }} + {{ form_row(registrationForm.agreeTerms) }} + + + {{ form_end(registrationForm) }} +{% endblock %} -- 2.43.0 From 2faae9e088d5712b39e1d3368a29dd2b45786ae1 Mon Sep 17 00:00:00 2001 From: bastien ollier Date: Wed, 22 May 2024 17:30:31 +0200 Subject: [PATCH 02/37] Connexion par formulaire --- config/packages/security.yaml | 8 +++++ src/Controller/LoginController.php | 27 +++++++++++++++++ src/Controller/SecurityController.php | 32 ++++++++++++++++++++ templates/login/index.html.twig | 22 ++++++++++++++ templates/security/login.html.twig | 42 +++++++++++++++++++++++++++ 5 files changed, 131 insertions(+) create mode 100644 src/Controller/LoginController.php create mode 100644 src/Controller/SecurityController.php create mode 100644 templates/login/index.html.twig create mode 100644 templates/security/login.html.twig diff --git a/config/packages/security.yaml b/config/packages/security.yaml index fbfb8ed..405c27d 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -16,6 +16,14 @@ security: main: lazy: true provider: app_user_provider + form_login: + login_path: app_login + check_path: app_login + enable_csrf: true + logout: + path: app_logout + # where to redirect after logout + # target: app_any_route # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php new file mode 100644 index 0000000..ba8f478 --- /dev/null +++ b/src/Controller/LoginController.php @@ -0,0 +1,27 @@ +getLastAuthenticationError(); + + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('login/index.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..76bf5c4 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,32 @@ +getLastAuthenticationError(); + + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/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.'); + } +} diff --git a/templates/login/index.html.twig b/templates/login/index.html.twig new file mode 100644 index 0000000..9bdb835 --- /dev/null +++ b/templates/login/index.html.twig @@ -0,0 +1,22 @@ +{% extends 'base.html.twig' %} + +{% block title %}Hello LoginController!{% endblock %} + +{% block body %} + {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + +
+ + + + + + + {# If you want to control the URL the user is redirected to on success + #} + + +
+{% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..916c1d3 --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,42 @@ +{% extends 'base.html.twig' %} + +{% block title %}Log in!{% endblock %} + +{% block body %} +
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + + {% if app.user %} +
+ You are logged in as {{ app.user.userIdentifier }}, Logout +
+ {% endif %} + +

Please sign in

+ + + + + + + + {# + Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. + See https://symfony.com/doc/current/security/remember_me.html + +
+ +
+ #} + + +
+{% endblock %} -- 2.43.0 From efe59ee705e187b18cdc5d045445503a93d40fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Laporte?= Date: Wed, 5 Jun 2024 10:20:41 +0200 Subject: [PATCH 03/37] =?UTF-8?q?Cr=C3=A9ation=20du=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 6 ++++++ src/Controller/SpeciesController.php | 18 ++++++++++++++++++ templates/species/index.html.twig | 20 ++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 package-lock.json create mode 100644 src/Controller/SpeciesController.php create mode 100644 templates/species/index.html.twig diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..72e05cd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "herbarium", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/Controller/SpeciesController.php b/src/Controller/SpeciesController.php new file mode 100644 index 0000000..6a3d410 --- /dev/null +++ b/src/Controller/SpeciesController.php @@ -0,0 +1,18 @@ +render('species/index.html.twig', [ + 'controller_name' => 'SpeciesController', + ]); + } +} diff --git a/templates/species/index.html.twig b/templates/species/index.html.twig new file mode 100644 index 0000000..d9f27ea --- /dev/null +++ b/templates/species/index.html.twig @@ -0,0 +1,20 @@ +{% extends 'base.html.twig' %} + +{% block title %}Hello SpeciesController!{% endblock %} + +{% block body %} + + +
+

Hello {{ controller_name }}! ✅

+ + This friendly message is coming from: +
    +
  • Your controller at C:/wamp64/www/Symfony/herbarium/src/Controller/SpeciesController.php
  • +
  • Your template at C:/wamp64/www/Symfony/herbarium/templates/species/index.html.twig
  • +
+
+{% endblock %} -- 2.43.0 From 1b56f805b2d5786e1d86650643722959e6b83919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Laporte?= Date: Wed, 5 Jun 2024 10:20:41 +0200 Subject: [PATCH 04/37] =?UTF-8?q?Cr=C3=A9ation=20du=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 6 ++++++ src/Controller/SpeciesController.php | 18 ++++++++++++++++++ templates/species/index.html.twig | 20 ++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 package-lock.json create mode 100644 src/Controller/SpeciesController.php create mode 100644 templates/species/index.html.twig diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..72e05cd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "herbarium", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/Controller/SpeciesController.php b/src/Controller/SpeciesController.php new file mode 100644 index 0000000..6a3d410 --- /dev/null +++ b/src/Controller/SpeciesController.php @@ -0,0 +1,18 @@ +render('species/index.html.twig', [ + 'controller_name' => 'SpeciesController', + ]); + } +} diff --git a/templates/species/index.html.twig b/templates/species/index.html.twig new file mode 100644 index 0000000..d9f27ea --- /dev/null +++ b/templates/species/index.html.twig @@ -0,0 +1,20 @@ +{% extends 'base.html.twig' %} + +{% block title %}Hello SpeciesController!{% endblock %} + +{% block body %} + + +
+

Hello {{ controller_name }}! ✅

+ + This friendly message is coming from: +
    +
  • Your controller at C:/wamp64/www/Symfony/herbarium/src/Controller/SpeciesController.php
  • +
  • Your template at C:/wamp64/www/Symfony/herbarium/templates/species/index.html.twig
  • +
+
+{% endblock %} -- 2.43.0 From 6caa9a1ed08f1e696e188155cf0cd839bc675783 Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Wed, 5 Jun 2024 10:34:13 +0200 Subject: [PATCH 05/37] Fix lint --- src/Entity/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/User.php b/src/Entity/User.php index b9e43ef..8e4dab6 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -87,7 +87,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface /** * @see PasswordAuthenticatedUserInterface */ - public function getPassword(): string + public function getPassword(): ?string { return $this->password; } -- 2.43.0 From 48bc5fd8c3e94ec08c058eeab1f4108a4bf4ea55 Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Wed, 5 Jun 2024 10:36:04 +0200 Subject: [PATCH 06/37] Remove generated migrations --- .gitignore | 2 ++ migrations/Version20240522150738.php | 47 ---------------------------- 2 files changed, 2 insertions(+), 47 deletions(-) delete mode 100644 migrations/Version20240522150738.php diff --git a/.gitignore b/.gitignore index ff3a72e..be35b2f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ ###> phpstan/phpstan ### phpstan.neon ###< phpstan/phpstan ### + +migrations diff --git a/migrations/Version20240522150738.php b/migrations/Version20240522150738.php deleted file mode 100644 index 653ab20..0000000 --- a/migrations/Version20240522150738.php +++ /dev/null @@ -1,47 +0,0 @@ -addSql('CREATE TABLE post (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, found_date DATETIME NOT NULL --(DC2Type:datetime_immutable) - , publication_date DATETIME NOT NULL --(DC2Type:datetime_immutable) - , latitude DOUBLE PRECISION DEFAULT NULL, longitude DOUBLE PRECISION DEFAULT NULL, altitude DOUBLE PRECISION DEFAULT NULL, commentary CLOB NOT NULL)'); - $this->addSql('CREATE TABLE species (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, scientific_name VARCHAR(255) NOT NULL, vernacular_name VARCHAR(255) NOT NULL, region VARCHAR(255) NOT NULL)'); - $this->addSql('CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json) - , password VARCHAR(255) NOT NULL)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON user (email)'); - $this->addSql('CREATE TABLE messenger_messages (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, body CLOB NOT NULL, headers CLOB NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable) - , available_at DATETIME NOT NULL --(DC2Type:datetime_immutable) - , delivered_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable) - )'); - $this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)'); - $this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)'); - $this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP TABLE post'); - $this->addSql('DROP TABLE species'); - $this->addSql('DROP TABLE user'); - $this->addSql('DROP TABLE messenger_messages'); - } -} -- 2.43.0 From 22be991b8c52ef4c06d9280a32a0b931ca7cd04e Mon Sep 17 00:00:00 2001 From: syldium Date: Wed, 5 Jun 2024 17:37:53 +0200 Subject: [PATCH 07/37] Describe entities as API Platform resources --- .env | 4 + composer.json | 3 + composer.lock | 375 +++++++++++++++++- config/bundles.php | 3 + config/packages/api_platform.yaml | 19 + .../packages/dama_doctrine_test_bundle.yaml | 5 + config/packages/nelmio_cors.yaml | 10 + config/routes/api_platform.yaml | 4 + config/services.yaml | 4 + phpunit.xml.dist | 1 + src/Entity/Post.php | 29 ++ src/Entity/Species.php | 13 + src/Entity/User.php | 39 ++ src/State/UserPasswordHasher.php | 44 ++ symfony.lock | 38 ++ tests/Api/UserApiTest.php | 31 ++ 16 files changed, 621 insertions(+), 1 deletion(-) create mode 100644 config/packages/api_platform.yaml create mode 100644 config/packages/dama_doctrine_test_bundle.yaml create mode 100644 config/packages/nelmio_cors.yaml create mode 100644 config/routes/api_platform.yaml create mode 100644 src/State/UserPasswordHasher.php create mode 100644 tests/Api/UserApiTest.php diff --git a/.env b/.env index 0b4b6b2..b743dd6 100755 --- a/.env +++ b/.env @@ -39,3 +39,7 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###> symfony/mailer ### # MAILER_DSN=null://null ###< symfony/mailer ### + +###> nelmio/cors-bundle ### +CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' +###< nelmio/cors-bundle ### diff --git a/composer.json b/composer.json index d59ef97..0a5a255 100644 --- a/composer.json +++ b/composer.json @@ -7,10 +7,12 @@ "php": ">=8.2", "ext-ctype": "*", "ext-iconv": "*", + "api-platform/core": "^3.3", "doctrine/dbal": "^3", "doctrine/doctrine-bundle": "^2.12", "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.1", + "nelmio/cors-bundle": "^2.4", "phpdocumentor/reflection-docblock": "^5.4", "phpstan/phpdoc-parser": "^1.29", "runtime/frankenphp-symfony": "^0.2.0", @@ -97,6 +99,7 @@ } }, "require-dev": { + "dama/doctrine-test-bundle": "*", "doctrine/doctrine-fixtures-bundle": "^3.6", "fakerphp/faker": "^1.23", "phpstan/phpstan": "^1.11", diff --git a/composer.lock b/composer.lock index 9e8ea25..a528b05 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,196 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "888e1953f86d0f1fe9b271b85aab0ed0", + "content-hash": "cce9cbfaf4a49449e6431a8515f9d9eb", "packages": [ + { + "name": "api-platform/core", + "version": "v3.3.5", + "source": { + "type": "git", + "url": "https://github.com/api-platform/core.git", + "reference": "b5a93fb0bb855273aabb0807505ba61b68813246" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/core/zipball/b5a93fb0bb855273aabb0807505ba61b68813246", + "reference": "b5a93fb0bb855273aabb0807505ba61b68813246", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.0 || ^2.0", + "php": ">=8.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^3.1", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/property-access": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", + "symfony/translation-contracts": "^3.3", + "symfony/web-link": "^6.4 || ^7.0", + "willdurand/negotiation": "^3.0" + }, + "conflict": { + "doctrine/common": "<3.2.2", + "doctrine/dbal": "<2.10", + "doctrine/mongodb-odm": "<2.4", + "doctrine/orm": "<2.14.0", + "doctrine/persistence": "<1.3", + "elasticsearch/elasticsearch": ">=8.0,<8.4", + "phpspec/prophecy": "<1.15", + "phpunit/phpunit": "<9.5", + "symfony/framework-bundle": "6.4.6 || 7.0.6", + "symfony/var-exporter": "<6.1.1" + }, + "require-dev": { + "behat/behat": "^3.11", + "behat/mink": "^1.9", + "doctrine/cache": "^1.11 || ^2.1", + "doctrine/common": "^3.2.2", + "doctrine/dbal": "^3.4.0", + "doctrine/doctrine-bundle": "^1.12 || ^2.0", + "doctrine/mongodb-odm": "^2.2", + "doctrine/mongodb-odm-bundle": "^4.0 || ^5.0", + "doctrine/orm": "^2.14 || ^3.0", + "elasticsearch/elasticsearch": "^7.11 || ^8.4", + "friends-of-behat/mink-browserkit-driver": "^1.3.1", + "friends-of-behat/mink-extension": "^2.2", + "friends-of-behat/symfony-extension": "^2.1", + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "jangregor/phpstan-prophecy": "^1.0", + "justinrainbow/json-schema": "^5.2.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpdoc-parser": "^1.13", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-doctrine": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-symfony": "^1.0", + "phpunit/phpunit": "^9.6", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^3.9.7 || ^4.0", + "ramsey/uuid-doctrine": "^1.4 || ^2.0", + "sebastian/comparator": "<5.0", + "soyuka/contexts": "v3.3.9", + "soyuka/pmu": "^0.0.2", + "soyuka/stubs-mongodb": "^1.0", + "symfony/asset": "^6.4 || ^7.0", + "symfony/browser-kit": "^6.4 || ^7.0", + "symfony/cache": "^6.4 || ^7.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/css-selector": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0.12", + "symfony/doctrine-bridge": "^6.4 || ^7.0", + "symfony/dom-crawler": "^6.4 || ^7.0", + "symfony/error-handler": "^6.4 || ^7.0", + "symfony/event-dispatcher": "^6.4 || ^7.0", + "symfony/expression-language": "^6.4 || ^7.0", + "symfony/finder": "^6.4 || ^7.0", + "symfony/form": "^6.4 || ^7.0", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/http-client": "^6.4 || ^7.0", + "symfony/intl": "^6.4 || ^7.0", + "symfony/maker-bundle": "^1.24", + "symfony/mercure-bundle": "*", + "symfony/messenger": "^6.4 || ^7.0", + "symfony/phpunit-bridge": "^6.4.1 || ^7.0", + "symfony/routing": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/security-core": "^6.4 || ^7.0", + "symfony/stopwatch": "^6.4 || ^7.0", + "symfony/twig-bundle": "^6.4 || ^7.0", + "symfony/uid": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "symfony/web-profiler-bundle": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0", + "twig/twig": "^1.42.3 || ^2.12 || ^3.0", + "webonyx/graphql-php": "^14.0 || ^15.0" + }, + "suggest": { + "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.", + "elasticsearch/elasticsearch": "To support Elasticsearch.", + "ocramius/package-versions": "To display the API Platform's version in the debug bar.", + "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.", + "psr/cache-implementation": "To use metadata caching.", + "ramsey/uuid": "To support Ramsey's UUID identifiers.", + "symfony/cache": "To have metadata caching when using Symfony integration.", + "symfony/config": "To load XML configuration files.", + "symfony/expression-language": "To use authorization features.", + "symfony/http-client": "To use the HTTP cache invalidation system.", + "symfony/messenger": "To support messenger integration.", + "symfony/security": "To use authorization features.", + "symfony/twig-bundle": "To use the Swagger UI integration.", + "symfony/uid": "To support Symfony UUID/ULID identifiers.", + "symfony/web-profiler-bundle": "To use the data collector.", + "webonyx/graphql-php": "To support GraphQL." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3.x-dev" + }, + "symfony": { + "require": "^6.4 || ^7.0" + }, + "projects": [ + "api-platform/doctrine-common", + "api-platform/doctrine-orm", + "api-platform/doctrine-odm", + "api-platform/metadata", + "api-platform/json-schema", + "api-platform/elasticsearch", + "api-platform/jsonld", + "api-platform/hydra", + "api-platform/openapi", + "api-platform/graphql", + "api-platform/http-cache", + "api-platform/documentation", + "api-platform/parameter-validator", + "api-platform/ramsey-uuid", + "api-platform/serializer", + "api-platform/state", + "api-platform/symfony", + "api-platform/validator" + ] + }, + "autoload": { + "psr-4": { + "ApiPlatform\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + } + ], + "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", + "homepage": "https://api-platform.com", + "keywords": [ + "Hydra", + "JSON-LD", + "api", + "graphql", + "hal", + "jsonapi", + "openapi", + "rest", + "swagger" + ], + "support": { + "issues": "https://github.com/api-platform/core/issues", + "source": "https://github.com/api-platform/core/tree/v3.3.5" + }, + "time": "2024-05-29T05:48:47+00:00" + }, { "name": "composer/semver", "version": "3.4.0", @@ -1478,6 +1666,68 @@ ], "time": "2024-04-12T21:02:21+00:00" }, + { + "name": "nelmio/cors-bundle", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "78fcdb91f76b080a1008133def9c7f613833933d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/78fcdb91f76b080a1008133def9c7f613833933d", + "reference": "78fcdb91f76b080a1008133def9c7f613833933d", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.6", + "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.4.0" + }, + "time": "2023-11-30T16:41:19+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -7548,9 +7798,132 @@ "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "willdurand/negotiation", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/willdurand/Negotiation.git", + "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", + "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Negotiation\\": "src/Negotiation" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "will+git@drnd.me" + } + ], + "description": "Content Negotiation tools for PHP provided as a standalone library.", + "homepage": "http://williamdurand.fr/Negotiation/", + "keywords": [ + "accept", + "content", + "format", + "header", + "negotiation" + ], + "support": { + "issues": "https://github.com/willdurand/Negotiation/issues", + "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" + }, + "time": "2022-01-30T20:08:53+00:00" } ], "packages-dev": [ + { + "name": "dama/doctrine-test-bundle", + "version": "v8.2.0", + "source": { + "type": "git", + "url": "https://github.com/dmaicher/doctrine-test-bundle.git", + "reference": "1f81a280ea63f049d24e9c8ce00e557b18e0ff2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/1f81a280ea63f049d24e9c8ce00e557b18e0ff2f", + "reference": "1f81a280ea63f049d24e9c8ce00e557b18e0ff2f", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^3.3 || ^4.0", + "doctrine/doctrine-bundle": "^2.11.0", + "php": "^7.4 || ^8.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^5.4 || ^6.3 || ^7.0", + "symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0" + }, + "require-dev": { + "behat/behat": "^3.0", + "friendsofphp/php-cs-fixer": "^3.27", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "symfony/phpunit-bridge": "^6.3", + "symfony/process": "^5.4 || ^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.3 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "DAMA\\DoctrineTestBundle\\": "src/DAMA/DoctrineTestBundle" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David Maicher", + "email": "mail@dmaicher.de" + } + ], + "description": "Symfony bundle to isolate doctrine database tests and improve test performance", + "keywords": [ + "doctrine", + "isolation", + "performance", + "symfony", + "testing", + "tests" + ], + "support": { + "issues": "https://github.com/dmaicher/doctrine-test-bundle/issues", + "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.2.0" + }, + "time": "2024-05-28T15:41:06+00:00" + }, { "name": "doctrine/data-fixtures", "version": "1.7.0", diff --git a/config/bundles.php b/config/bundles.php index 3584c3f..fbce34f 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -15,4 +15,7 @@ return [ Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], + ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], + DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], ]; diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml new file mode 100644 index 0000000..1f8d9fb --- /dev/null +++ b/config/packages/api_platform.yaml @@ -0,0 +1,19 @@ +api_platform: + title: Hello API Platform + version: 1.0.0 + formats: + jsonld: ['application/ld+json'] + json: ['application/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 + keep_legacy_inflector: false + use_symfony_listeners: true diff --git a/config/packages/dama_doctrine_test_bundle.yaml b/config/packages/dama_doctrine_test_bundle.yaml new file mode 100644 index 0000000..3482cba --- /dev/null +++ b/config/packages/dama_doctrine_test_bundle.yaml @@ -0,0 +1,5 @@ +when@test: + dama_doctrine_test: + enable_static_connection: true + enable_static_meta_data_cache: true + enable_static_query_cache: true diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml new file mode 100644 index 0000000..c766508 --- /dev/null +++ b/config/packages/nelmio_cors.yaml @@ -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 diff --git a/config/routes/api_platform.yaml b/config/routes/api_platform.yaml new file mode 100644 index 0000000..38f11cb --- /dev/null +++ b/config/routes/api_platform.yaml @@ -0,0 +1,4 @@ +api_platform: + resource: . + type: api_platform + prefix: /api diff --git a/config/services.yaml b/config/services.yaml index 2d6a76f..2021197 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -20,5 +20,9 @@ services: - '../src/Entity/' - '../src/Kernel.php' + App\State\UserPasswordHasher: + bind: + $processor: '@api_platform.doctrine.orm.state.persist_processor' + # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6c4bfed..4e6c0e2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -34,5 +34,6 @@ + diff --git a/src/Entity/Post.php b/src/Entity/Post.php index 17ce70c..d2f968b 100644 --- a/src/Entity/Post.php +++ b/src/Entity/Post.php @@ -2,38 +2,60 @@ namespace App\Entity; +use ApiPlatform\Metadata; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; use App\Repository\PostRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Attribute\Groups; #[ORM\Entity(repositoryClass: PostRepository::class)] +#[ORM\HasLifecycleCallbacks] +#[ApiResource( + operations: [new Metadata\Post(), new Metadata\Get(), new Metadata\Put(), new Metadata\Delete(), new Metadata\Patch()], + normalizationContext: ['groups' => ['post:collection:read', 'post:read']], +)] +#[GetCollection(normalizationContext: ['groups' => ['post:collection:read']])] class Post { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] + #[Groups(['post:collection:read'])] private ?int $id = null; #[ORM\Column] + #[Groups(['post:collection:read'])] private ?\DateTimeImmutable $foundDate = null; #[ORM\Column] + #[ApiProperty(writable: false)] + #[Groups(['post:collection:read'])] private ?\DateTimeImmutable $publicationDate = null; #[ORM\Column(nullable: true)] + #[Groups(['post:collection:read'])] private ?float $latitude = null; #[ORM\Column(nullable: true)] + #[Groups(['post:collection:read'])] private ?float $longitude = null; #[ORM\Column(nullable: true)] + #[Groups(['post:collection:read'])] private ?float $altitude = null; #[ORM\Column(type: Types::TEXT)] + #[Groups(['post:read'])] private ?string $commentary = null; #[ORM\ManyToOne(inversedBy: 'posts')] + #[ApiProperty(readableLink: false)] + #[Groups(['post:collection:read'])] private ?Species $species = null; + public function getId(): ?int { return $this->id; @@ -122,4 +144,11 @@ class Post return $this; } + + #[ORM\PrePersist] + #[ORM\PreUpdate] + public function setPublicationDateValue(): void + { + $this->publicationDate = new \DateTimeImmutable(); + } } diff --git a/src/Entity/Species.php b/src/Entity/Species.php index 9243896..e2e1b78 100644 --- a/src/Entity/Species.php +++ b/src/Entity/Species.php @@ -2,32 +2,45 @@ namespace App\Entity; +use ApiPlatform\Metadata; +use ApiPlatform\Metadata\ApiResource; use App\Repository\SpeciesRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Attribute\Groups; #[ORM\Entity(repositoryClass: SpeciesRepository::class)] +#[ApiResource( + operations: [new Metadata\Post(), new Metadata\Get(), new Metadata\Put(), new Metadata\Delete(), new Metadata\Patch()], + normalizationContext: ['groups' => ['species:collection:read', 'species:read']], +)] +#[Metadata\GetCollection(normalizationContext: ['groups' => ['species:collection:read']])] class Species { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] + #[Groups(['species:collection:read'])] private ?int $id = null; #[ORM\Column(length: 255)] + #[Groups(['species:collection:read'])] private ?string $scientific_name = null; #[ORM\Column(length: 255)] + #[Groups(['species:collection:read'])] private ?string $vernacular_name = null; #[ORM\Column(length: 255)] + #[Groups(['species:collection:read'])] private ?string $region = null; /** * @var Collection */ #[ORM\OneToMany(targetEntity: Post::class, mappedBy: 'species')] + #[Groups(['species:read'])] private Collection $posts; public function __construct() diff --git a/src/Entity/User.php b/src/Entity/User.php index 8e4dab6..e924817 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -2,15 +2,36 @@ namespace App\Entity; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Put; use App\Repository\UserRepository; +use App\State\UserPasswordHasher; 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 Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])] #[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')] +#[ApiResource( + operations: [ + new GetCollection(), + new \ApiPlatform\Metadata\Post(validationContext: ['groups' => ['Default', 'user:create']], processor: UserPasswordHasher::class), + new Get(), + new Put(processor: UserPasswordHasher::class), + new Patch(processor: UserPasswordHasher::class), + new Delete(), + ], + normalizationContext: ['groups' => ['user:read']], + denormalizationContext: ['groups' => ['user:create', 'user:update']], +)] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] @@ -18,6 +39,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column] private ?int $id = null; + #[Assert\Email] + #[Groups(['user:read', 'user:create', 'user:update'])] #[ORM\Column(length: 180)] private ?string $email = null; @@ -33,6 +56,10 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column] private ?string $password = null; + #[Assert\NotBlank(groups: ['user:create'])] + #[Groups(['user:create', 'user:update'])] + private ?string $plainPassword = null; + public function getId(): ?int { return $this->id; @@ -99,6 +126,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + public function getPlainPassword(): ?string + { + return $this->plainPassword; + } + + public function setPlainPassword(?string $plainPassword): self + { + $this->plainPassword = $plainPassword; + + return $this; + } + /** * @see UserInterface */ diff --git a/src/State/UserPasswordHasher.php b/src/State/UserPasswordHasher.php new file mode 100644 index 0000000..0fc91be --- /dev/null +++ b/src/State/UserPasswordHasher.php @@ -0,0 +1,44 @@ + + */ +final readonly class UserPasswordHasher implements ProcessorInterface +{ + /** + * @param ProcessorInterface $processor + * @param UserPasswordHasherInterface $passwordHasher + */ + public function __construct( + private ProcessorInterface $processor, + private UserPasswordHasherInterface $passwordHasher, + ) + { + } + + /** + * @param User $data + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User + { + if (!$data->getPlainPassword()) { + return $this->processor->process($data, $operation, $uriVariables, $context); + } + + $hashedPassword = $this->passwordHasher->hashPassword( + $data, + $data->getPlainPassword() + ); + $data->setPassword($hashedPassword); + $data->eraseCredentials(); + + return $this->processor->process($data, $operation, $uriVariables, $context); + } +} diff --git a/symfony.lock b/symfony.lock index 59594a0..5e18ec6 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,4 +1,30 @@ { + "api-platform/core": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.3", + "ref": "74b45ac570c57eb1fbe56c984091a9ff87e18bab" + }, + "files": [ + "config/packages/api_platform.yaml", + "config/routes/api_platform.yaml", + "src/ApiResource/.gitignore" + ] + }, + "dama/doctrine-test-bundle": { + "version": "8.2", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "7.2", + "ref": "896306d79d4ee143af9eadf9b09fd34a8c391b70" + }, + "files": [ + "config/packages/dama_doctrine_test_bundle.yaml" + ] + }, "doctrine/doctrine-bundle": { "version": "2.12", "recipe": { @@ -38,6 +64,18 @@ "migrations/.gitignore" ] }, + "nelmio/cors-bundle": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.5", + "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" + }, + "files": [ + "config/packages/nelmio_cors.yaml" + ] + }, "phpstan/phpstan": { "version": "1.11", "recipe": { diff --git a/tests/Api/UserApiTest.php b/tests/Api/UserApiTest.php new file mode 100644 index 0000000..e5e997f --- /dev/null +++ b/tests/Api/UserApiTest.php @@ -0,0 +1,31 @@ +request('POST', '/api/users', [ + 'json' => [ + 'email' => 'test@test.com', + 'plainPassword' => 'password', + ] + ]); + + $this->assertResponseStatusCodeSame(201); + $this->assertResponseHasHeader('Content-Location'); + + $location = $response->getHeaders()['content-location'][0]; + static::createClient()->request('GET', $location); + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + '@context' => '/api/contexts/User', + '@id' => $location, + '@type' => 'User', + 'email' => 'test@test.com', + ]); + } +} -- 2.43.0 From 8789da089d4c20db120995907e89dcc05dbca824 Mon Sep 17 00:00:00 2001 From: syldium Date: Wed, 5 Jun 2024 18:06:08 +0200 Subject: [PATCH 08/37] Compile assets map --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index a5e8f9d..e5338da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ COPY . . RUN composer dump-autoload --classmap-authoritative --no-dev \ && composer dump-env prod \ && composer run-script --no-dev post-install-cmd \ + && php bin/console asset-map:compile \ ; RUN rm -Rf frankenphp/ -- 2.43.0 From 12f3b910444c7c1455eafe689277eb2809fa8e0a Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Wed, 5 Jun 2024 18:53:02 +0200 Subject: [PATCH 09/37] Set assets base path --- .drone.yml | 2 ++ .env | 2 ++ config/packages/framework.yaml | 3 +++ 3 files changed, 7 insertions(+) diff --git a/.drone.yml b/.drone.yml index 1acafc2..ddd4266 100644 --- a/.drone.yml +++ b/.drone.yml @@ -59,6 +59,8 @@ steps: environment: # Disable HTTPS redirection as it served by a reverse proxy CODEFIRST_CLIENTDRONE_ENV_SERVER_NAME: http://codefirst.iut.uca.fr + CODEFIRST_CLIENTDRONE_ENV_CORS_ALLOW_ORIGIN: https://codefirst.iut.uca.fr + CODEFIRST_CLIENTDRONE_ENV_ASSETS_BASE_PATH: /containers/clementfreville2-herbarium depends_on: - docker-image when: diff --git a/.env b/.env index b743dd6..1443282 100755 --- a/.env +++ b/.env @@ -14,6 +14,8 @@ # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration +ASSETS_BASE_PATH=/ + ###> symfony/framework-bundle ### APP_ENV=dev APP_SECRET=654d4972fc24500c7b43763fc3b3efa7 diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 877eb25..a09ef0c 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -3,6 +3,9 @@ framework: secret: '%env(APP_SECRET)%' #csrf_protection: true + assets: + base_path: '%env(ASSETS_BASE_PATH)%' + # Note that the session will be started ONLY if you read or write from it. session: true -- 2.43.0 From b94fd9a2a0b756014ba3641c877123b0b319100a Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Thu, 6 Jun 2024 20:08:47 +0200 Subject: [PATCH 10/37] Add more tests --- src/Entity/Post.php | 6 +- tests/Api/PostApiTest.php | 114 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 tests/Api/PostApiTest.php diff --git a/src/Entity/Post.php b/src/Entity/Post.php index d2f968b..0b54550 100644 --- a/src/Entity/Post.php +++ b/src/Entity/Post.php @@ -2,6 +2,7 @@ namespace App\Entity; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; @@ -18,6 +19,7 @@ use Symfony\Component\Serializer\Attribute\Groups; normalizationContext: ['groups' => ['post:collection:read', 'post:read']], )] #[GetCollection(normalizationContext: ['groups' => ['post:collection:read']])] +#[Metadata\ApiFilter(filterClass: SearchFilter::class, properties: ['species' => 'exact'])] class Post { #[ORM\Id] @@ -149,6 +151,8 @@ class Post #[ORM\PreUpdate] public function setPublicationDateValue(): void { - $this->publicationDate = new \DateTimeImmutable(); + if ($this->publicationDate === null) { + $this->publicationDate = new \DateTimeImmutable(); + } } } diff --git a/tests/Api/PostApiTest.php b/tests/Api/PostApiTest.php new file mode 100644 index 0000000..9304115 --- /dev/null +++ b/tests/Api/PostApiTest.php @@ -0,0 +1,114 @@ +getContainer()->get('doctrine')->getManager(); + $species = (new Species()) + ->setVernacularName('Oak') + ->setScientificName('Quercus') + ->setRegion('Europe'); + $post = (new Post()) + ->setPublicationDate(new \DateTimeImmutable('2024-06-07')) + ->setFoundDate(new \DateTimeImmutable('2024-06-06')) + ->setCommentary("maple") + ->setSpecies($species); + $manager->persist($species); + $manager->persist($post); + $manager->flush(); + $this->speciesId = $species->getId(); + $this->postId = $post->getId(); + } + + public function testFindInSpecies(): void + { + $response = static::createClient()->request('GET', '/api/species/' . $this->speciesId); + + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + '@context' => '/api/contexts/Species', + '@id' => '/api/species/' . $this->speciesId, + '@type' => 'Species', + 'id' => $this->speciesId, + 'vernacular_name' => 'Oak', + 'scientific_name' => 'Quercus', + 'region' => 'Europe', + 'posts' => ['/api/posts/' . $this->postId], + ]); + } + + public function testGetExisting(): void + { + $response = static::createClient()->request('GET', '/api/posts/' . $this->postId); + + $this->assertResponseIsSuccessful(); + $this->assertJsonEquals([ + '@context' => '/api/contexts/Post', + '@id' => '/api/posts/' . $this->postId, + '@type' => 'Post', + 'id' => $this->postId, + 'foundDate' => '2024-06-06T00:00:00+00:00', + 'publicationDate' => '2024-06-07T00:00:00+00:00', + 'commentary' => 'maple', + 'species' => '/api/species/' . $this->speciesId, + ]); + } + + public function testFilterBySpecies(): void + { + $response = static::createClient()->request('GET', '/api/posts', [ + 'query' => [ + 'species' => $this->speciesId, + ], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + '@context' => '/api/contexts/Post', + '@id' => '/api/posts', + '@type' => 'hydra:Collection', + 'hydra:totalItems' => 1, + 'hydra:member' => [ + [ + '@type' => 'Post', + 'id' => $this->postId, + 'foundDate' => '2024-06-06T00:00:00+00:00', + 'species' => '/api/species/' . $this->speciesId, + ], + ], + ]); + } + + public function testPostSetPublicationDate(): void + { + $response = static::createClient()->request('POST', '/api/posts', [ + 'json' => [ + 'foundDate' => '2024-06-06', + 'publicationDate' => '2024-06-07', + 'commentary' => 'maple', + 'species' => '/api/species/' . $this->speciesId, + ], + ]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + '@context' => '/api/contexts/Post', + '@type' => 'Post', + 'foundDate' => '2024-06-06T00:00:00+00:00', + 'commentary' => 'maple', + 'species' => '/api/species/' . $this->speciesId, + ]); + $this->assertArrayHasKey('publicationDate', $response->toArray(false)); + } +} -- 2.43.0 From 62da326e7508dfdca601a3d70938b9534ad729e2 Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Fri, 7 Jun 2024 09:10:06 +0200 Subject: [PATCH 11/37] Create an empty controllers directory --- assets/controllers/.gitignore | 0 templates/base.html.twig | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 assets/controllers/.gitignore diff --git a/assets/controllers/.gitignore b/assets/controllers/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/templates/base.html.twig b/templates/base.html.twig index bbf16f1..233148c 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -8,7 +8,7 @@ {% endblock %} {% block javascripts %} - {% block importmap %}{% endblock %} + {% block importmap %}{{ importmap('app') }}{% endblock %} {% endblock %} -- 2.43.0 From e5d265cc98443bea96053b103fde6f51e25b94b0 Mon Sep 17 00:00:00 2001 From: Matis MAZINGUE Date: Fri, 7 Jun 2024 15:26:54 +0200 Subject: [PATCH 12/37] post --- node_modules/.package-lock.json | 35 + node_modules/@popperjs/core/LICENSE.md | 20 + node_modules/@popperjs/core/README.md | 376 + node_modules/@popperjs/core/dist/cjs/enums.js | 65 + .../@popperjs/core/dist/cjs/enums.js.flow | 3 + .../@popperjs/core/dist/cjs/enums.js.map | 1 + .../@popperjs/core/dist/cjs/popper-base.js | 939 ++ .../core/dist/cjs/popper-base.js.flow | 3 + .../core/dist/cjs/popper-base.js.map | 1 + .../@popperjs/core/dist/cjs/popper-lite.js | 1260 ++ .../core/dist/cjs/popper-lite.js.flow | 3 + .../core/dist/cjs/popper-lite.js.map | 1 + .../@popperjs/core/dist/cjs/popper.js | 1819 +++ .../@popperjs/core/dist/cjs/popper.js.flow | 3 + .../@popperjs/core/dist/cjs/popper.js.map | 1 + .../@popperjs/core/dist/esm/createPopper.js | 199 + .../core/dist/esm/dom-utils/contains.js | 23 + .../esm/dom-utils/getBoundingClientRect.js | 41 + .../dist/esm/dom-utils/getClippingRect.js | 70 + .../dist/esm/dom-utils/getCompositeRect.js | 58 + .../dist/esm/dom-utils/getComputedStyle.js | 4 + .../dist/esm/dom-utils/getDocumentElement.js | 6 + .../dist/esm/dom-utils/getDocumentRect.js | 29 + .../esm/dom-utils/getHTMLElementScroll.js | 6 + .../core/dist/esm/dom-utils/getLayoutRect.js | 25 + .../core/dist/esm/dom-utils/getNodeName.js | 3 + .../core/dist/esm/dom-utils/getNodeScroll.js | 11 + .../dist/esm/dom-utils/getOffsetParent.js | 69 + .../core/dist/esm/dom-utils/getParentNode.js | 19 + .../dist/esm/dom-utils/getScrollParent.js | 16 + .../dist/esm/dom-utils/getViewportRect.js | 31 + .../core/dist/esm/dom-utils/getWindow.js | 12 + .../dist/esm/dom-utils/getWindowScroll.js | 10 + .../dist/esm/dom-utils/getWindowScrollBarX.js | 13 + .../core/dist/esm/dom-utils/instanceOf.js | 23 + .../dist/esm/dom-utils/isLayoutViewport.js | 4 + .../core/dist/esm/dom-utils/isScrollParent.js | 10 + .../core/dist/esm/dom-utils/isTableElement.js | 4 + .../dist/esm/dom-utils/listScrollParents.js | 26 + node_modules/@popperjs/core/dist/esm/enums.js | 31 + node_modules/@popperjs/core/dist/esm/index.js | 8 + .../core/dist/esm/modifiers/applyStyles.js | 84 + .../core/dist/esm/modifiers/arrow.js | 90 + .../core/dist/esm/modifiers/computeStyles.js | 169 + .../core/dist/esm/modifiers/eventListeners.js | 49 + .../@popperjs/core/dist/esm/modifiers/flip.js | 147 + .../@popperjs/core/dist/esm/modifiers/hide.js | 61 + .../core/dist/esm/modifiers/index.js | 9 + .../core/dist/esm/modifiers/offset.js | 54 + .../core/dist/esm/modifiers/popperOffsets.js | 25 + .../dist/esm/modifiers/preventOverflow.js | 142 + .../@popperjs/core/dist/esm/popper-base.js | 3 + .../@popperjs/core/dist/esm/popper-lite.js | 11 + .../@popperjs/core/dist/esm/popper.js | 20 + node_modules/@popperjs/core/dist/esm/types.js | 0 .../dist/esm/utils/computeAutoPlacement.js | 43 + .../core/dist/esm/utils/computeOffsets.js | 70 + .../@popperjs/core/dist/esm/utils/debounce.js | 15 + .../core/dist/esm/utils/detectOverflow.js | 65 + .../core/dist/esm/utils/expandToHashMap.js | 6 + .../core/dist/esm/utils/getAltAxis.js | 3 + .../core/dist/esm/utils/getAltLen.js | 3 + .../core/dist/esm/utils/getBasePlacement.js | 4 + .../core/dist/esm/utils/getFreshSideObject.js | 8 + .../esm/utils/getMainAxisFromPlacement.js | 3 + .../dist/esm/utils/getOppositePlacement.js | 11 + .../utils/getOppositeVariationPlacement.js | 9 + .../core/dist/esm/utils/getVariation.js | 3 + .../@popperjs/core/dist/esm/utils/math.js | 3 + .../core/dist/esm/utils/mergeByName.js | 14 + .../core/dist/esm/utils/mergePaddingObject.js | 4 + .../core/dist/esm/utils/orderModifiers.js | 44 + .../core/dist/esm/utils/rectToClientRect.js | 8 + .../@popperjs/core/dist/esm/utils/uniqueBy.js | 11 + .../core/dist/esm/utils/userAgent.js | 11 + .../@popperjs/core/dist/esm/utils/within.js | 8 + node_modules/@popperjs/core/dist/umd/enums.js | 71 + .../@popperjs/core/dist/umd/enums.js.map | 1 + .../@popperjs/core/dist/umd/enums.min.js | 6 + .../@popperjs/core/dist/umd/enums.min.js.flow | 3 + .../@popperjs/core/dist/umd/enums.min.js.map | 1 + .../@popperjs/core/dist/umd/popper-base.js | 945 ++ .../core/dist/umd/popper-base.js.map | 1 + .../core/dist/umd/popper-base.min.js | 6 + .../core/dist/umd/popper-base.min.js.flow | 3 + .../core/dist/umd/popper-base.min.js.map | 1 + .../@popperjs/core/dist/umd/popper-lite.js | 1266 ++ .../core/dist/umd/popper-lite.js.map | 1 + .../core/dist/umd/popper-lite.min.js | 6 + .../core/dist/umd/popper-lite.min.js.flow | 3 + .../core/dist/umd/popper-lite.min.js.map | 1 + .../@popperjs/core/dist/umd/popper.js | 1825 +++ .../@popperjs/core/dist/umd/popper.js.map | 1 + .../@popperjs/core/dist/umd/popper.min.js | 6 + .../core/dist/umd/popper.min.js.flow | 3 + .../@popperjs/core/dist/umd/popper.min.js.map | 1 + node_modules/@popperjs/core/index.d.ts | 1 + .../@popperjs/core/lib/createPopper.d.ts | 9 + .../@popperjs/core/lib/createPopper.js | 199 + .../@popperjs/core/lib/createPopper.js.flow | 218 + .../core/lib/dom-utils/contains.d.ts | 1 + .../@popperjs/core/lib/dom-utils/contains.js | 23 + .../core/lib/dom-utils/contains.js.flow | 25 + .../lib/dom-utils/getBoundingClientRect.d.ts | 2 + .../lib/dom-utils/getBoundingClientRect.js | 41 + .../dom-utils/getBoundingClientRect.js.flow | 52 + .../core/lib/dom-utils/getClippingRect.d.ts | 3 + .../core/lib/dom-utils/getClippingRect.js | 70 + .../lib/dom-utils/getClippingRect.js.flow | 106 + .../core/lib/dom-utils/getCompositeRect.d.ts | 2 + .../core/lib/dom-utils/getCompositeRect.js | 58 + .../lib/dom-utils/getCompositeRect.js.flow | 64 + .../core/lib/dom-utils/getComputedStyle.d.ts | 1 + .../core/lib/dom-utils/getComputedStyle.js | 4 + .../lib/dom-utils/getComputedStyle.js.flow | 8 + .../lib/dom-utils/getDocumentElement.d.ts | 2 + .../core/lib/dom-utils/getDocumentElement.js | 6 + .../lib/dom-utils/getDocumentElement.js.flow | 15 + .../core/lib/dom-utils/getDocumentRect.d.ts | 2 + .../core/lib/dom-utils/getDocumentRect.js | 29 + .../lib/dom-utils/getDocumentRect.js.flow | 37 + .../lib/dom-utils/getHTMLElementScroll.d.ts | 4 + .../lib/dom-utils/getHTMLElementScroll.js | 6 + .../dom-utils/getHTMLElementScroll.js.flow | 8 + .../core/lib/dom-utils/getLayoutRect.d.ts | 2 + .../core/lib/dom-utils/getLayoutRect.js | 25 + .../core/lib/dom-utils/getLayoutRect.js.flow | 29 + .../core/lib/dom-utils/getNodeName.d.ts | 2 + .../core/lib/dom-utils/getNodeName.js | 3 + .../core/lib/dom-utils/getNodeName.js.flow | 6 + .../core/lib/dom-utils/getNodeScroll.d.ts | 5 + .../core/lib/dom-utils/getNodeScroll.js | 11 + .../core/lib/dom-utils/getNodeScroll.js.flow | 14 + .../core/lib/dom-utils/getOffsetParent.d.ts | 1 + .../core/lib/dom-utils/getOffsetParent.js | 69 + .../lib/dom-utils/getOffsetParent.js.flow | 93 + .../core/lib/dom-utils/getParentNode.d.ts | 1 + .../core/lib/dom-utils/getParentNode.js | 19 + .../core/lib/dom-utils/getParentNode.js.flow | 21 + .../core/lib/dom-utils/getScrollParent.d.ts | 1 + .../core/lib/dom-utils/getScrollParent.js | 16 + .../lib/dom-utils/getScrollParent.js.flow | 18 + .../core/lib/dom-utils/getViewportRect.d.ts | 7 + .../core/lib/dom-utils/getViewportRect.js | 31 + .../lib/dom-utils/getViewportRect.js.flow | 39 + .../core/lib/dom-utils/getWindow.d.ts | 1 + .../@popperjs/core/lib/dom-utils/getWindow.js | 12 + .../core/lib/dom-utils/getWindow.js.flow | 16 + .../core/lib/dom-utils/getWindowScroll.d.ts | 5 + .../core/lib/dom-utils/getWindowScroll.js | 10 + .../lib/dom-utils/getWindowScroll.js.flow | 14 + .../lib/dom-utils/getWindowScrollBarX.d.ts | 1 + .../core/lib/dom-utils/getWindowScrollBarX.js | 13 + .../lib/dom-utils/getWindowScrollBarX.js.flow | 18 + .../core/lib/dom-utils/instanceOf.d.ts | 4 + .../core/lib/dom-utils/instanceOf.js | 23 + .../core/lib/dom-utils/instanceOf.js.flow | 29 + .../core/lib/dom-utils/isLayoutViewport.d.ts | 1 + .../core/lib/dom-utils/isLayoutViewport.js | 4 + .../lib/dom-utils/isLayoutViewport.js.flow | 6 + .../core/lib/dom-utils/isScrollParent.d.ts | 1 + .../core/lib/dom-utils/isScrollParent.js | 10 + .../core/lib/dom-utils/isScrollParent.js.flow | 8 + .../core/lib/dom-utils/isTableElement.d.ts | 1 + .../core/lib/dom-utils/isTableElement.js | 4 + .../core/lib/dom-utils/isTableElement.js.flow | 6 + .../core/lib/dom-utils/listScrollParents.d.ts | 2 + .../core/lib/dom-utils/listScrollParents.js | 26 + .../lib/dom-utils/listScrollParents.js.flow | 33 + node_modules/@popperjs/core/lib/enums.d.ts | 34 + node_modules/@popperjs/core/lib/enums.js | 31 + node_modules/@popperjs/core/lib/enums.js.flow | 91 + node_modules/@popperjs/core/lib/index.d.ts | 6 + node_modules/@popperjs/core/lib/index.js | 8 + node_modules/@popperjs/core/lib/index.js.flow | 13 + .../core/lib/modifiers/applyStyles.d.ts | 4 + .../core/lib/modifiers/applyStyles.js | 84 + .../core/lib/modifiers/applyStyles.js.flow | 98 + .../@popperjs/core/lib/modifiers/arrow.d.ts | 13 + .../@popperjs/core/lib/modifiers/arrow.js | 90 + .../core/lib/modifiers/arrow.js.flow | 120 + .../core/lib/modifiers/computeStyles.d.ts | 38 + .../core/lib/modifiers/computeStyles.js | 169 + .../core/lib/modifiers/computeStyles.js.flow | 233 + .../core/lib/modifiers/eventListeners.d.ts | 8 + .../core/lib/modifiers/eventListeners.js | 49 + .../core/lib/modifiers/eventListeners.js.flow | 54 + .../@popperjs/core/lib/modifiers/flip.d.ts | 16 + .../@popperjs/core/lib/modifiers/flip.js | 147 + .../@popperjs/core/lib/modifiers/flip.js.flow | 177 + .../@popperjs/core/lib/modifiers/hide.d.ts | 4 + .../@popperjs/core/lib/modifiers/hide.js | 61 + .../@popperjs/core/lib/modifiers/hide.js.flow | 76 + .../@popperjs/core/lib/modifiers/index.d.ts | 9 + .../@popperjs/core/lib/modifiers/index.js | 9 + .../core/lib/modifiers/index.js.flow | 10 + .../@popperjs/core/lib/modifiers/offset.d.ts | 18 + .../@popperjs/core/lib/modifiers/offset.js | 54 + .../core/lib/modifiers/offset.js.flow | 71 + .../core/lib/modifiers/popperOffsets.d.ts | 4 + .../core/lib/modifiers/popperOffsets.js | 25 + .../core/lib/modifiers/popperOffsets.js.flow | 26 + .../core/lib/modifiers/preventOverflow.d.ts | 30 + .../core/lib/modifiers/preventOverflow.js | 142 + .../lib/modifiers/preventOverflow.js.flow | 220 + .../@popperjs/core/lib/popper-base.d.ts | 3 + .../@popperjs/core/lib/popper-base.js | 3 + .../@popperjs/core/lib/popper-base.js.flow | 7 + .../@popperjs/core/lib/popper-lite.d.ts | 5 + .../@popperjs/core/lib/popper-lite.js | 11 + .../@popperjs/core/lib/popper-lite.js.flow | 21 + node_modules/@popperjs/core/lib/popper.d.ts | 7 + node_modules/@popperjs/core/lib/popper.js | 20 + .../@popperjs/core/lib/popper.js.flow | 35 + node_modules/@popperjs/core/lib/types.d.ts | 167 + node_modules/@popperjs/core/lib/types.js | 0 node_modules/@popperjs/core/lib/types.js.flow | 199 + .../core/lib/utils/computeAutoPlacement.d.ts | 12 + .../core/lib/utils/computeAutoPlacement.js | 43 + .../lib/utils/computeAutoPlacement.js.flow | 73 + .../core/lib/utils/computeOffsets.d.ts | 8 + .../core/lib/utils/computeOffsets.js | 70 + .../core/lib/utils/computeOffsets.js.flow | 82 + .../@popperjs/core/lib/utils/debounce.d.ts | 1 + .../@popperjs/core/lib/utils/debounce.js | 15 + .../@popperjs/core/lib/utils/debounce.js.flow | 17 + .../core/lib/utils/detectOverflow.d.ts | 12 + .../core/lib/utils/detectOverflow.js | 65 + .../core/lib/utils/detectOverflow.js.flow | 112 + .../core/lib/utils/expandToHashMap.d.ts | 3 + .../core/lib/utils/expandToHashMap.js | 6 + .../core/lib/utils/expandToHashMap.js.flow | 11 + .../@popperjs/core/lib/utils/getAltAxis.d.ts | 1 + .../@popperjs/core/lib/utils/getAltAxis.js | 3 + .../core/lib/utils/getAltAxis.js.flow | 5 + .../@popperjs/core/lib/utils/getAltLen.d.ts | 1 + .../@popperjs/core/lib/utils/getAltLen.js | 3 + .../core/lib/utils/getAltLen.js.flow | 5 + .../core/lib/utils/getBasePlacement.d.ts | 2 + .../core/lib/utils/getBasePlacement.js | 4 + .../core/lib/utils/getBasePlacement.js.flow | 8 + .../core/lib/utils/getFreshSideObject.d.ts | 2 + .../core/lib/utils/getFreshSideObject.js | 8 + .../core/lib/utils/getFreshSideObject.js.flow | 11 + .../lib/utils/getMainAxisFromPlacement.d.ts | 2 + .../lib/utils/getMainAxisFromPlacement.js | 3 + .../utils/getMainAxisFromPlacement.js.flow | 8 + .../core/lib/utils/getOppositePlacement.d.ts | 2 + .../core/lib/utils/getOppositePlacement.js | 11 + .../lib/utils/getOppositePlacement.js.flow | 11 + .../utils/getOppositeVariationPlacement.d.ts | 2 + .../utils/getOppositeVariationPlacement.js | 9 + .../getOppositeVariationPlacement.js.flow | 10 + .../core/lib/utils/getVariation.d.ts | 2 + .../@popperjs/core/lib/utils/getVariation.js | 3 + .../core/lib/utils/getVariation.js.flow | 6 + .../@popperjs/core/lib/utils/math.d.ts | 3 + node_modules/@popperjs/core/lib/utils/math.js | 3 + .../@popperjs/core/lib/utils/math.js.flow | 4 + .../@popperjs/core/lib/utils/mergeByName.d.ts | 2 + .../@popperjs/core/lib/utils/mergeByName.js | 14 + .../core/lib/utils/mergeByName.js.flow | 22 + .../core/lib/utils/mergePaddingObject.d.ts | 2 + .../core/lib/utils/mergePaddingObject.js | 4 + .../core/lib/utils/mergePaddingObject.js.flow | 12 + .../core/lib/utils/orderModifiers.d.ts | 2 + .../core/lib/utils/orderModifiers.js | 44 + .../core/lib/utils/orderModifiers.js.flow | 59 + .../core/lib/utils/rectToClientRect.d.ts | 2 + .../core/lib/utils/rectToClientRect.js | 8 + .../core/lib/utils/rectToClientRect.js.flow | 12 + .../@popperjs/core/lib/utils/uniqueBy.d.ts | 1 + .../@popperjs/core/lib/utils/uniqueBy.js | 11 + .../@popperjs/core/lib/utils/uniqueBy.js.flow | 14 + .../@popperjs/core/lib/utils/userAgent.d.ts | 1 + .../@popperjs/core/lib/utils/userAgent.js | 11 + .../core/lib/utils/userAgent.js.flow | 20 + .../@popperjs/core/lib/utils/within.d.ts | 2 + .../@popperjs/core/lib/utils/within.js | 8 + .../@popperjs/core/lib/utils/within.js.flow | 11 + node_modules/@popperjs/core/package.json | 119 + node_modules/bootstrap/LICENSE | 21 + node_modules/bootstrap/README.md | 246 + .../bootstrap/dist/css/bootstrap-grid.css | 4085 ++++++ .../bootstrap/dist/css/bootstrap-grid.css.map | 1 + .../bootstrap/dist/css/bootstrap-grid.min.css | 6 + .../dist/css/bootstrap-grid.min.css.map | 1 + .../bootstrap/dist/css/bootstrap-grid.rtl.css | 4084 ++++++ .../dist/css/bootstrap-grid.rtl.css.map | 1 + .../dist/css/bootstrap-grid.rtl.min.css | 6 + .../dist/css/bootstrap-grid.rtl.min.css.map | 1 + .../bootstrap/dist/css/bootstrap-reboot.css | 597 + .../dist/css/bootstrap-reboot.css.map | 1 + .../dist/css/bootstrap-reboot.min.css | 6 + .../dist/css/bootstrap-reboot.min.css.map | 1 + .../dist/css/bootstrap-reboot.rtl.css | 594 + .../dist/css/bootstrap-reboot.rtl.css.map | 1 + .../dist/css/bootstrap-reboot.rtl.min.css | 6 + .../dist/css/bootstrap-reboot.rtl.min.css.map | 1 + .../dist/css/bootstrap-utilities.css | 5402 +++++++ .../dist/css/bootstrap-utilities.css.map | 1 + .../dist/css/bootstrap-utilities.min.css | 6 + .../dist/css/bootstrap-utilities.min.css.map | 1 + .../dist/css/bootstrap-utilities.rtl.css | 5393 +++++++ .../dist/css/bootstrap-utilities.rtl.css.map | 1 + .../dist/css/bootstrap-utilities.rtl.min.css | 6 + .../css/bootstrap-utilities.rtl.min.css.map | 1 + node_modules/bootstrap/dist/css/bootstrap.css | 12057 ++++++++++++++++ .../bootstrap/dist/css/bootstrap.css.map | 1 + .../bootstrap/dist/css/bootstrap.min.css | 6 + .../bootstrap/dist/css/bootstrap.min.css.map | 1 + .../bootstrap/dist/css/bootstrap.rtl.css | 12030 +++++++++++++++ .../bootstrap/dist/css/bootstrap.rtl.css.map | 1 + .../bootstrap/dist/css/bootstrap.rtl.min.css | 6 + .../dist/css/bootstrap.rtl.min.css.map | 1 + .../bootstrap/dist/js/bootstrap.bundle.js | 6314 ++++++++ .../bootstrap/dist/js/bootstrap.bundle.js.map | 1 + .../bootstrap/dist/js/bootstrap.bundle.min.js | 7 + .../dist/js/bootstrap.bundle.min.js.map | 1 + .../bootstrap/dist/js/bootstrap.esm.js | 4447 ++++++ .../bootstrap/dist/js/bootstrap.esm.js.map | 1 + .../bootstrap/dist/js/bootstrap.esm.min.js | 7 + .../dist/js/bootstrap.esm.min.js.map | 1 + node_modules/bootstrap/dist/js/bootstrap.js | 4494 ++++++ .../bootstrap/dist/js/bootstrap.js.map | 1 + .../bootstrap/dist/js/bootstrap.min.js | 7 + .../bootstrap/dist/js/bootstrap.min.js.map | 1 + node_modules/bootstrap/js/dist/alert.js | 90 + node_modules/bootstrap/js/dist/alert.js.map | 1 + .../bootstrap/js/dist/base-component.js | 84 + .../bootstrap/js/dist/base-component.js.map | 1 + node_modules/bootstrap/js/dist/button.js | 79 + node_modules/bootstrap/js/dist/button.js.map | 1 + node_modules/bootstrap/js/dist/carousel.js | 388 + .../bootstrap/js/dist/carousel.js.map | 1 + node_modules/bootstrap/js/dist/collapse.js | 249 + .../bootstrap/js/dist/collapse.js.map | 1 + node_modules/bootstrap/js/dist/dom/data.js | 63 + .../bootstrap/js/dist/dom/data.js.map | 1 + .../bootstrap/js/dist/dom/event-handler.js | 237 + .../js/dist/dom/event-handler.js.map | 1 + .../bootstrap/js/dist/dom/manipulator.js | 72 + .../bootstrap/js/dist/dom/manipulator.js.map | 1 + .../bootstrap/js/dist/dom/selector-engine.js | 104 + .../js/dist/dom/selector-engine.js.map | 1 + node_modules/bootstrap/js/dist/dropdown.js | 402 + .../bootstrap/js/dist/dropdown.js.map | 1 + node_modules/bootstrap/js/dist/modal.js | 320 + node_modules/bootstrap/js/dist/modal.js.map | 1 + node_modules/bootstrap/js/dist/offcanvas.js | 246 + .../bootstrap/js/dist/offcanvas.js.map | 1 + node_modules/bootstrap/js/dist/popover.js | 96 + node_modules/bootstrap/js/dist/popover.js.map | 1 + node_modules/bootstrap/js/dist/scrollspy.js | 275 + .../bootstrap/js/dist/scrollspy.js.map | 1 + node_modules/bootstrap/js/dist/tab.js | 285 + node_modules/bootstrap/js/dist/tab.js.map | 1 + node_modules/bootstrap/js/dist/toast.js | 199 + node_modules/bootstrap/js/dist/toast.js.map | 1 + node_modules/bootstrap/js/dist/tooltip.js | 546 + node_modules/bootstrap/js/dist/tooltip.js.map | 1 + .../bootstrap/js/dist/util/backdrop.js | 139 + .../bootstrap/js/dist/util/backdrop.js.map | 1 + .../js/dist/util/component-functions.js | 42 + .../js/dist/util/component-functions.js.map | 1 + node_modules/bootstrap/js/dist/util/config.js | 68 + .../bootstrap/js/dist/util/config.js.map | 1 + .../bootstrap/js/dist/util/focustrap.js | 113 + .../bootstrap/js/dist/util/focustrap.js.map | 1 + node_modules/bootstrap/js/dist/util/index.js | 281 + .../bootstrap/js/dist/util/index.js.map | 1 + .../bootstrap/js/dist/util/sanitizer.js | 114 + .../bootstrap/js/dist/util/sanitizer.js.map | 1 + .../bootstrap/js/dist/util/scrollbar.js | 113 + .../bootstrap/js/dist/util/scrollbar.js.map | 1 + node_modules/bootstrap/js/dist/util/swipe.js | 135 + .../bootstrap/js/dist/util/swipe.js.map | 1 + .../js/dist/util/template-factory.js | 151 + .../js/dist/util/template-factory.js.map | 1 + node_modules/bootstrap/js/index.esm.js | 19 + node_modules/bootstrap/js/index.umd.js | 34 + node_modules/bootstrap/js/src/alert.js | 87 + .../bootstrap/js/src/base-component.js | 85 + node_modules/bootstrap/js/src/button.js | 72 + node_modules/bootstrap/js/src/carousel.js | 474 + node_modules/bootstrap/js/src/collapse.js | 297 + node_modules/bootstrap/js/src/dom/data.js | 55 + .../bootstrap/js/src/dom/event-handler.js | 317 + .../bootstrap/js/src/dom/manipulator.js | 71 + .../bootstrap/js/src/dom/selector-engine.js | 126 + node_modules/bootstrap/js/src/dropdown.js | 455 + node_modules/bootstrap/js/src/modal.js | 378 + node_modules/bootstrap/js/src/offcanvas.js | 282 + node_modules/bootstrap/js/src/popover.js | 97 + node_modules/bootstrap/js/src/scrollspy.js | 296 + node_modules/bootstrap/js/src/tab.js | 315 + node_modules/bootstrap/js/src/toast.js | 225 + node_modules/bootstrap/js/src/tooltip.js | 633 + .../bootstrap/js/src/util/backdrop.js | 151 + .../js/src/util/component-functions.js | 35 + node_modules/bootstrap/js/src/util/config.js | 65 + .../bootstrap/js/src/util/focustrap.js | 115 + node_modules/bootstrap/js/src/util/index.js | 306 + .../bootstrap/js/src/util/sanitizer.js | 117 + .../bootstrap/js/src/util/scrollbar.js | 114 + node_modules/bootstrap/js/src/util/swipe.js | 146 + .../bootstrap/js/src/util/template-factory.js | 160 + node_modules/bootstrap/package.json | 184 + node_modules/bootstrap/scss/_accordion.scss | 158 + node_modules/bootstrap/scss/_alert.scss | 68 + node_modules/bootstrap/scss/_badge.scss | 38 + node_modules/bootstrap/scss/_breadcrumb.scss | 40 + .../bootstrap/scss/_button-group.scss | 142 + node_modules/bootstrap/scss/_buttons.scss | 216 + node_modules/bootstrap/scss/_card.scss | 239 + node_modules/bootstrap/scss/_carousel.scss | 236 + node_modules/bootstrap/scss/_close.scss | 63 + node_modules/bootstrap/scss/_containers.scss | 41 + node_modules/bootstrap/scss/_dropdown.scss | 250 + node_modules/bootstrap/scss/_forms.scss | 9 + node_modules/bootstrap/scss/_functions.scss | 302 + node_modules/bootstrap/scss/_grid.scss | 39 + node_modules/bootstrap/scss/_helpers.scss | 12 + node_modules/bootstrap/scss/_images.scss | 42 + node_modules/bootstrap/scss/_list-group.scss | 197 + node_modules/bootstrap/scss/_maps.scss | 174 + node_modules/bootstrap/scss/_mixins.scss | 42 + node_modules/bootstrap/scss/_modal.scss | 236 + node_modules/bootstrap/scss/_nav.scss | 197 + node_modules/bootstrap/scss/_navbar.scss | 289 + node_modules/bootstrap/scss/_offcanvas.scss | 143 + node_modules/bootstrap/scss/_pagination.scss | 109 + .../bootstrap/scss/_placeholders.scss | 51 + node_modules/bootstrap/scss/_popover.scss | 196 + node_modules/bootstrap/scss/_progress.scss | 68 + node_modules/bootstrap/scss/_reboot.scss | 611 + node_modules/bootstrap/scss/_root.scss | 187 + node_modules/bootstrap/scss/_spinners.scss | 85 + node_modules/bootstrap/scss/_tables.scss | 171 + node_modules/bootstrap/scss/_toasts.scss | 73 + node_modules/bootstrap/scss/_tooltip.scss | 119 + node_modules/bootstrap/scss/_transitions.scss | 27 + node_modules/bootstrap/scss/_type.scss | 106 + node_modules/bootstrap/scss/_utilities.scss | 806 ++ .../bootstrap/scss/_variables-dark.scss | 87 + node_modules/bootstrap/scss/_variables.scss | 1751 +++ .../bootstrap/scss/bootstrap-grid.scss | 62 + .../bootstrap/scss/bootstrap-reboot.scss | 10 + .../bootstrap/scss/bootstrap-utilities.scss | 19 + node_modules/bootstrap/scss/bootstrap.scss | 52 + .../scss/forms/_floating-labels.scss | 95 + .../bootstrap/scss/forms/_form-check.scss | 189 + .../bootstrap/scss/forms/_form-control.scss | 214 + .../bootstrap/scss/forms/_form-range.scss | 91 + .../bootstrap/scss/forms/_form-select.scss | 80 + .../bootstrap/scss/forms/_form-text.scss | 11 + .../bootstrap/scss/forms/_input-group.scss | 132 + .../bootstrap/scss/forms/_labels.scss | 36 + .../bootstrap/scss/forms/_validation.scss | 12 + .../bootstrap/scss/helpers/_clearfix.scss | 3 + .../bootstrap/scss/helpers/_color-bg.scss | 7 + .../scss/helpers/_colored-links.scss | 30 + .../bootstrap/scss/helpers/_focus-ring.scss | 5 + .../bootstrap/scss/helpers/_icon-link.scss | 25 + .../bootstrap/scss/helpers/_position.scss | 36 + .../bootstrap/scss/helpers/_ratio.scss | 26 + .../bootstrap/scss/helpers/_stacks.scss | 15 + .../scss/helpers/_stretched-link.scss | 15 + .../scss/helpers/_text-truncation.scss | 7 + .../scss/helpers/_visually-hidden.scss | 8 + node_modules/bootstrap/scss/helpers/_vr.scss | 8 + .../bootstrap/scss/mixins/_alert.scss | 18 + .../bootstrap/scss/mixins/_backdrop.scss | 14 + .../bootstrap/scss/mixins/_banner.scss | 7 + .../bootstrap/scss/mixins/_border-radius.scss | 78 + .../bootstrap/scss/mixins/_box-shadow.scss | 18 + .../bootstrap/scss/mixins/_breakpoints.scss | 127 + .../bootstrap/scss/mixins/_buttons.scss | 70 + .../bootstrap/scss/mixins/_caret.scss | 69 + .../bootstrap/scss/mixins/_clearfix.scss | 9 + .../bootstrap/scss/mixins/_color-mode.scss | 21 + .../bootstrap/scss/mixins/_color-scheme.scss | 7 + .../bootstrap/scss/mixins/_container.scss | 11 + .../bootstrap/scss/mixins/_deprecate.scss | 10 + .../bootstrap/scss/mixins/_forms.scss | 163 + .../bootstrap/scss/mixins/_gradients.scss | 47 + node_modules/bootstrap/scss/mixins/_grid.scss | 151 + .../bootstrap/scss/mixins/_image.scss | 16 + .../bootstrap/scss/mixins/_list-group.scss | 26 + .../bootstrap/scss/mixins/_lists.scss | 7 + .../bootstrap/scss/mixins/_pagination.scss | 10 + .../bootstrap/scss/mixins/_reset-text.scss | 17 + .../bootstrap/scss/mixins/_resize.scss | 6 + .../scss/mixins/_table-variants.scss | 24 + .../bootstrap/scss/mixins/_text-truncate.scss | 8 + .../bootstrap/scss/mixins/_transition.scss | 26 + .../bootstrap/scss/mixins/_utilities.scss | 97 + .../scss/mixins/_visually-hidden.scss | 33 + .../bootstrap/scss/utilities/_api.scss | 47 + node_modules/bootstrap/scss/vendor/_rfs.scss | 348 + package-lock.json | 36 +- package.json | 5 + src/Controller/PostController.php | 10 +- src/Repository/PostRepository.php | 9 + templates/base.html.twig | 2 + templates/post/index.html.twig | 39 +- 506 files changed, 96932 insertions(+), 7 deletions(-) create mode 100644 node_modules/.package-lock.json create mode 100644 node_modules/@popperjs/core/LICENSE.md create mode 100644 node_modules/@popperjs/core/README.md create mode 100644 node_modules/@popperjs/core/dist/cjs/enums.js create mode 100644 node_modules/@popperjs/core/dist/cjs/enums.js.flow create mode 100644 node_modules/@popperjs/core/dist/cjs/enums.js.map create mode 100644 node_modules/@popperjs/core/dist/cjs/popper-base.js create mode 100644 node_modules/@popperjs/core/dist/cjs/popper-base.js.flow create mode 100644 node_modules/@popperjs/core/dist/cjs/popper-base.js.map create mode 100644 node_modules/@popperjs/core/dist/cjs/popper-lite.js create mode 100644 node_modules/@popperjs/core/dist/cjs/popper-lite.js.flow create mode 100644 node_modules/@popperjs/core/dist/cjs/popper-lite.js.map create mode 100644 node_modules/@popperjs/core/dist/cjs/popper.js create mode 100644 node_modules/@popperjs/core/dist/cjs/popper.js.flow create mode 100644 node_modules/@popperjs/core/dist/cjs/popper.js.map create mode 100644 node_modules/@popperjs/core/dist/esm/createPopper.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/contains.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getBoundingClientRect.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getClippingRect.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getCompositeRect.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getComputedStyle.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getDocumentElement.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getDocumentRect.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getHTMLElementScroll.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getLayoutRect.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getNodeName.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getNodeScroll.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getOffsetParent.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getParentNode.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getScrollParent.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getViewportRect.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getWindow.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getWindowScroll.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/getWindowScrollBarX.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/instanceOf.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/isLayoutViewport.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/isScrollParent.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/isTableElement.js create mode 100644 node_modules/@popperjs/core/dist/esm/dom-utils/listScrollParents.js create mode 100644 node_modules/@popperjs/core/dist/esm/enums.js create mode 100644 node_modules/@popperjs/core/dist/esm/index.js create mode 100644 node_modules/@popperjs/core/dist/esm/modifiers/applyStyles.js create mode 100644 node_modules/@popperjs/core/dist/esm/modifiers/arrow.js create mode 100644 node_modules/@popperjs/core/dist/esm/modifiers/computeStyles.js create mode 100644 node_modules/@popperjs/core/dist/esm/modifiers/eventListeners.js create mode 100644 node_modules/@popperjs/core/dist/esm/modifiers/flip.js create mode 100644 node_modules/@popperjs/core/dist/esm/modifiers/hide.js create mode 100644 node_modules/@popperjs/core/dist/esm/modifiers/index.js create mode 100644 node_modules/@popperjs/core/dist/esm/modifiers/offset.js create mode 100644 node_modules/@popperjs/core/dist/esm/modifiers/popperOffsets.js create mode 100644 node_modules/@popperjs/core/dist/esm/modifiers/preventOverflow.js create mode 100644 node_modules/@popperjs/core/dist/esm/popper-base.js create mode 100644 node_modules/@popperjs/core/dist/esm/popper-lite.js create mode 100644 node_modules/@popperjs/core/dist/esm/popper.js create mode 100644 node_modules/@popperjs/core/dist/esm/types.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/computeAutoPlacement.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/computeOffsets.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/debounce.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/detectOverflow.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/expandToHashMap.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/getAltAxis.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/getAltLen.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/getBasePlacement.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/getFreshSideObject.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/getMainAxisFromPlacement.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/getOppositePlacement.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/getOppositeVariationPlacement.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/getVariation.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/math.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/mergeByName.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/mergePaddingObject.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/orderModifiers.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/rectToClientRect.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/uniqueBy.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/userAgent.js create mode 100644 node_modules/@popperjs/core/dist/esm/utils/within.js create mode 100644 node_modules/@popperjs/core/dist/umd/enums.js create mode 100644 node_modules/@popperjs/core/dist/umd/enums.js.map create mode 100644 node_modules/@popperjs/core/dist/umd/enums.min.js create mode 100644 node_modules/@popperjs/core/dist/umd/enums.min.js.flow create mode 100644 node_modules/@popperjs/core/dist/umd/enums.min.js.map create mode 100644 node_modules/@popperjs/core/dist/umd/popper-base.js create mode 100644 node_modules/@popperjs/core/dist/umd/popper-base.js.map create mode 100644 node_modules/@popperjs/core/dist/umd/popper-base.min.js create mode 100644 node_modules/@popperjs/core/dist/umd/popper-base.min.js.flow create mode 100644 node_modules/@popperjs/core/dist/umd/popper-base.min.js.map create mode 100644 node_modules/@popperjs/core/dist/umd/popper-lite.js create mode 100644 node_modules/@popperjs/core/dist/umd/popper-lite.js.map create mode 100644 node_modules/@popperjs/core/dist/umd/popper-lite.min.js create mode 100644 node_modules/@popperjs/core/dist/umd/popper-lite.min.js.flow create mode 100644 node_modules/@popperjs/core/dist/umd/popper-lite.min.js.map create mode 100644 node_modules/@popperjs/core/dist/umd/popper.js create mode 100644 node_modules/@popperjs/core/dist/umd/popper.js.map create mode 100644 node_modules/@popperjs/core/dist/umd/popper.min.js create mode 100644 node_modules/@popperjs/core/dist/umd/popper.min.js.flow create mode 100644 node_modules/@popperjs/core/dist/umd/popper.min.js.map create mode 100644 node_modules/@popperjs/core/index.d.ts create mode 100644 node_modules/@popperjs/core/lib/createPopper.d.ts create mode 100644 node_modules/@popperjs/core/lib/createPopper.js create mode 100644 node_modules/@popperjs/core/lib/createPopper.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/contains.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/contains.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/contains.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getClippingRect.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getNodeName.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getNodeName.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getNodeName.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getParentNode.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getParentNode.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getParentNode.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getScrollParent.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getViewportRect.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getWindow.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getWindow.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getWindow.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/instanceOf.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/instanceOf.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/instanceOf.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/isScrollParent.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/isTableElement.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/isTableElement.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/isTableElement.js.flow create mode 100644 node_modules/@popperjs/core/lib/dom-utils/listScrollParents.d.ts create mode 100644 node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js create mode 100644 node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js.flow create mode 100644 node_modules/@popperjs/core/lib/enums.d.ts create mode 100644 node_modules/@popperjs/core/lib/enums.js create mode 100644 node_modules/@popperjs/core/lib/enums.js.flow create mode 100644 node_modules/@popperjs/core/lib/index.d.ts create mode 100644 node_modules/@popperjs/core/lib/index.js create mode 100644 node_modules/@popperjs/core/lib/index.js.flow create mode 100644 node_modules/@popperjs/core/lib/modifiers/applyStyles.d.ts create mode 100644 node_modules/@popperjs/core/lib/modifiers/applyStyles.js create mode 100644 node_modules/@popperjs/core/lib/modifiers/applyStyles.js.flow create mode 100644 node_modules/@popperjs/core/lib/modifiers/arrow.d.ts create mode 100644 node_modules/@popperjs/core/lib/modifiers/arrow.js create mode 100644 node_modules/@popperjs/core/lib/modifiers/arrow.js.flow create mode 100644 node_modules/@popperjs/core/lib/modifiers/computeStyles.d.ts create mode 100644 node_modules/@popperjs/core/lib/modifiers/computeStyles.js create mode 100644 node_modules/@popperjs/core/lib/modifiers/computeStyles.js.flow create mode 100644 node_modules/@popperjs/core/lib/modifiers/eventListeners.d.ts create mode 100644 node_modules/@popperjs/core/lib/modifiers/eventListeners.js create mode 100644 node_modules/@popperjs/core/lib/modifiers/eventListeners.js.flow create mode 100644 node_modules/@popperjs/core/lib/modifiers/flip.d.ts create mode 100644 node_modules/@popperjs/core/lib/modifiers/flip.js create mode 100644 node_modules/@popperjs/core/lib/modifiers/flip.js.flow create mode 100644 node_modules/@popperjs/core/lib/modifiers/hide.d.ts create mode 100644 node_modules/@popperjs/core/lib/modifiers/hide.js create mode 100644 node_modules/@popperjs/core/lib/modifiers/hide.js.flow create mode 100644 node_modules/@popperjs/core/lib/modifiers/index.d.ts create mode 100644 node_modules/@popperjs/core/lib/modifiers/index.js create mode 100644 node_modules/@popperjs/core/lib/modifiers/index.js.flow create mode 100644 node_modules/@popperjs/core/lib/modifiers/offset.d.ts create mode 100644 node_modules/@popperjs/core/lib/modifiers/offset.js create mode 100644 node_modules/@popperjs/core/lib/modifiers/offset.js.flow create mode 100644 node_modules/@popperjs/core/lib/modifiers/popperOffsets.d.ts create mode 100644 node_modules/@popperjs/core/lib/modifiers/popperOffsets.js create mode 100644 node_modules/@popperjs/core/lib/modifiers/popperOffsets.js.flow create mode 100644 node_modules/@popperjs/core/lib/modifiers/preventOverflow.d.ts create mode 100644 node_modules/@popperjs/core/lib/modifiers/preventOverflow.js create mode 100644 node_modules/@popperjs/core/lib/modifiers/preventOverflow.js.flow create mode 100644 node_modules/@popperjs/core/lib/popper-base.d.ts create mode 100644 node_modules/@popperjs/core/lib/popper-base.js create mode 100644 node_modules/@popperjs/core/lib/popper-base.js.flow create mode 100644 node_modules/@popperjs/core/lib/popper-lite.d.ts create mode 100644 node_modules/@popperjs/core/lib/popper-lite.js create mode 100644 node_modules/@popperjs/core/lib/popper-lite.js.flow create mode 100644 node_modules/@popperjs/core/lib/popper.d.ts create mode 100644 node_modules/@popperjs/core/lib/popper.js create mode 100644 node_modules/@popperjs/core/lib/popper.js.flow create mode 100644 node_modules/@popperjs/core/lib/types.d.ts create mode 100644 node_modules/@popperjs/core/lib/types.js create mode 100644 node_modules/@popperjs/core/lib/types.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/computeAutoPlacement.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js create mode 100644 node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/computeOffsets.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/computeOffsets.js create mode 100644 node_modules/@popperjs/core/lib/utils/computeOffsets.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/debounce.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/debounce.js create mode 100644 node_modules/@popperjs/core/lib/utils/debounce.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/detectOverflow.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/detectOverflow.js create mode 100644 node_modules/@popperjs/core/lib/utils/detectOverflow.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/expandToHashMap.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/expandToHashMap.js create mode 100644 node_modules/@popperjs/core/lib/utils/expandToHashMap.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/getAltAxis.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/getAltAxis.js create mode 100644 node_modules/@popperjs/core/lib/utils/getAltAxis.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/getAltLen.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/getAltLen.js create mode 100644 node_modules/@popperjs/core/lib/utils/getAltLen.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/getBasePlacement.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/getBasePlacement.js create mode 100644 node_modules/@popperjs/core/lib/utils/getBasePlacement.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/getFreshSideObject.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/getFreshSideObject.js create mode 100644 node_modules/@popperjs/core/lib/utils/getFreshSideObject.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js create mode 100644 node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/getOppositePlacement.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/getOppositePlacement.js create mode 100644 node_modules/@popperjs/core/lib/utils/getOppositePlacement.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js create mode 100644 node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/getVariation.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/getVariation.js create mode 100644 node_modules/@popperjs/core/lib/utils/getVariation.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/math.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/math.js create mode 100644 node_modules/@popperjs/core/lib/utils/math.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/mergeByName.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/mergeByName.js create mode 100644 node_modules/@popperjs/core/lib/utils/mergeByName.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/mergePaddingObject.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/mergePaddingObject.js create mode 100644 node_modules/@popperjs/core/lib/utils/mergePaddingObject.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/orderModifiers.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/orderModifiers.js create mode 100644 node_modules/@popperjs/core/lib/utils/orderModifiers.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/rectToClientRect.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/rectToClientRect.js create mode 100644 node_modules/@popperjs/core/lib/utils/rectToClientRect.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/uniqueBy.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/uniqueBy.js create mode 100644 node_modules/@popperjs/core/lib/utils/uniqueBy.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/userAgent.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/userAgent.js create mode 100644 node_modules/@popperjs/core/lib/utils/userAgent.js.flow create mode 100644 node_modules/@popperjs/core/lib/utils/within.d.ts create mode 100644 node_modules/@popperjs/core/lib/utils/within.js create mode 100644 node_modules/@popperjs/core/lib/utils/within.js.flow create mode 100644 node_modules/@popperjs/core/package.json create mode 100644 node_modules/bootstrap/LICENSE create mode 100644 node_modules/bootstrap/README.md create mode 100644 node_modules/bootstrap/dist/css/bootstrap-grid.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-grid.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-grid.min.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-grid.min.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-grid.rtl.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-grid.rtl.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-grid.rtl.min.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-reboot.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-reboot.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-reboot.min.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-reboot.min.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-reboot.rtl.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-reboot.rtl.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-reboot.rtl.min.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-utilities.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-utilities.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-utilities.min.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-utilities.min.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-utilities.rtl.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-utilities.rtl.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap-utilities.rtl.min.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap.min.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap.min.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap.rtl.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap.rtl.css.map create mode 100644 node_modules/bootstrap/dist/css/bootstrap.rtl.min.css create mode 100644 node_modules/bootstrap/dist/css/bootstrap.rtl.min.css.map create mode 100644 node_modules/bootstrap/dist/js/bootstrap.bundle.js create mode 100644 node_modules/bootstrap/dist/js/bootstrap.bundle.js.map create mode 100644 node_modules/bootstrap/dist/js/bootstrap.bundle.min.js create mode 100644 node_modules/bootstrap/dist/js/bootstrap.bundle.min.js.map create mode 100644 node_modules/bootstrap/dist/js/bootstrap.esm.js create mode 100644 node_modules/bootstrap/dist/js/bootstrap.esm.js.map create mode 100644 node_modules/bootstrap/dist/js/bootstrap.esm.min.js create mode 100644 node_modules/bootstrap/dist/js/bootstrap.esm.min.js.map create mode 100644 node_modules/bootstrap/dist/js/bootstrap.js create mode 100644 node_modules/bootstrap/dist/js/bootstrap.js.map create mode 100644 node_modules/bootstrap/dist/js/bootstrap.min.js create mode 100644 node_modules/bootstrap/dist/js/bootstrap.min.js.map create mode 100644 node_modules/bootstrap/js/dist/alert.js create mode 100644 node_modules/bootstrap/js/dist/alert.js.map create mode 100644 node_modules/bootstrap/js/dist/base-component.js create mode 100644 node_modules/bootstrap/js/dist/base-component.js.map create mode 100644 node_modules/bootstrap/js/dist/button.js create mode 100644 node_modules/bootstrap/js/dist/button.js.map create mode 100644 node_modules/bootstrap/js/dist/carousel.js create mode 100644 node_modules/bootstrap/js/dist/carousel.js.map create mode 100644 node_modules/bootstrap/js/dist/collapse.js create mode 100644 node_modules/bootstrap/js/dist/collapse.js.map create mode 100644 node_modules/bootstrap/js/dist/dom/data.js create mode 100644 node_modules/bootstrap/js/dist/dom/data.js.map create mode 100644 node_modules/bootstrap/js/dist/dom/event-handler.js create mode 100644 node_modules/bootstrap/js/dist/dom/event-handler.js.map create mode 100644 node_modules/bootstrap/js/dist/dom/manipulator.js create mode 100644 node_modules/bootstrap/js/dist/dom/manipulator.js.map create mode 100644 node_modules/bootstrap/js/dist/dom/selector-engine.js create mode 100644 node_modules/bootstrap/js/dist/dom/selector-engine.js.map create mode 100644 node_modules/bootstrap/js/dist/dropdown.js create mode 100644 node_modules/bootstrap/js/dist/dropdown.js.map create mode 100644 node_modules/bootstrap/js/dist/modal.js create mode 100644 node_modules/bootstrap/js/dist/modal.js.map create mode 100644 node_modules/bootstrap/js/dist/offcanvas.js create mode 100644 node_modules/bootstrap/js/dist/offcanvas.js.map create mode 100644 node_modules/bootstrap/js/dist/popover.js create mode 100644 node_modules/bootstrap/js/dist/popover.js.map create mode 100644 node_modules/bootstrap/js/dist/scrollspy.js create mode 100644 node_modules/bootstrap/js/dist/scrollspy.js.map create mode 100644 node_modules/bootstrap/js/dist/tab.js create mode 100644 node_modules/bootstrap/js/dist/tab.js.map create mode 100644 node_modules/bootstrap/js/dist/toast.js create mode 100644 node_modules/bootstrap/js/dist/toast.js.map create mode 100644 node_modules/bootstrap/js/dist/tooltip.js create mode 100644 node_modules/bootstrap/js/dist/tooltip.js.map create mode 100644 node_modules/bootstrap/js/dist/util/backdrop.js create mode 100644 node_modules/bootstrap/js/dist/util/backdrop.js.map create mode 100644 node_modules/bootstrap/js/dist/util/component-functions.js create mode 100644 node_modules/bootstrap/js/dist/util/component-functions.js.map create mode 100644 node_modules/bootstrap/js/dist/util/config.js create mode 100644 node_modules/bootstrap/js/dist/util/config.js.map create mode 100644 node_modules/bootstrap/js/dist/util/focustrap.js create mode 100644 node_modules/bootstrap/js/dist/util/focustrap.js.map create mode 100644 node_modules/bootstrap/js/dist/util/index.js create mode 100644 node_modules/bootstrap/js/dist/util/index.js.map create mode 100644 node_modules/bootstrap/js/dist/util/sanitizer.js create mode 100644 node_modules/bootstrap/js/dist/util/sanitizer.js.map create mode 100644 node_modules/bootstrap/js/dist/util/scrollbar.js create mode 100644 node_modules/bootstrap/js/dist/util/scrollbar.js.map create mode 100644 node_modules/bootstrap/js/dist/util/swipe.js create mode 100644 node_modules/bootstrap/js/dist/util/swipe.js.map create mode 100644 node_modules/bootstrap/js/dist/util/template-factory.js create mode 100644 node_modules/bootstrap/js/dist/util/template-factory.js.map create mode 100644 node_modules/bootstrap/js/index.esm.js create mode 100644 node_modules/bootstrap/js/index.umd.js create mode 100644 node_modules/bootstrap/js/src/alert.js create mode 100644 node_modules/bootstrap/js/src/base-component.js create mode 100644 node_modules/bootstrap/js/src/button.js create mode 100644 node_modules/bootstrap/js/src/carousel.js create mode 100644 node_modules/bootstrap/js/src/collapse.js create mode 100644 node_modules/bootstrap/js/src/dom/data.js create mode 100644 node_modules/bootstrap/js/src/dom/event-handler.js create mode 100644 node_modules/bootstrap/js/src/dom/manipulator.js create mode 100644 node_modules/bootstrap/js/src/dom/selector-engine.js create mode 100644 node_modules/bootstrap/js/src/dropdown.js create mode 100644 node_modules/bootstrap/js/src/modal.js create mode 100644 node_modules/bootstrap/js/src/offcanvas.js create mode 100644 node_modules/bootstrap/js/src/popover.js create mode 100644 node_modules/bootstrap/js/src/scrollspy.js create mode 100644 node_modules/bootstrap/js/src/tab.js create mode 100644 node_modules/bootstrap/js/src/toast.js create mode 100644 node_modules/bootstrap/js/src/tooltip.js create mode 100644 node_modules/bootstrap/js/src/util/backdrop.js create mode 100644 node_modules/bootstrap/js/src/util/component-functions.js create mode 100644 node_modules/bootstrap/js/src/util/config.js create mode 100644 node_modules/bootstrap/js/src/util/focustrap.js create mode 100644 node_modules/bootstrap/js/src/util/index.js create mode 100644 node_modules/bootstrap/js/src/util/sanitizer.js create mode 100644 node_modules/bootstrap/js/src/util/scrollbar.js create mode 100644 node_modules/bootstrap/js/src/util/swipe.js create mode 100644 node_modules/bootstrap/js/src/util/template-factory.js create mode 100644 node_modules/bootstrap/package.json create mode 100644 node_modules/bootstrap/scss/_accordion.scss create mode 100644 node_modules/bootstrap/scss/_alert.scss create mode 100644 node_modules/bootstrap/scss/_badge.scss create mode 100644 node_modules/bootstrap/scss/_breadcrumb.scss create mode 100644 node_modules/bootstrap/scss/_button-group.scss create mode 100644 node_modules/bootstrap/scss/_buttons.scss create mode 100644 node_modules/bootstrap/scss/_card.scss create mode 100644 node_modules/bootstrap/scss/_carousel.scss create mode 100644 node_modules/bootstrap/scss/_close.scss create mode 100644 node_modules/bootstrap/scss/_containers.scss create mode 100644 node_modules/bootstrap/scss/_dropdown.scss create mode 100644 node_modules/bootstrap/scss/_forms.scss create mode 100644 node_modules/bootstrap/scss/_functions.scss create mode 100644 node_modules/bootstrap/scss/_grid.scss create mode 100644 node_modules/bootstrap/scss/_helpers.scss create mode 100644 node_modules/bootstrap/scss/_images.scss create mode 100644 node_modules/bootstrap/scss/_list-group.scss create mode 100644 node_modules/bootstrap/scss/_maps.scss create mode 100644 node_modules/bootstrap/scss/_mixins.scss create mode 100644 node_modules/bootstrap/scss/_modal.scss create mode 100644 node_modules/bootstrap/scss/_nav.scss create mode 100644 node_modules/bootstrap/scss/_navbar.scss create mode 100644 node_modules/bootstrap/scss/_offcanvas.scss create mode 100644 node_modules/bootstrap/scss/_pagination.scss create mode 100644 node_modules/bootstrap/scss/_placeholders.scss create mode 100644 node_modules/bootstrap/scss/_popover.scss create mode 100644 node_modules/bootstrap/scss/_progress.scss create mode 100644 node_modules/bootstrap/scss/_reboot.scss create mode 100644 node_modules/bootstrap/scss/_root.scss create mode 100644 node_modules/bootstrap/scss/_spinners.scss create mode 100644 node_modules/bootstrap/scss/_tables.scss create mode 100644 node_modules/bootstrap/scss/_toasts.scss create mode 100644 node_modules/bootstrap/scss/_tooltip.scss create mode 100644 node_modules/bootstrap/scss/_transitions.scss create mode 100644 node_modules/bootstrap/scss/_type.scss create mode 100644 node_modules/bootstrap/scss/_utilities.scss create mode 100644 node_modules/bootstrap/scss/_variables-dark.scss create mode 100644 node_modules/bootstrap/scss/_variables.scss create mode 100644 node_modules/bootstrap/scss/bootstrap-grid.scss create mode 100644 node_modules/bootstrap/scss/bootstrap-reboot.scss create mode 100644 node_modules/bootstrap/scss/bootstrap-utilities.scss create mode 100644 node_modules/bootstrap/scss/bootstrap.scss create mode 100644 node_modules/bootstrap/scss/forms/_floating-labels.scss create mode 100644 node_modules/bootstrap/scss/forms/_form-check.scss create mode 100644 node_modules/bootstrap/scss/forms/_form-control.scss create mode 100644 node_modules/bootstrap/scss/forms/_form-range.scss create mode 100644 node_modules/bootstrap/scss/forms/_form-select.scss create mode 100644 node_modules/bootstrap/scss/forms/_form-text.scss create mode 100644 node_modules/bootstrap/scss/forms/_input-group.scss create mode 100644 node_modules/bootstrap/scss/forms/_labels.scss create mode 100644 node_modules/bootstrap/scss/forms/_validation.scss create mode 100644 node_modules/bootstrap/scss/helpers/_clearfix.scss create mode 100644 node_modules/bootstrap/scss/helpers/_color-bg.scss create mode 100644 node_modules/bootstrap/scss/helpers/_colored-links.scss create mode 100644 node_modules/bootstrap/scss/helpers/_focus-ring.scss create mode 100644 node_modules/bootstrap/scss/helpers/_icon-link.scss create mode 100644 node_modules/bootstrap/scss/helpers/_position.scss create mode 100644 node_modules/bootstrap/scss/helpers/_ratio.scss create mode 100644 node_modules/bootstrap/scss/helpers/_stacks.scss create mode 100644 node_modules/bootstrap/scss/helpers/_stretched-link.scss create mode 100644 node_modules/bootstrap/scss/helpers/_text-truncation.scss create mode 100644 node_modules/bootstrap/scss/helpers/_visually-hidden.scss create mode 100644 node_modules/bootstrap/scss/helpers/_vr.scss create mode 100644 node_modules/bootstrap/scss/mixins/_alert.scss create mode 100644 node_modules/bootstrap/scss/mixins/_backdrop.scss create mode 100644 node_modules/bootstrap/scss/mixins/_banner.scss create mode 100644 node_modules/bootstrap/scss/mixins/_border-radius.scss create mode 100644 node_modules/bootstrap/scss/mixins/_box-shadow.scss create mode 100644 node_modules/bootstrap/scss/mixins/_breakpoints.scss create mode 100644 node_modules/bootstrap/scss/mixins/_buttons.scss create mode 100644 node_modules/bootstrap/scss/mixins/_caret.scss create mode 100644 node_modules/bootstrap/scss/mixins/_clearfix.scss create mode 100644 node_modules/bootstrap/scss/mixins/_color-mode.scss create mode 100644 node_modules/bootstrap/scss/mixins/_color-scheme.scss create mode 100644 node_modules/bootstrap/scss/mixins/_container.scss create mode 100644 node_modules/bootstrap/scss/mixins/_deprecate.scss create mode 100644 node_modules/bootstrap/scss/mixins/_forms.scss create mode 100644 node_modules/bootstrap/scss/mixins/_gradients.scss create mode 100644 node_modules/bootstrap/scss/mixins/_grid.scss create mode 100644 node_modules/bootstrap/scss/mixins/_image.scss create mode 100644 node_modules/bootstrap/scss/mixins/_list-group.scss create mode 100644 node_modules/bootstrap/scss/mixins/_lists.scss create mode 100644 node_modules/bootstrap/scss/mixins/_pagination.scss create mode 100644 node_modules/bootstrap/scss/mixins/_reset-text.scss create mode 100644 node_modules/bootstrap/scss/mixins/_resize.scss create mode 100644 node_modules/bootstrap/scss/mixins/_table-variants.scss create mode 100644 node_modules/bootstrap/scss/mixins/_text-truncate.scss create mode 100644 node_modules/bootstrap/scss/mixins/_transition.scss create mode 100644 node_modules/bootstrap/scss/mixins/_utilities.scss create mode 100644 node_modules/bootstrap/scss/mixins/_visually-hidden.scss create mode 100644 node_modules/bootstrap/scss/utilities/_api.scss create mode 100644 node_modules/bootstrap/scss/vendor/_rfs.scss create mode 100644 package.json diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..cda22f4 --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,35 @@ +{ + "name": "herbarium", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + } + } +} diff --git a/node_modules/@popperjs/core/LICENSE.md b/node_modules/@popperjs/core/LICENSE.md new file mode 100644 index 0000000..0370c45 --- /dev/null +++ b/node_modules/@popperjs/core/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2019 Federico Zivolo + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/@popperjs/core/README.md b/node_modules/@popperjs/core/README.md new file mode 100644 index 0000000..53be7b9 --- /dev/null +++ b/node_modules/@popperjs/core/README.md @@ -0,0 +1,376 @@ + +

+ Popper +

+ +
+

Tooltip & Popover Positioning Engine

+
+ +

+ + npm version + + + npm downloads per month (popper.js + @popperjs/core) + + + Rolling Versions + +

+ +
+ + +**Positioning tooltips and popovers is difficult. Popper is here to help!** + +Given an element, such as a button, and a tooltip element describing it, Popper +will automatically put the tooltip in the right place near the button. + +It will position _any_ UI element that "pops out" from the flow of your document +and floats near a target element. The most common example is a tooltip, but it +also includes popovers, drop-downs, and more. All of these can be generically +described as a "popper" element. + +## Demo + +[![Popper visualized](https://i.imgur.com/F7qWsmV.jpg)](https://popper.js.org) + +## Docs + +- [v2.x (latest)](https://popper.js.org/docs/v2/) +- [v1.x](https://popper.js.org/docs/v1/) + +We've created a +[Migration Guide](https://popper.js.org/docs/v2/migration-guide/) to help you +migrate from Popper 1 to Popper 2. + +To contribute to the Popper website and documentation, please visit the +[dedicated repository](https://github.com/popperjs/website). + +## Why not use pure CSS? + +- **Clipping and overflow issues**: Pure CSS poppers will not be prevented from + overflowing clipping boundaries, such as the viewport. It will get partially + cut off or overflows if it's near the edge since there is no dynamic + positioning logic. When using Popper, your popper will always be positioned in + the right place without needing manual adjustments. +- **No flipping**: CSS poppers will not flip to a different placement to fit + better in view if necessary. While you can manually adjust for the main axis + overflow, this feature cannot be achieved via CSS alone. Popper automatically + flips the tooltip to make it fit in view as best as possible for the user. +- **No virtual positioning**: CSS poppers cannot follow the mouse cursor or be + used as a context menu. Popper allows you to position your tooltip relative to + any coordinates you desire. +- **Slower development cycle**: When pure CSS is used to position popper + elements, the lack of dynamic positioning means they must be carefully placed + to consider overflow on all screen sizes. In reusable component libraries, + this means a developer can't just add the component anywhere on the page, + because these issues need to be considered and adjusted for every time. With + Popper, you can place your elements anywhere and they will be positioned + correctly, without needing to consider different screen sizes, layouts, etc. + This massively speeds up development time because this work is automatically + offloaded to Popper. +- **Lack of extensibility**: CSS poppers cannot be easily extended to fit any + arbitrary use case you may need to adjust for. Popper is built with + extensibility in mind. + +## Why Popper? + +With the CSS drawbacks out of the way, we now move on to Popper in the +JavaScript space itself. + +Naive JavaScript tooltip implementations usually have the following problems: + +- **Scrolling containers**: They don't ensure the tooltip stays with the + reference element while scrolling when inside any number of scrolling + containers. +- **DOM context**: They often require the tooltip move outside of its original + DOM context because they don't handle `offsetParent` contexts. +- **Compatibility**: Popper handles an incredible number of edge cases regarding + different browsers and environments (mobile viewports, RTL, scrollbars enabled + or disabled, etc.). Popper is a popular and well-maintained library, so you + can be confident positioning will work for your users on any device. +- **Configurability**: They often lack advanced configurability to suit any + possible use case. +- **Size**: They are usually relatively large in size, or require an ancient + jQuery dependency. +- **Performance**: They often have runtime performance issues and update the + tooltip position too slowly. + +**Popper solves all of these key problems in an elegant, performant manner.** It +is a lightweight ~3 kB library that aims to provide a reliable and extensible +positioning engine you can use to ensure all your popper elements are positioned +in the right place. + +When you start writing your own popper implementation, you'll quickly run into +all of the problems mentioned above. These widgets are incredibly common in our +UIs; we've done the hard work figuring this out so you don't need to spend hours +fixing and handling numerous edge cases that we already ran into while building +the library! + +Popper is used in popular libraries like Bootstrap, Foundation, Material UI, and +more. It's likely you've already used popper elements on the web positioned by +Popper at some point in the past few years. + +Since we write UIs using powerful abstraction libraries such as React or Angular +nowadays, you'll also be glad to know Popper can fully integrate with them and +be a good citizen together with your other components. Check out `react-popper` +for the official Popper wrapper for React. + +## Installation + +### 1. Package Manager + +```bash +# With npm +npm i @popperjs/core + +# With Yarn +yarn add @popperjs/core +``` + +### 2. CDN + +```html + + + + + +``` + +### 3. Direct Download? + +Managing dependencies by "directly downloading" them and placing them into your +source code is not recommended for a variety of reasons, including missing out +on feat/fix updates easily. Please use a versioning management system like a CDN +or npm/Yarn. + +## Usage + +The most straightforward way to get started is to import Popper from the `unpkg` +CDN, which includes all of its features. You can call the `Popper.createPopper` +constructor to create new popper instances. + +Here is a complete example: + +```html + +Popper example + + + + + + + + +``` + +Visit the [tutorial](https://popper.js.org/docs/v2/tutorial/) for an example of +how to build your own tooltip from scratch using Popper. + +### Module bundlers + +You can import the `createPopper` constructor from the fully-featured file: + +```js +import { createPopper } from '@popperjs/core'; + +const button = document.querySelector('#button'); +const tooltip = document.querySelector('#tooltip'); + +// Pass the button, the tooltip, and some options, and Popper will do the +// magic positioning for you: +createPopper(button, tooltip, { + placement: 'right', +}); +``` + +All the modifiers listed in the docs menu will be enabled and "just work", so +you don't need to think about setting Popper up. The size of Popper including +all of its features is about 5 kB minzipped, but it may grow a bit in the +future. + +#### Popper Lite (tree-shaking) + +If bundle size is important, you'll want to take advantage of tree-shaking. The +library is built in a modular way to allow to import only the parts you really +need. + +```js +import { createPopperLite as createPopper } from '@popperjs/core'; +``` + +The Lite version includes the most necessary modifiers that will compute the +offsets of the popper, compute and add the positioning styles, and add event +listeners. This is close in bundle size to pure CSS tooltip libraries, and +behaves somewhat similarly. + +However, this does not include the features that makes Popper truly useful. + +The two most useful modifiers not included in Lite are `preventOverflow` and +`flip`: + +```js +import { + createPopperLite as createPopper, + preventOverflow, + flip, +} from '@popperjs/core'; + +const button = document.querySelector('#button'); +const tooltip = document.querySelector('#tooltip'); + +createPopper(button, tooltip, { + modifiers: [preventOverflow, flip], +}); +``` + +As you make more poppers, you may be finding yourself needing other modifiers +provided by the library. + +See [tree-shaking](https://popper.js.org/docs/v2/performance/#tree-shaking) for more +information. + +## Distribution targets + +Popper is distributed in 3 different versions, in 3 different file formats. + +The 3 file formats are: + +- `esm` (works with `import` syntax — **recommended**) +- `umd` (works with ` {% endblock %} -- 2.43.0 From 82a3f69fa42598e2322fbc37f06f3ecf0c72e972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20LAPORTE?= Date: Fri, 7 Jun 2024 15:27:55 +0200 Subject: [PATCH 13/37] View (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Création de la page species et detail specie Co-authored-by: Clément Laporte Co-authored-by: clfreville2 Reviewed-on: https://codefirst.iut.uca.fr/git/clement.freville2/herbarium/pulls/8 Co-authored-by: Clément LAPORTE Co-committed-by: Clément LAPORTE --- src/Controller/SpeciesController.php | 29 +++++++++++++++++++ src/DataFixtures/AppFixtures.php | 3 +- templates/species/detail.html.twig | 43 ++++++++++++++++++++++++++++ templates/species/index.html.twig | 29 +++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/Controller/SpeciesController.php create mode 100644 templates/species/detail.html.twig create mode 100644 templates/species/index.html.twig diff --git a/src/Controller/SpeciesController.php b/src/Controller/SpeciesController.php new file mode 100644 index 0000000..e727c11 --- /dev/null +++ b/src/Controller/SpeciesController.php @@ -0,0 +1,29 @@ +findAll(); + return $this->render('species/index.html.twig', [ + 'species' => $species, + ]); + } + + #[Route('/species/{id}', name: 'app_species_detail')] + public function detail(Species $specie): Response + { + return $this->render('species/detail.html.twig', [ + 'specie' => $specie, + ]); + } +} diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index 12d5f40..2fbb47f 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -25,7 +25,8 @@ class AppFixtures extends Fixture ->setPublicationDate($date) ->setLatitude($faker->randomFloat()) ->setLongitude($faker->randomFloat()) - ->setCommentary($faker->text()); + ->setCommentary($faker->text()) + -> setSpecies($species); $manager->persist($species); $manager->persist($post); } diff --git a/templates/species/detail.html.twig b/templates/species/detail.html.twig new file mode 100644 index 0000000..d6a5ebf --- /dev/null +++ b/templates/species/detail.html.twig @@ -0,0 +1,43 @@ +{% extends 'base.html.twig' %} + +{% block title %}Herbarium - Détail de l'espèces{% endblock %} + +{% block body %} + + +
+

{{ specie.vernacularName }}

+

+ 🔬 Nom Scientifique : {{ specie.scientificName }}
+ 📍 Region : {{ specie.region }} +

+
+

Posts :

+ + {% for post in specie.posts %} +
+
+

+ + 📅 {{ post.publicationDate | date }} + +

+
+
+ 📍 géolocalisation :
+ - Longitude : {{ post.longitude }}
+ - Latitude : {{ post.latitude }}
+ - Altitude : {{ post.altitude }}

+ + 💬 Commentaire :
+ - {{ post.getCommentary }} +
+
+ {% endfor %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/species/index.html.twig b/templates/species/index.html.twig new file mode 100644 index 0000000..666ad41 --- /dev/null +++ b/templates/species/index.html.twig @@ -0,0 +1,29 @@ +{% extends 'base.html.twig' %} + +{% block title %}Herbarium - Espèces{% endblock %} + +{% block body %} + + +
+

Liste des espèces

+ +
+ {% for specie in species %} +
+ + 🌿 {{ specie.getVernacularName }} + +
+
+ 🔬 Nom Scientifique : {{ specie.getScientificName }}
+ 📍 Region : {{ specie.getRegion }} +

+ {% endfor %} +
+ +
+{% endblock %} -- 2.43.0 From 7268375b4a6fbdfccc2fa189b1de9663e3a23932 Mon Sep 17 00:00:00 2001 From: Matis MAZINGUE Date: Fri, 7 Jun 2024 15:35:06 +0200 Subject: [PATCH 14/37] pagination post --- src/Controller/PostController.php | 7 ++++--- templates/post/index.html.twig | 10 ++-------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Controller/PostController.php b/src/Controller/PostController.php index a14e165..4a55e24 100644 --- a/src/Controller/PostController.php +++ b/src/Controller/PostController.php @@ -14,11 +14,12 @@ class PostController extends AbstractController #[Route('/', name: 'app_posts')] public function index(PostRepository $repository, Request $request): Response { - $posts = $repository->findPaginatedPosts($request->query->getInt('page',1), 10); + $page = $request->query->getInt('page',1); + $posts = $repository->findPaginatedPosts($page, 10); return $this->render('post/index.html.twig', [ 'posts' => $posts, - 'maxPosts' => $request->query->getInt('page',1) * 10, - 'currentPage' => $request->query->getInt('maxPosts',1) / 10 + 'maxPosts' => $page * 10, + 'currentPage' => $page ]); } } diff --git a/templates/post/index.html.twig b/templates/post/index.html.twig index a2de1bc..d9e48c5 100644 --- a/templates/post/index.html.twig +++ b/templates/post/index.html.twig @@ -16,7 +16,7 @@ {% for post in posts.iterator %}
-
User
+
User
crayon
{{ post.foundDate | date("d/m/Y \\à H \\h") }}

{{ post.latitude }}, {{ post.longitude }}, {{ post.altitude }}m

{{ post.commentary }}

@@ -30,13 +30,7 @@ {% if posts.count > maxPosts %} - Load More + Load More {% endif %} - {% endblock %} -- 2.43.0 From 49d60871c981b39dce1e7726b4691af63ab290bc Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Fri, 7 Jun 2024 17:49:12 +0200 Subject: [PATCH 15/37] Add post and species forms Squashed commit of the following: Author: Hugo PRADIER Author: bastien ollier Author: clfreville2 Reviewed on #7 --- composer.json | 2 +- composer.lock | 29 ++--- phpstan.dist.neon | 1 - src/Controller/PostController.php | 68 ++++++++++- src/Controller/RegistrationController.php | 6 +- src/Controller/SpeciesController.php | 72 ++++++++++-- src/DataFixtures/AppFixtures.php | 14 ++- src/Entity/Post.php | 3 + src/Entity/Species.php | 4 + src/Form/PostType.php | 38 ++++++ src/Form/SpeciesType.php | 27 +++++ src/Repository/UserRepository.php | 10 ++ templates/post/_delete_form.html.twig | 4 + templates/post/_form.html.twig | 4 + templates/post/edit.html.twig | 13 +++ templates/post/index.html.twig | 11 -- templates/post/new.html.twig | 11 ++ templates/post/show.html.twig | 46 ++++++++ templates/post/table.html.twig | 45 +++++++ templates/species/_delete_form.html.twig | 4 + templates/species/_form.html.twig | 4 + templates/species/detail.html.twig | 43 ------- templates/species/edit.html.twig | 13 +++ templates/species/index.html.twig | 11 +- templates/species/new.html.twig | 11 ++ templates/species/show.html.twig | 36 ++++++ templates/species/table.html.twig | 39 +++++++ tests/Controller/PostControllerTest.php | 128 ++++++++++++++++++++ tests/Controller/SpeciesControllerTest.php | 129 +++++++++++++++++++++ 29 files changed, 740 insertions(+), 86 deletions(-) create mode 100644 src/Form/PostType.php create mode 100644 src/Form/SpeciesType.php create mode 100644 templates/post/_delete_form.html.twig create mode 100644 templates/post/_form.html.twig create mode 100644 templates/post/edit.html.twig delete mode 100644 templates/post/index.html.twig create mode 100644 templates/post/new.html.twig create mode 100644 templates/post/show.html.twig create mode 100644 templates/post/table.html.twig create mode 100644 templates/species/_delete_form.html.twig create mode 100644 templates/species/_form.html.twig delete mode 100644 templates/species/detail.html.twig create mode 100644 templates/species/edit.html.twig create mode 100644 templates/species/new.html.twig create mode 100644 templates/species/show.html.twig create mode 100644 templates/species/table.html.twig create mode 100644 tests/Controller/PostControllerTest.php create mode 100644 tests/Controller/SpeciesControllerTest.php diff --git a/composer.json b/composer.json index 0a5a255..c02ecbd 100644 --- a/composer.json +++ b/composer.json @@ -109,7 +109,7 @@ "symfony/browser-kit": "7.0.*", "symfony/css-selector": "7.0.*", "symfony/debug-bundle": "7.0.*", - "symfony/maker-bundle": "^1.0", + "symfony/maker-bundle": "^1.59", "symfony/phpunit-bridge": "^7.0", "symfony/stopwatch": "7.0.*", "symfony/web-profiler-bundle": "7.0.*" diff --git a/composer.lock b/composer.lock index a528b05..12a1306 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "cce9cbfaf4a49449e6431a8515f9d9eb", + "content-hash": "9df70d112adfdfa4fbcde08b504d0bb9", "packages": [ { "name": "api-platform/core", @@ -3757,16 +3757,16 @@ }, { "name": "symfony/form", - "version": "v7.0.7", + "version": "v7.0.8", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "b4df6a399a2b03782a0163807239db342659f54f" + "reference": "1d0128e2f7e80c346ec51fa4d1ce4fec0d435eeb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/b4df6a399a2b03782a0163807239db342659f54f", - "reference": "b4df6a399a2b03782a0163807239db342659f54f", + "url": "https://api.github.com/repos/symfony/form/zipball/1d0128e2f7e80c346ec51fa4d1ce4fec0d435eeb", + "reference": "1d0128e2f7e80c346ec51fa4d1ce4fec0d435eeb", "shasum": "" }, "require": { @@ -3833,7 +3833,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v7.0.7" + "source": "https://github.com/symfony/form/tree/v7.0.8" }, "funding": [ { @@ -3849,7 +3849,7 @@ "type": "tidelift" } ], - "time": "2024-04-28T11:44:19+00:00" + "time": "2024-05-31T14:55:39+00:00" }, { "name": "symfony/framework-bundle", @@ -7137,16 +7137,16 @@ }, { "name": "symfony/validator", - "version": "v7.0.7", + "version": "v7.0.8", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "ab4e75b9d23ba70e78480aecbe4d8da15adf10eb" + "reference": "23af65dff1f4dfee9aab3a0123a243e40fa3d9cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/ab4e75b9d23ba70e78480aecbe4d8da15adf10eb", - "reference": "ab4e75b9d23ba70e78480aecbe4d8da15adf10eb", + "url": "https://api.github.com/repos/symfony/validator/zipball/23af65dff1f4dfee9aab3a0123a243e40fa3d9cf", + "reference": "23af65dff1f4dfee9aab3a0123a243e40fa3d9cf", "shasum": "" }, "require": { @@ -7191,7 +7191,8 @@ "Symfony\\Component\\Validator\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/Resources/bin/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7211,7 +7212,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.0.7" + "source": "https://github.com/symfony/validator/tree/v7.0.8" }, "funding": [ { @@ -7227,7 +7228,7 @@ "type": "tidelift" } ], - "time": "2024-04-28T11:44:19+00:00" + "time": "2024-06-02T15:49:03+00:00" }, { "name": "symfony/var-dumper", diff --git a/phpstan.dist.neon b/phpstan.dist.neon index afb7a06..b7cb307 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -7,4 +7,3 @@ parameters: level: 8 paths: - src - - tests diff --git a/src/Controller/PostController.php b/src/Controller/PostController.php index 0df3016..8bbe909 100644 --- a/src/Controller/PostController.php +++ b/src/Controller/PostController.php @@ -2,19 +2,85 @@ namespace App\Controller; +use App\Entity\Post; +use App\Form\PostType; use App\Repository\PostRepository; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; class PostController extends AbstractController { #[Route('/', name: 'app_posts')] + #[Route('/post', name: 'app_post_index', methods: ['GET'])] public function index(PostRepository $repository): Response { $posts = $repository->findAll(); - return $this->render('post/index.html.twig', [ + return $this->render('post/table.html.twig', [ 'posts' => $posts, ]); } + + #[Route('/post/new', name: 'app_post_new', methods: ['GET', 'POST'])] + #[IsGranted('ROLE_USER')] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $post = new Post(); + $form = $this->createForm(PostType::class, $post); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($post); + $entityManager->flush(); + + return $this->redirectToRoute('app_posts', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('post/new.html.twig', [ + 'post' => $post, + 'form' => $form, + ]); + } + + #[Route('/post/{id}', name: 'app_post_show', methods: ['GET'])] + public function show(Post $post): Response + { + return $this->render('post/show.html.twig', [ + 'post' => $post, + ]); + } + + #[Route('/post/{id}/edit', name: 'app_post_edit', methods: ['GET', 'POST'])] + #[IsGranted('ROLE_USER')] + public function edit(Request $request, Post $post, EntityManagerInterface $entityManager): Response + { + $form = $this->createForm(PostType::class, $post); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + return $this->redirectToRoute('app_posts', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('post/edit.html.twig', [ + 'post' => $post, + 'form' => $form, + ]); + } + + #[Route('/post/{id}', name: 'app_post_delete', methods: ['POST'])] + #[IsGranted('ROLE_USER')] + public function delete(Request $request, Post $post, EntityManagerInterface $entityManager): Response + { + if ($this->isCsrfTokenValid('delete'.$post->getId(), (string) $request->getPayload()->get('_token'))) { + $entityManager->remove($post); + $entityManager->flush(); + } + + return $this->redirectToRoute('app_posts', [], Response::HTTP_SEE_OTHER); + } } diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index 6bfc237..43043a9 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -34,7 +34,11 @@ class RegistrationController extends AbstractController // do anything else you need here, like send an email - return $this->redirectToRoute('_profiler_home'); + return $this->redirectToRoute('app_login'); + } + + if ($this->getUser()) { + return $this->redirectToRoute('app_posts'); } return $this->render('registration/register.html.twig', [ diff --git a/src/Controller/SpeciesController.php b/src/Controller/SpeciesController.php index e727c11..a495a0f 100644 --- a/src/Controller/SpeciesController.php +++ b/src/Controller/SpeciesController.php @@ -3,27 +3,83 @@ namespace App\Controller; use App\Entity\Species; +use App\Form\SpeciesType; use App\Repository\SpeciesRepository; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; +#[Route('/species')] class SpeciesController extends AbstractController { - #[Route('/species', name: 'app_species')] - public function index(SpeciesRepository $repository): Response + #[Route('/', name: 'app_species_index', methods: ['GET'])] + public function table(SpeciesRepository $speciesRepository): Response { - $species = $repository->findAll(); - return $this->render('species/index.html.twig', [ + return $this->render('species/table.html.twig', [ + 'species' => $speciesRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_species_new', methods: ['GET', 'POST'])] + #[IsGranted('ROLE_USER')] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $species = new Species(); + $form = $this->createForm(SpeciesType::class, $species); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($species); + $entityManager->flush(); + + return $this->redirectToRoute('app_species_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('species/new.html.twig', [ + 'species' => $species, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_species_show', methods: ['GET'])] + public function show(Species $species): Response + { + return $this->render('species/show.html.twig', [ 'species' => $species, ]); } - #[Route('/species/{id}', name: 'app_species_detail')] - public function detail(Species $specie): Response + #[Route('/{id}/edit', name: 'app_species_edit', methods: ['GET', 'POST'])] + #[IsGranted('ROLE_USER')] + public function edit(Request $request, Species $species, EntityManagerInterface $entityManager): Response { - return $this->render('species/detail.html.twig', [ - 'specie' => $specie, + $form = $this->createForm(SpeciesType::class, $species); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + return $this->redirectToRoute('app_species_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('species/edit.html.twig', [ + 'species' => $species, + 'form' => $form, ]); } + + #[Route('/{id}', name: 'app_species_delete', methods: ['POST'])] + #[IsGranted('ROLE_USER')] + public function delete(Request $request, Species $species, EntityManagerInterface $entityManager): Response + { + if ($this->isCsrfTokenValid('delete'.$species->getId(), (string) $request->getPayload()->get('_token'))) { + $entityManager->remove($species); + $entityManager->flush(); + } + + return $this->redirectToRoute('app_species_index', [], Response::HTTP_SEE_OTHER); + } } diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index 2fbb47f..6bff9e2 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -4,17 +4,29 @@ namespace App\DataFixtures; use App\Entity\Post; use App\Entity\Species; +use App\Entity\User; use DateTimeImmutable; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; class AppFixtures extends Fixture { + public function __construct( + private readonly UserPasswordHasherInterface $passwordHasher + ) + { + } + public function load(ObjectManager $manager): void { + $user = (new User())->setEmail('test@test.fr'); + $user->setPassword($this->passwordHasher->hashPassword($user, 'password')); + $manager->persist($user); + $faker = \Faker\Factory::create(); for ($i = 0; $i < 20; ++$i) { - $name = $faker->text(); + $name = $faker->name(); $species = (new Species()) ->setScientificName($name) ->setVernacularName($name) diff --git a/src/Entity/Post.php b/src/Entity/Post.php index 0b54550..22e346a 100644 --- a/src/Entity/Post.php +++ b/src/Entity/Post.php @@ -11,6 +11,7 @@ use App\Repository\PostRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: PostRepository::class)] #[ORM\HasLifecycleCallbacks] @@ -30,6 +31,7 @@ class Post #[ORM\Column] #[Groups(['post:collection:read'])] + #[Assert\NotBlank] private ?\DateTimeImmutable $foundDate = null; #[ORM\Column] @@ -51,6 +53,7 @@ class Post #[ORM\Column(type: Types::TEXT)] #[Groups(['post:read'])] + #[Assert\NotBlank] private ?string $commentary = null; #[ORM\ManyToOne(inversedBy: 'posts')] diff --git a/src/Entity/Species.php b/src/Entity/Species.php index e2e1b78..285fe6c 100644 --- a/src/Entity/Species.php +++ b/src/Entity/Species.php @@ -9,6 +9,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: SpeciesRepository::class)] #[ApiResource( @@ -26,14 +27,17 @@ class Species #[ORM\Column(length: 255)] #[Groups(['species:collection:read'])] + #[Assert\NotBlank] private ?string $scientific_name = null; #[ORM\Column(length: 255)] #[Groups(['species:collection:read'])] + #[Assert\NotBlank] private ?string $vernacular_name = null; #[ORM\Column(length: 255)] #[Groups(['species:collection:read'])] + #[Assert\NotBlank] private ?string $region = null; /** diff --git a/src/Form/PostType.php b/src/Form/PostType.php new file mode 100644 index 0000000..bea7e6f --- /dev/null +++ b/src/Form/PostType.php @@ -0,0 +1,38 @@ +add('foundDate', null, [ + 'widget' => 'single_text', + ]) + ->add('latitude') + ->add('longitude') + ->add('altitude') + ->add('commentary') + ->add('species', EntityType::class, [ + 'class' => Species::class, + 'choice_label' => 'scientific_name', + ]) + + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Post::class, + ]); + } +} diff --git a/src/Form/SpeciesType.php b/src/Form/SpeciesType.php new file mode 100644 index 0000000..ae3b919 --- /dev/null +++ b/src/Form/SpeciesType.php @@ -0,0 +1,27 @@ +add('scientific_name') + ->add('vernacular_name') + ->add('region') + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Species::class, + ]); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 4f2804e..e861224 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -33,6 +33,16 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader $this->getEntityManager()->flush(); } + public function findOneByEmail(string $email): ?User + { + return $this->createQueryBuilder('u') + ->andWhere('u.email = :email') + ->setParameter('email', $email) + ->getQuery() + ->getOneOrNullResult() + ; + } + // /** // * @return User[] Returns an array of User objects // */ diff --git a/templates/post/_delete_form.html.twig b/templates/post/_delete_form.html.twig new file mode 100644 index 0000000..1223c9b --- /dev/null +++ b/templates/post/_delete_form.html.twig @@ -0,0 +1,4 @@ +
+ + +
diff --git a/templates/post/_form.html.twig b/templates/post/_form.html.twig new file mode 100644 index 0000000..bf20b98 --- /dev/null +++ b/templates/post/_form.html.twig @@ -0,0 +1,4 @@ +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} diff --git a/templates/post/edit.html.twig b/templates/post/edit.html.twig new file mode 100644 index 0000000..16b6b5f --- /dev/null +++ b/templates/post/edit.html.twig @@ -0,0 +1,13 @@ +{% extends 'base.html.twig' %} + +{% block title %}Edit Post{% endblock %} + +{% block body %} +

Edit Post

+ + {{ include('post/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('post/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/post/index.html.twig b/templates/post/index.html.twig deleted file mode 100644 index 94f3756..0000000 --- a/templates/post/index.html.twig +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}Posts!{% endblock %} - -{% block body %} -{% for post in posts %} -
- #{{ post.id }} trouvé le {{ post.foundDate | date("d/m/Y \\à H \\h") }} -
-{% endfor %} -{% endblock %} diff --git a/templates/post/new.html.twig b/templates/post/new.html.twig new file mode 100644 index 0000000..b8bc236 --- /dev/null +++ b/templates/post/new.html.twig @@ -0,0 +1,11 @@ +{% extends 'base.html.twig' %} + +{% block title %}New Post{% endblock %} + +{% block body %} +

Create new Post

+ + {{ include('post/_form.html.twig') }} + + back to list +{% endblock %} diff --git a/templates/post/show.html.twig b/templates/post/show.html.twig new file mode 100644 index 0000000..a332dba --- /dev/null +++ b/templates/post/show.html.twig @@ -0,0 +1,46 @@ +{% extends 'base.html.twig' %} + +{% block title %}Post{% endblock %} + +{% block body %} +

Post

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Id{{ post.id }}
FoundDate{{ post.foundDate ? post.foundDate|date('Y-m-d H:i:s') : '' }}
PublicationDate{{ post.publicationDate ? post.publicationDate|date('Y-m-d H:i:s') : '' }}
Latitude{{ post.latitude }}
Longitude{{ post.longitude }}
Altitude{{ post.altitude }}
Commentary{{ post.commentary }}
+ + back to list + + edit + + {{ include('post/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/post/table.html.twig b/templates/post/table.html.twig new file mode 100644 index 0000000..3a5db93 --- /dev/null +++ b/templates/post/table.html.twig @@ -0,0 +1,45 @@ +{% extends 'base.html.twig' %} + +{% block title %}Post index{% endblock %} + +{% block body %} +

Post index

+ + + + + + + + + + + + + + + + {% for post in posts %} + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
IdFoundDatePublicationDateLatitudeLongitudeAltitudeCommentaryactions
{{ post.id }}{{ post.foundDate ? post.foundDate|date('Y-m-d H:i:s') : '' }}{{ post.publicationDate ? post.publicationDate|date('Y-m-d H:i:s') : '' }}{{ post.latitude }}{{ post.longitude }}{{ post.altitude }}{{ post.commentary }} + show + edit +
no records found
+ + Create new +{% endblock %} diff --git a/templates/species/_delete_form.html.twig b/templates/species/_delete_form.html.twig new file mode 100644 index 0000000..9e7c05a --- /dev/null +++ b/templates/species/_delete_form.html.twig @@ -0,0 +1,4 @@ +
+ + +
diff --git a/templates/species/_form.html.twig b/templates/species/_form.html.twig new file mode 100644 index 0000000..bf20b98 --- /dev/null +++ b/templates/species/_form.html.twig @@ -0,0 +1,4 @@ +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} diff --git a/templates/species/detail.html.twig b/templates/species/detail.html.twig deleted file mode 100644 index d6a5ebf..0000000 --- a/templates/species/detail.html.twig +++ /dev/null @@ -1,43 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}Herbarium - Détail de l'espèces{% endblock %} - -{% block body %} - - -
-

{{ specie.vernacularName }}

-

- 🔬 Nom Scientifique : {{ specie.scientificName }}
- 📍 Region : {{ specie.region }} -

-
-

Posts :

- - {% for post in specie.posts %} -
-
-

- - 📅 {{ post.publicationDate | date }} - -

-
-
- 📍 géolocalisation :
- - Longitude : {{ post.longitude }}
- - Latitude : {{ post.latitude }}
- - Altitude : {{ post.altitude }}

- - 💬 Commentaire :
- - {{ post.getCommentary }} -
-
- {% endfor %} -
- -
-{% endblock %} \ No newline at end of file diff --git a/templates/species/edit.html.twig b/templates/species/edit.html.twig new file mode 100644 index 0000000..8e4401b --- /dev/null +++ b/templates/species/edit.html.twig @@ -0,0 +1,13 @@ +{% extends 'base.html.twig' %} + +{% block title %}Edit Species{% endblock %} + +{% block body %} +

Edit Species

+ + {{ include('species/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('species/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/species/index.html.twig b/templates/species/index.html.twig index 666ad41..e855f6c 100644 --- a/templates/species/index.html.twig +++ b/templates/species/index.html.twig @@ -1,6 +1,6 @@ {% extends 'base.html.twig' %} -{% block title %}Herbarium - Espèces{% endblock %} +{% block title %}Species{% endblock %} {% block body %} - - - - - - -``` - -Visit the [tutorial](https://popper.js.org/docs/v2/tutorial/) for an example of -how to build your own tooltip from scratch using Popper. - -### Module bundlers - -You can import the `createPopper` constructor from the fully-featured file: - -```js -import { createPopper } from '@popperjs/core'; - -const button = document.querySelector('#button'); -const tooltip = document.querySelector('#tooltip'); - -// Pass the button, the tooltip, and some options, and Popper will do the -// magic positioning for you: -createPopper(button, tooltip, { - placement: 'right', -}); -``` - -All the modifiers listed in the docs menu will be enabled and "just work", so -you don't need to think about setting Popper up. The size of Popper including -all of its features is about 5 kB minzipped, but it may grow a bit in the -future. - -#### Popper Lite (tree-shaking) - -If bundle size is important, you'll want to take advantage of tree-shaking. The -library is built in a modular way to allow to import only the parts you really -need. - -```js -import { createPopperLite as createPopper } from '@popperjs/core'; -``` - -The Lite version includes the most necessary modifiers that will compute the -offsets of the popper, compute and add the positioning styles, and add event -listeners. This is close in bundle size to pure CSS tooltip libraries, and -behaves somewhat similarly. - -However, this does not include the features that makes Popper truly useful. - -The two most useful modifiers not included in Lite are `preventOverflow` and -`flip`: - -```js -import { - createPopperLite as createPopper, - preventOverflow, - flip, -} from '@popperjs/core'; - -const button = document.querySelector('#button'); -const tooltip = document.querySelector('#tooltip'); - -createPopper(button, tooltip, { - modifiers: [preventOverflow, flip], -}); -``` - -As you make more poppers, you may be finding yourself needing other modifiers -provided by the library. - -See [tree-shaking](https://popper.js.org/docs/v2/performance/#tree-shaking) for more -information. - -## Distribution targets - -Popper is distributed in 3 different versions, in 3 different file formats. - -The 3 file formats are: - -- `esm` (works with `import` syntax — **recommended**) -- `umd` (works with ` {{ form_end(form) }} -- 2.43.0 From b0507a44ea4f6e26726582a9b8a893d37ab4bd56 Mon Sep 17 00:00:00 2001 From: Matis MAZINGUE Date: Fri, 14 Jun 2024 09:41:05 +0200 Subject: [PATCH 33/37] like (#19) Co-authored-by: Matis MAZINGUE Co-authored-by: clfreville2 Reviewed-on: https://codefirst.iut.uca.fr/git/clement.freville2/herbarium/pulls/19 Co-authored-by: Matis MAZINGUE Co-committed-by: Matis MAZINGUE --- public/css/app.css | 12 +++++++++++ public/js/like_toggle.js | 27 ++++++++++++++++++++++++ src/Controller/PostController.php | 26 +++++++++++++++++++++++ src/Entity/Post.php | 31 ++++++++++++++++++++++++++++ src/Entity/User.php | 34 +++++++++++++++++++++++++++++++ templates/base.html.twig | 1 + templates/post/index.html.twig | 17 +++++++++++++++- 7 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 public/css/app.css create mode 100644 public/js/like_toggle.js diff --git a/public/css/app.css b/public/css/app.css new file mode 100644 index 0000000..5da752c --- /dev/null +++ b/public/css/app.css @@ -0,0 +1,12 @@ +.no-style { + background: none; + border: none; + padding: 0; + font: inherit; + color: inherit; + cursor: pointer; +} + +.no-style:focus { + outline: none; +} diff --git a/public/js/like_toggle.js b/public/js/like_toggle.js new file mode 100644 index 0000000..1f89096 --- /dev/null +++ b/public/js/like_toggle.js @@ -0,0 +1,27 @@ +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.like-toggle').forEach(button => { + button.addEventListener('click', function (event) { + event.preventDefault(); + + let isLiked = this.classList.contains('liked'); + let url = isLiked ? this.dataset.unlikeUrl : this.dataset.likeUrl; + + fetch(url, { method: 'POST' }) + .then(response => response.json()) + .then(data => { + if (data.success) { + let likesCountElement = this.parentElement.querySelector('.likes-count'); + likesCountElement.textContent = data.likesCount; + this.classList.toggle('liked'); + this.classList.toggle('not-liked'); + this.innerHTML = isLiked ? '♡' : '❤️'; + } else { + console.error('Erreur lors du traitement du like/unlike.'); + } + }) + .catch(error => { + console.error('Erreur lors de la requête fetch:', error); + }); + }); + }); +}); diff --git a/src/Controller/PostController.php b/src/Controller/PostController.php index 7f4e77f..5be66b8 100644 --- a/src/Controller/PostController.php +++ b/src/Controller/PostController.php @@ -16,6 +16,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\CurrentUser; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\UX\Turbo\TurboBundle; +use Symfony\Component\HttpFoundation\JsonResponse; class PostController extends AbstractController { @@ -170,4 +171,29 @@ class PostController extends AbstractController } return $this->redirectToRoute('app_post_show', ['id' => $comment->getRelatedPost()->getId()]); } + + #[Route('/post/{id}/like', name: 'app_posts_like', methods: ['POST'])] + #[IsGranted('ROLE_USER')] + public function addLike(#[CurrentUser] User $user, Post $post, EntityManagerInterface $entityManager): JsonResponse + { + $user->addLikedPost($post); + $entityManager->flush(); + + $likesCount = $post->getLikes()->count(); + + return new JsonResponse(['success' => true, 'likesCount' => $likesCount]); + } + + #[Route('/post/{id}/unlike', name: 'app_posts_unlike', methods: ['POST'])] + #[IsGranted('ROLE_USER')] + public function deleteLike(#[CurrentUser] User $user, Post $post, EntityManagerInterface $entityManager): JsonResponse + { + $user->removeLikedPost($post); + $entityManager->flush(); + + $likesCount = $post->getLikes()->count(); + + return new JsonResponse(['success' => true, 'likesCount' => $likesCount]); + } + } diff --git a/src/Entity/Post.php b/src/Entity/Post.php index fa0217a..b8b0046 100644 --- a/src/Entity/Post.php +++ b/src/Entity/Post.php @@ -84,9 +84,16 @@ class Post #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'related_post', fetch: 'EXTRA_LAZY')] private Collection $comments; + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'liked_post')] + private Collection $likes; + public function __construct() { $this->comments = new ArrayCollection(); + $this->likes = new ArrayCollection(); } public function getId(): ?int @@ -256,4 +263,28 @@ class Post return $this; } + + /** + * @return Collection + */ + public function getLikes(): Collection + { + return $this->likes; + } + + public function addLike(User $user): static + { + if (!$this->likes->contains($user)) { + $this->likes->add($user); + } + + return $this; + } + + public function removeLike(User $user): static + { + $this->likes->removeElement($user); + + return $this; + } } diff --git a/src/Entity/User.php b/src/Entity/User.php index 8519d9d..81a6b3a 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -68,9 +68,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'author')] private Collection $comments; + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Post::class, mappedBy: 'likes')] + private Collection $liked_post; + public function __construct() { $this->comments = new ArrayCollection(); + $this->liked_post = new ArrayCollection(); } public function getId(): ?int @@ -189,4 +196,31 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + /** + * @return Collection + */ + public function getLikedPost(): Collection + { + return $this->liked_post; + } + + public function addLikedPost(Post $likedPost): static + { + if (!$this->liked_post->contains($likedPost)) { + $this->liked_post->add($likedPost); + $likedPost->addLike($this); + } + + return $this; + } + + public function removeLikedPost(Post $likedPost): static + { + if ($this->liked_post->removeElement($likedPost)) { + $likedPost->removeLike($this); + } + + return $this; + } } diff --git a/templates/base.html.twig b/templates/base.html.twig index 0ced7a9..903f586 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -6,6 +6,7 @@ {% block stylesheets %} + {% endblock %} {% block javascripts %} diff --git a/templates/post/index.html.twig b/templates/post/index.html.twig index d14adbc..20fc847 100644 --- a/templates/post/index.html.twig +++ b/templates/post/index.html.twig @@ -12,10 +12,25 @@

{{ post.commentary }}

{% endfor %} {% include '_pagination.html.twig' %} {% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} -- 2.43.0 From ba82630d8a42760c437c3d26024bcdc8479a0ea3 Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Fri, 14 Jun 2024 09:42:09 +0200 Subject: [PATCH 34/37] Disable service implementation --- config/services.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/services.yaml b/config/services.yaml index f07d469..8919b4f 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -30,7 +30,3 @@ services: # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones - -when@prod: - services: - App\Service\ImageSafetyServiceInterface: '@App\Service\SightEngineImageSafetyService' -- 2.43.0 From 4f19bac7078530d5535c0b1b66d714c3eba78be0 Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Fri, 14 Jun 2024 12:18:58 +0200 Subject: [PATCH 35/37] Fix upload base URL behind a subdirectory --- templates/post/show.html.twig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/post/show.html.twig b/templates/post/show.html.twig index e990e12..c79e591 100644 --- a/templates/post/show.html.twig +++ b/templates/post/show.html.twig @@ -34,7 +34,8 @@ {% if post.image %} Image - + {# Vich doesn't prefix the path, as asset() would. #} + {% endif %} -- 2.43.0 From 71ddec420c7226323fc0879a4d0ffe5212eb4eff Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Fri, 14 Jun 2024 12:51:03 +0200 Subject: [PATCH 36/37] Tweak style --- public/css/app.css | 12 ++++++++++++ templates/_pagination.html.twig | 2 +- templates/base.html.twig | 6 +++--- templates/post/index.html.twig | 12 +++++++++++- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/public/css/app.css b/public/css/app.css index 5da752c..c62a3cc 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -10,3 +10,15 @@ .no-style:focus { outline: none; } + +.grid-4 { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} + +@media (max-width: 992px) { + .grid-4 { + grid-template-columns: 1fr; + } +} diff --git a/templates/_pagination.html.twig b/templates/_pagination.html.twig index 9c9fdae..c3f6baa 100644 --- a/templates/_pagination.html.twig +++ b/templates/_pagination.html.twig @@ -1,6 +1,6 @@ {% set route = app.request.attributes.get('_route') %}