├── migrations ├── .gitignore ├── Version20210510171213.php ├── Version20210523174745.php ├── Version20210523175734.php ├── Version20210510163646.php └── Version20210510155228.php ├── src ├── Entity │ ├── .gitignore │ ├── Distribution.php │ └── Dataset.php ├── Controller │ ├── .gitignore │ ├── SupplierController.php │ └── ApiController.php ├── Repository │ ├── .gitignore │ ├── DistributionRepository.php │ └── DatasetRepository.php ├── Api │ ├── Entity │ │ ├── Request │ │ │ ├── ApiRequestInterface.php │ │ │ ├── ApiDatasetRequest.php │ │ │ ├── ApiGetDatasetRequest.php │ │ │ └── ApiSearchRequest.php │ │ └── Response │ │ │ ├── ApiResponseInterface.php │ │ │ ├── ApiSuccessResponse.php │ │ │ ├── ApiGetDatasetResponse.php │ │ │ └── ApiDatasetsResponse.php │ └── Builder │ │ ├── ApiRequestBuilder.php │ │ └── ApiResponseBuilder.php ├── SupplierFacade │ ├── Exception │ │ ├── AbstractCoreSupplierException.php │ │ └── UnnecessaryDistributionCoreSupplierException.php │ └── DataEu │ │ ├── Bridge │ │ ├── GetDataset │ │ │ └── DataEuGetDatasetResponseBridge.php │ │ ├── Search │ │ │ └── DataEuSearchResponseBridge.php │ │ └── Common │ │ │ ├── DataEuDistributionResponseBridge.php │ │ │ └── DataEuDatasetResponseBridge.php │ │ ├── RequestBuilder │ │ └── DataEuSearchRequestBuilder.php │ │ ├── DataEuSupplierFacade.php │ │ └── MethodFacade │ │ ├── DataEuSearchMethodFacade.php │ │ └── DataEuGetDatasetMethodFacade.php ├── Supplier │ └── DataEu │ │ ├── DataEuConst.php │ │ ├── Common │ │ ├── Collection │ │ │ ├── DataEuDatasetCollection.php │ │ │ └── DataEuDistributionCollection.php │ │ ├── DataEuFormat.php │ │ ├── DataEuCountry.php │ │ ├── DataEuDistribution.php │ │ ├── DataEuDescription.php │ │ └── DataEuDataset.php │ │ ├── GetDataset │ │ └── DataEuGetDatasetResponse.php │ │ └── Search │ │ ├── DataEuSearchResponse.php │ │ └── DataEuResultResponse.php ├── Utils │ └── StringCollection.php ├── Base │ ├── CollectionInterface.php │ └── AbstractCollection.php └── Kernel.php ├── translations └── .gitignore ├── public ├── assets │ ├── js │ │ ├── main.js │ │ ├── observe.js │ │ └── import.js │ └── lib │ │ └── bootstrap │ │ └── css │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.rtl.min.css │ │ ├── bootstrap-reboot.rtl.css │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.min.css.map │ │ └── bootstrap-reboot.rtl.min.css.map ├── index.php └── .htaccess ├── Procfile ├── config ├── packages │ ├── test │ │ ├── twig.yaml │ │ ├── validator.yaml │ │ ├── framework.yaml │ │ ├── web_profiler.yaml │ │ ├── doctrine.yaml │ │ └── monolog.yaml │ ├── twig.yaml │ ├── mailer.yaml │ ├── prod │ │ ├── routing.yaml │ │ ├── jms_serializer.yaml │ │ ├── deprecations.yaml │ │ ├── monolog.yaml │ │ └── doctrine.yaml │ ├── sensio_framework_extra.yaml │ ├── dev │ │ ├── web_profiler.yaml │ │ ├── jms_serializer.yaml │ │ ├── debug.yaml │ │ └── monolog.yaml │ ├── translation.yaml │ ├── routing.yaml │ ├── validator.yaml │ ├── doctrine_migrations.yaml │ ├── doctrine.yaml │ ├── notifier.yaml │ ├── framework.yaml │ ├── jms_serializer.yaml │ ├── cache.yaml │ └── security.yaml ├── routes.yaml ├── routes │ ├── dev │ │ ├── framework.yaml │ │ └── web_profiler.yaml │ └── annotations.yaml ├── preload.php ├── bundles.php └── services.yaml ├── .env.test ├── tests └── bootstrap.php ├── .gitignore ├── bin ├── phpunit └── console ├── templates ├── page │ ├── observe.html.twig │ └── import.html.twig └── base.html.twig ├── phpunit.xml.dist ├── .env.dist ├── composer.json └── symfony.lock /migrations/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Entity/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /translations/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/js/main.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Controller/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Repository/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: vendor/bin/heroku-php-apache2 public/ -------------------------------------------------------------------------------- /config/packages/test/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | strict_variables: true 3 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | -------------------------------------------------------------------------------- /config/packages/mailer.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | mailer: 3 | dsn: '%env(MAILER_DSN)%' 4 | -------------------------------------------------------------------------------- /config/packages/prod/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: null 4 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | #index: 2 | # path: / 3 | # controller: App\Controller\DefaultController::index 4 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/test/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | not_compromised_password: false 4 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/routes/dev/framework.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /src/Api/Entity/Request/ApiRequestInterface.php: -------------------------------------------------------------------------------- 1 | dataset = $dataset; 16 | } 17 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /.idea/ 3 | 4 | ###> symfony/framework-bundle ### 5 | /.env.local 6 | /.env.local.php 7 | /.env.*.local 8 | /config/secrets/prod/prod.decrypt.private.php 9 | /public/bundles/ 10 | /var/ 11 | /vendor/ 12 | ###< symfony/framework-bundle ### 13 | 14 | ###> symfony/phpunit-bridge ### 15 | .phpunit 16 | .phpunit.result.cache 17 | /phpunit.xml 18 | .env 19 | ###< symfony/phpunit-bridge ### 20 | -------------------------------------------------------------------------------- /config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | channels: ["!event"] 9 | nested: 10 | type: stream 11 | path: "%kernel.logs_dir%/%kernel.environment%.log" 12 | level: debug 13 | -------------------------------------------------------------------------------- /src/Supplier/DataEu/Common/Collection/DataEuDatasetCollection.php: -------------------------------------------------------------------------------- 1 | dataset; 18 | } 19 | } -------------------------------------------------------------------------------- /bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | title; 20 | } 21 | 22 | public function getId(): string 23 | { 24 | return $this->id; 25 | } 26 | } -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | # IMPORTANT: DATABASE_URL *must* define the server version 4 | url: '%env(resolve:DATABASE_URL)%' 5 | orm: 6 | auto_generate_proxy_classes: true 7 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 8 | auto_mapping: true 9 | mappings: 10 | App: 11 | is_bundle: false 12 | type: annotation 13 | dir: '%kernel.project_dir%/src/Entity' 14 | prefix: 'App\Entity' 15 | alias: App 16 | -------------------------------------------------------------------------------- /config/packages/notifier.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | notifier: 3 | #chatter_transports: 4 | # slack: '%env(SLACK_DSN)%' 5 | # telegram: '%env(TELEGRAM_DSN)%' 6 | #texter_transports: 7 | # twilio: '%env(TWILIO_DSN)%' 8 | # nexmo: '%env(NEXMO_DSN)%' 9 | channel_policy: 10 | # use chat/slack, chat/telegram, sms/twilio or sms/nexmo 11 | urgent: ['email'] 12 | high: ['email'] 13 | medium: ['email'] 14 | low: ['email'] 15 | admin_recipients: 16 | - { email: admin@example.com } 17 | -------------------------------------------------------------------------------- /src/Api/Entity/Request/ApiGetDatasetRequest.php: -------------------------------------------------------------------------------- 1 | externalId; 20 | } 21 | 22 | public function usePersist(): bool 23 | { 24 | return $this->usePersist; 25 | } 26 | } -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | #http_method_override: true 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: null 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | 14 | #esi: true 15 | #fragments: true 16 | php_errors: 17 | log: true 18 | -------------------------------------------------------------------------------- /templates/page/observe.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | 5 | {% endblock %} 6 | 7 | {% block body %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
#КраїнаОпис(EN)Опис(DE)Опис(FR)Доступні формати
21 | {% endblock %} -------------------------------------------------------------------------------- /config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks 9 | nested: 10 | type: stream 11 | path: php://stderr 12 | level: debug 13 | formatter: monolog.formatter.json 14 | console: 15 | type: console 16 | process_psr_3_messages: false 17 | channels: ["!event", "!doctrine"] 18 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | 12 | if ($_SERVER['APP_DEBUG']) { 13 | umask(0000); 14 | 15 | Debug::enable(); 16 | } 17 | 18 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 19 | $request = Request::createFromGlobals(); 20 | $response = $kernel->handle($request); 21 | $response->send(); 22 | $kernel->terminate($request, $response); 23 | -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | auto_generate_proxy_classes: false 4 | metadata_cache_driver: 5 | type: pool 6 | pool: doctrine.system_cache_pool 7 | query_cache_driver: 8 | type: pool 9 | pool: doctrine.system_cache_pool 10 | result_cache_driver: 11 | type: pool 12 | pool: doctrine.result_cache_pool 13 | 14 | framework: 15 | cache: 16 | pools: 17 | doctrine.result_cache_pool: 18 | adapter: cache.app 19 | doctrine.system_cache_pool: 20 | adapter: cache.system 21 | -------------------------------------------------------------------------------- /src/Api/Builder/ApiRequestBuilder.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 17 | } 18 | 19 | public function build(string $requestClass, string $data): ApiRequestInterface 20 | { 21 | /** @var ApiRequestInterface $request */ 22 | return $this->serializer->deserialize($data, $requestClass, 'json'); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Supplier/DataEu/GetDataset/DataEuGetDatasetResponse.php: -------------------------------------------------------------------------------- 1 | success; 21 | } 22 | 23 | public function getResult(): DataEuDataset 24 | { 25 | return $this->result; 26 | } 27 | } -------------------------------------------------------------------------------- /config/packages/jms_serializer.yaml: -------------------------------------------------------------------------------- 1 | jms_serializer: 2 | visitors: 3 | xml_serialization: 4 | format_output: '%kernel.debug%' 5 | json_serialization: 6 | options: [JSON_UNESCAPED_UNICODE] 7 | json_deserialization: 8 | options: [JSON_UNESCAPED_UNICODE] 9 | # metadata: 10 | # auto_detection: false 11 | # directories: 12 | # any-name: 13 | # namespace_prefix: "My\\FooBundle" 14 | # path: "@MyFooBundle/Resources/config/serializer" 15 | # another-name: 16 | # namespace_prefix: "My\\BarBundle" 17 | # path: "@MyBarBundle/Resources/config/serializer" 18 | -------------------------------------------------------------------------------- /config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: debug 7 | channels: ["!event"] 8 | # uncomment to get logging in your browser 9 | # you may have to allow bigger header sizes in your Web server configuration 10 | #firephp: 11 | # type: firephp 12 | # level: info 13 | #chromephp: 14 | # type: chromephp 15 | # level: info 16 | console: 17 | type: console 18 | process_psr_3_messages: false 19 | channels: ["!event", "!doctrine", "!console"] 20 | -------------------------------------------------------------------------------- /src/Supplier/DataEu/Search/DataEuSearchResponse.php: -------------------------------------------------------------------------------- 1 | success; 21 | } 22 | 23 | public function getResult(): DataEuResultResponse 24 | { 25 | return $this->result; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Api/Entity/Request/ApiSearchRequest.php: -------------------------------------------------------------------------------- 1 | query; 23 | } 24 | 25 | public function getPage(): int 26 | { 27 | return $this->page; 28 | } 29 | 30 | public function getPerPage(): int 31 | { 32 | return $this->perPage; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Supplier/DataEu/Common/DataEuCountry.php: -------------------------------------------------------------------------------- 1 | title; 20 | } 21 | 22 | public function setTitle(string $title): void 23 | { 24 | $this->title = $title; 25 | } 26 | 27 | public function getId(): string 28 | { 29 | return $this->id; 30 | } 31 | 32 | public function setId(string $id): void 33 | { 34 | $this->id = $id; 35 | } 36 | } -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /src/Api/Entity/Response/ApiDatasetsResponse.php: -------------------------------------------------------------------------------- 1 | ") */ 13 | private Collection $datasetCollection; 14 | 15 | private ?int $totalDatasetsAmount = null; 16 | 17 | public function setDatasetCollection(Collection $datasetCollection): void 18 | { 19 | $this->datasetCollection = $datasetCollection; 20 | } 21 | 22 | public function setTotalDatasetsAmount(int $totalDatasetsAmount): void 23 | { 24 | $this->totalDatasetsAmount = $totalDatasetsAmount; 25 | } 26 | } -------------------------------------------------------------------------------- /src/SupplierFacade/DataEu/Bridge/GetDataset/DataEuGetDatasetResponseBridge.php: -------------------------------------------------------------------------------- 1 | datasetResponseBridge = $datasetResponseBridge; 18 | } 19 | 20 | public function build(DataEuGetDatasetResponse $response): Dataset 21 | { 22 | return $this->datasetResponseBridge->build($response->getResult()); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Supplier/DataEu/Common/DataEuDistribution.php: -------------------------------------------------------------------------------- 1 | access_url; 23 | } 24 | 25 | public function getFormat(): ?DataEuFormat 26 | { 27 | return $this->format; 28 | } 29 | 30 | public function getId(): string 31 | { 32 | return $this->id; 33 | } 34 | } -------------------------------------------------------------------------------- /migrations/Version20210510171213.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE dataset ADD external_id LONGTEXT DEFAULT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE dataset DROP external_id'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20210523174745.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE distribution ADD payload LONGTEXT NOT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE distribution DROP payload'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20210523175734.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE distribution ADD external_id VARCHAR(255) NOT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE distribution DROP external_id'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Supplier/DataEu/Search/DataEuResultResponse.php: -------------------------------------------------------------------------------- 1 | ") 15 | * @var DataEuDataset[]|DataEuDatasetCollection 16 | */ 17 | private $results = []; 18 | 19 | /** @Serializer\Type("int") */ 20 | private int $count; 21 | 22 | public function getResults(): DataEuDatasetCollection 23 | { 24 | if (!$this->results instanceof DataEuDatasetCollection) { 25 | $this->results = new DataEuDatasetCollection($this->results); 26 | } 27 | 28 | return $this->results; 29 | } 30 | 31 | public function getCount(): int 32 | { 33 | return $this->count; 34 | } 35 | } -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers 3 | providers: 4 | users_in_memory: { memory: null } 5 | firewalls: 6 | dev: 7 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 8 | security: false 9 | main: 10 | anonymous: true 11 | lazy: true 12 | provider: users_in_memory 13 | 14 | # activate different ways to authenticate 15 | # https://symfony.com/doc/current/security.html#firewalls-authentication 16 | 17 | # https://symfony.com/doc/current/security/impersonating_user.html 18 | # switch_user: true 19 | 20 | # Easy way to control access for large sections of your site 21 | # Note: Only the *first* access control that matches will be used 22 | access_control: 23 | # - { path: ^/admin, roles: ROLE_ADMIN } 24 | # - { path: ^/profile, roles: ROLE_USER } 25 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 6 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 7 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 8 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 9 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 10 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 11 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 12 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 13 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], 14 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 15 | JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true], 16 | ]; 17 | -------------------------------------------------------------------------------- /src/Api/Builder/ApiResponseBuilder.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 19 | } 20 | 21 | public function build(ApiResponseInterface $response): JsonResponse 22 | { 23 | $statusCode = $response->isSuccess() ? Response::HTTP_OK : Response::HTTP_INTERNAL_SERVER_ERROR; 24 | $data = $this->buildJsonString($response); 25 | 26 | return new JsonResponse($data, $statusCode, [], true); 27 | } 28 | 29 | public function buildJsonString(ApiResponseInterface $response): string 30 | { 31 | return $this->serializer->serialize($response, 'json'); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Supplier/DataEu/Common/DataEuDescription.php: -------------------------------------------------------------------------------- 1 | en; 23 | } 24 | 25 | public function setEn(string $en): void 26 | { 27 | $this->en = $en; 28 | } 29 | 30 | public function getDe(): ?string 31 | { 32 | return $this->de; 33 | } 34 | 35 | public function setDe(string $de): void 36 | { 37 | $this->de = $de; 38 | } 39 | 40 | public function getFr(): ?string 41 | { 42 | return $this->fr; 43 | } 44 | 45 | public function setFr(string $fr): void 46 | { 47 | $this->fr = $fr; 48 | } 49 | } -------------------------------------------------------------------------------- /templates/page/import.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | 5 | {% endblock %} 6 | 7 | {% block body %} 8 |
9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
#КраїнаОпис(EN)Опис(DE)Опис(FR)Доступні форматиДія
28 |
29 | 30 | 31 | 32 |
33 | {% endblock %} -------------------------------------------------------------------------------- /src/SupplierFacade/DataEu/RequestBuilder/DataEuSearchRequestBuilder.php: -------------------------------------------------------------------------------- 1 | getQuery()) { 18 | $query['q'] = $searchRequest->getQuery(); 19 | } 20 | $query['page'] = $searchRequest->getPage(); 21 | $query['limit'] = $searchRequest->getPerPage(); 22 | $query['sort'] = self::SORT_BY; 23 | $query['filter'] = self::FILTER_DATASET; 24 | $query['facets'] = $this->buildFacets($searchRequest); 25 | 26 | return $query; 27 | } 28 | 29 | private function buildFacets(ApiSearchRequest $searchRequest): string 30 | { 31 | $facets['format'] = DataEuConst::SUPPORTABLE_FORMATS; 32 | 33 | return json_encode($facets, JSON_THROW_ON_ERROR); 34 | } 35 | } -------------------------------------------------------------------------------- /src/SupplierFacade/DataEu/Bridge/Search/DataEuSearchResponseBridge.php: -------------------------------------------------------------------------------- 1 | datasetResponseBridge = $datasetResponseBridge; 21 | } 22 | 23 | /** @return Dataset[]|Collection */ 24 | public function build(DataEuSearchResponse $searchResponse): Collection 25 | { 26 | $result = new ArrayCollection(); 27 | 28 | /** @var DataEuDataset $dataset */ 29 | foreach ($searchResponse->getResult()->getResults() as $dataset) { 30 | $dataset = $this->datasetResponseBridge->build($dataset); 31 | $result->add($dataset); 32 | } 33 | 34 | return $result; 35 | } 36 | } -------------------------------------------------------------------------------- /src/Supplier/DataEu/Common/DataEuDataset.php: -------------------------------------------------------------------------------- 1 | ") 24 | */ 25 | private $distributions; 26 | 27 | public function getCountry(): DataEuCountry 28 | { 29 | return $this->country; 30 | } 31 | 32 | public function getDescription(): DataEuDescription 33 | { 34 | return $this->description; 35 | } 36 | 37 | public function getDistributions() 38 | { 39 | return $this->distributions; 40 | } 41 | 42 | public function getId(): string 43 | { 44 | return $this->id; 45 | } 46 | } -------------------------------------------------------------------------------- /src/Base/CollectionInterface.php: -------------------------------------------------------------------------------- 1 | searchMethodFacade = $searchMethodFacade; 23 | $this->getDatasetMethodFacade = $getDatasetMethodFacade; 24 | } 25 | 26 | public function search(ApiSearchRequest $searchRequest): ApiDatasetsResponse 27 | { 28 | return $this->searchMethodFacade->commit($searchRequest); 29 | } 30 | 31 | public function getDataset(ApiGetDatasetRequest $getDatasetRequest): ApiSuccessResponse 32 | { 33 | return $this->getDatasetMethodFacade->commit($getDatasetRequest); 34 | } 35 | } -------------------------------------------------------------------------------- /migrations/Version20210510163646.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE dataset CHANGE description_en description_en LONGTEXT DEFAULT NULL, CHANGE description_de description_de LONGTEXT DEFAULT NULL, CHANGE description_fr description_fr LONGTEXT DEFAULT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE dataset CHANGE description_en description_en VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE description_de description_de VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`, CHANGE description_fr description_fr VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/assets/js/observe.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | loadDatasets(); 3 | }); 4 | 5 | async function loadDatasets() { 6 | const response = await fetch('/api/get-all-datasets', { 7 | method: 'GET', 8 | headers: { 9 | 'Content-Type': 'application/json;charset=utf-8' 10 | } 11 | }); 12 | 13 | const result = await response.json(); 14 | const datasetsTbody = $('#datasets-table tbody'); 15 | datasetsTbody.empty(); 16 | result.dataset_collection.forEach(function (elem, index) { 17 | datasetsTbody.append(` 18 | ${index + 1} 19 | ${elem.country_code} 20 | ${elem.description_en || 'N/A'} 21 | ${elem.description_de || 'N/A'} 22 | ${elem.description_fr || 'N/A'} 23 | ${getDistributionsString(elem.distributions)} 24 | `); 25 | }); 26 | 27 | $('#page-number').html(page); 28 | } 29 | 30 | function getDistributionsString(distributions) { 31 | let result = []; 32 | 33 | distributions.forEach(function (elem) { 34 | result.push(`${elem.format}`); 35 | }) 36 | 37 | return result.join(', '); 38 | } -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | tests 21 | 22 | 23 | 24 | 25 | 26 | src 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 40 | 41 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 24 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 25 | } 26 | 27 | if ($input->hasParameterOption('--no-debug', true)) { 28 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 29 | } 30 | 31 | (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); 32 | 33 | if ($_SERVER['APP_DEBUG']) { 34 | umask(0000); 35 | 36 | if (class_exists(Debug::class)) { 37 | Debug::enable(); 38 | } 39 | } 40 | 41 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 42 | $application = new Application($kernel); 43 | $application->run($input); 44 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | # This file is the entry point to configure your own services. 2 | # Files in the packages/ subdirectory configure your dependencies. 3 | 4 | # Put parameters here that don't need to change on each machine where the app is deployed 5 | # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 6 | parameters: 7 | 8 | services: 9 | # default configuration for services in *this* file 10 | _defaults: 11 | autowire: true # Automatically injects dependencies in your services. 12 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 13 | 14 | # makes classes in src/ available to be used as services 15 | # this creates a service per class whose id is the fully-qualified class name 16 | App\: 17 | resource: '../src/' 18 | exclude: 19 | - '../src/DependencyInjection/' 20 | - '../src/Entity/' 21 | - '../src/Kernel.php' 22 | - '../src/Tests/' 23 | 24 | # controllers are imported separately to make sure services can be injected 25 | # as action arguments even if you don't extend any base controller class 26 | App\Controller\: 27 | resource: '../src/Controller/' 28 | tags: ['controller.service_arguments'] 29 | 30 | # add more service definitions when explicit configuration is needed 31 | # please note that last definitions always *replace* previous ones 32 | -------------------------------------------------------------------------------- /src/SupplierFacade/DataEu/Bridge/Common/DataEuDistributionResponseBridge.php: -------------------------------------------------------------------------------- 1 | setExternalId($supplierDistribution->getId()); 19 | 20 | $format = $supplierDistribution->getFormat(); 21 | if (!$format || !$this->isFormatAllowed($format->getId())) { 22 | throw new UnnecessaryDistributionCoreSupplierException(); 23 | } 24 | $distribution->setFormat($format->getId()); 25 | 26 | $accessUrl = $supplierDistribution->getAccessUrl(); 27 | if (!$accessUrl) { 28 | throw new UnnecessaryDistributionCoreSupplierException(); 29 | } 30 | $distribution->setDownloadUrl($supplierDistribution->getAccessUrl()); 31 | 32 | return $distribution; 33 | } 34 | 35 | private function isFormatAllowed(string $format): bool 36 | { 37 | foreach (DataEuConst::SUPPORTABLE_FORMATS as $supportableFormat) { 38 | if (strcasecmp($format, $supportableFormat) === 0) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | } -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | import('../config/{packages}/*.yaml'); 17 | $container->import('../config/{packages}/'.$this->environment.'/*.yaml'); 18 | 19 | if (is_file(\dirname(__DIR__).'/config/services.yaml')) { 20 | $container->import('../config/services.yaml'); 21 | $container->import('../config/{services}_'.$this->environment.'.yaml'); 22 | } elseif (is_file($path = \dirname(__DIR__).'/config/services.php')) { 23 | (require $path)($container->withPath($path), $this); 24 | } 25 | } 26 | 27 | protected function configureRoutes(RoutingConfigurator $routes): void 28 | { 29 | $routes->import('../config/{routes}/'.$this->environment.'/*.yaml'); 30 | $routes->import('../config/{routes}/*.yaml'); 31 | 32 | if (is_file(\dirname(__DIR__).'/config/routes.yaml')) { 33 | $routes->import('../config/routes.yaml'); 34 | } elseif (is_file($path = \dirname(__DIR__).'/config/routes.php')) { 35 | (require $path)($routes->withPath($path), $this); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | # the latter taking precedence over the former: 3 | # 4 | # * .env contains default values for the environment variables needed by the app 5 | # * .env.local uncommitted file with local overrides 6 | # * .env.$APP_ENV committed environment-specific defaults 7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides 8 | # 9 | # Real environment variables win over .env files. 10 | # 11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. 12 | # 13 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). 14 | # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration 15 | 16 | ###> symfony/framework-bundle ### 17 | APP_ENV=dev 18 | APP_SECRET=ca69f36ecdc0a6dfad72471ddb5336df 19 | ###< symfony/framework-bundle ### 20 | 21 | ###> symfony/mailer ### 22 | # MAILER_DSN=smtp://localhost 23 | ###< symfony/mailer ### 24 | 25 | ###> doctrine/doctrine-bundle ### 26 | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 27 | # IMPORTANT: DATABASE_URL *must* define the server version 28 | # 29 | # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" 30 | DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" 31 | #DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8" 32 | ###< doctrine/doctrine-bundle ### 33 | -------------------------------------------------------------------------------- /src/Repository/DistributionRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('d') 29 | ->andWhere('d.exampleField = :val') 30 | ->setParameter('val', $value) 31 | ->orderBy('d.id', 'ASC') 32 | ->setMaxResults(10) 33 | ->getQuery() 34 | ->getResult() 35 | ; 36 | } 37 | */ 38 | 39 | /* 40 | public function findOneBySomeField($value): ?Distribution 41 | { 42 | return $this->createQueryBuilder('d') 43 | ->andWhere('d.exampleField = :val') 44 | ->setParameter('val', $value) 45 | ->getQuery() 46 | ->getOneOrNullResult() 47 | ; 48 | } 49 | */ 50 | } 51 | -------------------------------------------------------------------------------- /migrations/Version20210510155228.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE dataset (id INT AUTO_INCREMENT NOT NULL, country_code VARCHAR(255) NOT NULL, description_en VARCHAR(255) DEFAULT NULL, description_de VARCHAR(255) DEFAULT NULL, description_fr VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); 24 | $this->addSql('CREATE TABLE distribution (id INT AUTO_INCREMENT NOT NULL, dataset_id INT NOT NULL, format VARCHAR(255) NOT NULL, download_url VARCHAR(255) NOT NULL, INDEX IDX_A4483781D47C2D1B (dataset_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); 25 | $this->addSql('ALTER TABLE distribution ADD CONSTRAINT FK_A4483781D47C2D1B FOREIGN KEY (dataset_id) REFERENCES dataset (id)'); 26 | } 27 | 28 | public function down(Schema $schema): void 29 | { 30 | // this down() migration is auto-generated, please modify it to your needs 31 | $this->addSql('ALTER TABLE distribution DROP FOREIGN KEY FK_A4483781D47C2D1B'); 32 | $this->addSql('DROP TABLE dataset'); 33 | $this->addSql('DROP TABLE distribution'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Repository/DatasetRepository.php: -------------------------------------------------------------------------------- 1 | findBy([], ['id' => 'desc'])); 26 | } 27 | 28 | // /** 29 | // * @return Dataset[] Returns an array of Dataset objects 30 | // */ 31 | /* 32 | public function findByExampleField($value) 33 | { 34 | return $this->createQueryBuilder('d') 35 | ->andWhere('d.exampleField = :val') 36 | ->setParameter('val', $value) 37 | ->orderBy('d.id', 'ASC') 38 | ->setMaxResults(10) 39 | ->getQuery() 40 | ->getResult() 41 | ; 42 | } 43 | */ 44 | 45 | /* 46 | public function findOneBySomeField($value): ?Dataset 47 | { 48 | return $this->createQueryBuilder('d') 49 | ->andWhere('d.exampleField = :val') 50 | ->setParameter('val', $value) 51 | ->getQuery() 52 | ->getOneOrNullResult() 53 | ; 54 | } 55 | */ 56 | } 57 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | {% block stylesheets %} 9 | {% endblock %} 10 | 11 | 12 | 13 | 14 | {% block javascripts %} 15 | {% endblock %} 16 | 17 | 18 | 36 | {% block body %}{% endblock %} 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/SupplierFacade/DataEu/Bridge/Common/DataEuDatasetResponseBridge.php: -------------------------------------------------------------------------------- 1 | distributionResponseBridge = $distributionResponseBridge; 21 | $this->logger = $logger; 22 | } 23 | 24 | public function build(DataEuDataset $supplierDataset): Dataset 25 | { 26 | $dataset = new Dataset(); 27 | 28 | $dataset->setExternalId($supplierDataset->getId()); 29 | 30 | $countryCode = strtoupper($supplierDataset->getCountry()->getId()); 31 | $dataset->setCountryCode($countryCode); 32 | 33 | $description = $supplierDataset->getDescription(); 34 | $dataset->setDescriptionEn($description->getEn()); 35 | $dataset->setDescriptionDe($description->getDe()); 36 | $dataset->setDescriptionFr($description->getFr()); 37 | 38 | foreach ($supplierDataset->getDistributions() as $supplierDistribution) { 39 | try { 40 | $distribution = $this->distributionResponseBridge->build($supplierDistribution); 41 | $dataset->addDistribution($distribution); 42 | } catch (UnnecessaryDistributionCoreSupplierException $throwable) { 43 | $this->logger->info('Distribution was ignored due to empty download url'); 44 | } 45 | } 46 | 47 | return $dataset; 48 | } 49 | } -------------------------------------------------------------------------------- /src/Controller/SupplierController.php: -------------------------------------------------------------------------------- 1 | requestBuilder = $requestBuilder; 30 | $this->responseBuilder = $responseBuilder; 31 | $this->dataEuSupplierFacade = $dataEuSupplierFacade; 32 | } 33 | 34 | /** 35 | * @Route("/search", name="search", methods={"POST"}) 36 | */ 37 | public function search(Request $request): Response 38 | { 39 | $data = $request->getContent(); 40 | /** @var ApiSearchRequest $searchRequest */ 41 | $searchRequest = $this->requestBuilder->build(ApiSearchRequest::class, $data); 42 | 43 | $response = $this->dataEuSupplierFacade->search($searchRequest); 44 | 45 | return $this->responseBuilder->build($response); 46 | } 47 | 48 | /** 49 | * @Route("/get-dataset", name="import_dataset", methods={"POST"}) 50 | */ 51 | public function getDataset(Request $request): Response 52 | { 53 | $data = $request->getContent(); 54 | /** @var ApiGetDatasetRequest $getDatasetRequest */ 55 | $getDatasetRequest = $this->requestBuilder->build(ApiGetDatasetRequest::class, $data); 56 | 57 | $response = $this->dataEuSupplierFacade->getDataset($getDatasetRequest); 58 | 59 | return $this->responseBuilder->build($response); 60 | } 61 | } -------------------------------------------------------------------------------- /src/SupplierFacade/DataEu/MethodFacade/DataEuSearchMethodFacade.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 30 | $this->searchResponseBridge = $searchResponseBridge; 31 | $this->searchRequestBuilder = $searchRequestBuilder; 32 | } 33 | 34 | public function commit(ApiSearchRequest $searchRequest): ApiDatasetsResponse 35 | { 36 | $stringResponse = $this->sendAndGetStringResponse($searchRequest); 37 | 38 | /** @var DataEuSearchResponse $response */ 39 | $response = $this->serializer->deserialize($stringResponse, DataEuSearchResponse::class, 'json'); 40 | $datasetCollection = $this->searchResponseBridge->build($response); 41 | $totalDatasetsAmount = $response->getResult()->getCount(); 42 | 43 | $apiSearchResponse = new ApiDatasetsResponse(); 44 | $apiSearchResponse->setDatasetCollection($datasetCollection); 45 | $apiSearchResponse->setTotalDatasetsAmount($totalDatasetsAmount); 46 | 47 | return $apiSearchResponse; 48 | } 49 | 50 | private function sendAndGetStringResponse(ApiSearchRequest $searchRequest): string 51 | { 52 | $client = new Client(['base_uri' => DataEuConst::BASE_URI]); 53 | 54 | $response = $client->get( 55 | 'search', 56 | [ 57 | 'query' => $this->searchRequestBuilder->build($searchRequest), 58 | ] 59 | ); 60 | 61 | return $response->getBody(); 62 | } 63 | } -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | Header always add Access-Control-Allow-Origin "*" 4 | Header always add Access-Control-Allow-Headers: "content-type, Referer, Accept, User-Agent" 5 | Header always add Access-Control-Allow-Methods: "POST, GET, OPTIONS, DELETE, PUT" 6 | 7 | # Redirect to URI without front controller to prevent duplicate content 8 | # (with and without `/app.php`). Only do this redirect on the initial 9 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an 10 | # endless redirect loop (request -> rewrite to front controller -> 11 | # redirect -> request -> ...). 12 | # So in case you get a "too many redirects" error or you always get redirected 13 | # to the startpage because your Apache does not expose the REDIRECT_STATUS 14 | # environment variable, you have 2 choices: 15 | # - disable this feature by commenting the following 2 lines or 16 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the 17 | # following RewriteCond (best solution) 18 | RewriteCond %{ENV:REDIRECT_STATUS} ^$ 19 | RewriteRule ^index\.php(/(.*)|$) %{CONTEXT_PREFIX}/$2 [R=301,L] 20 | 21 | # If the requested filename exists, simply serve it. 22 | # We only want to let Apache serve files and not directories. 23 | RewriteCond %{REQUEST_FILENAME} -f 24 | RewriteRule .? - [L] 25 | 26 | RewriteCond %{REQUEST_METHOD} OPTIONS 27 | RewriteRule ^(.*)$ $1 [R=200,L] 28 | 29 | # cache-bust assets url rewrite 30 | # Example format: /cpv-10/js/test123.js -> /js/test123.js 31 | # This allows us to change the asset version and "bust" intermediate caches (like varnish) 32 | # See http://symfony.com/doc/current/reference/configuration/framework.html#ref-framework-assets-version 33 | # See http://symfony.com/doc/current/reference/configuration/framework.html#assets-version-format 34 | RewriteCond %{REQUEST_FILENAME} !-f 35 | RewriteCond %{REQUEST_FILENAME} !-d 36 | RewriteRule ^cpv-\d+\/(.+)$ $1 [L] 37 | 38 | RewriteCond %{REQUEST_FILENAME} -f 39 | RewriteRule ^(.*)$ index.php [QSA,L] 40 | 41 | # The following rewrites all other queries to the front controller. The 42 | # condition ensures that if you are using Apache aliases to do mass virtual 43 | # hosting, the base path will be prepended to allow proper resolution of the 44 | # app.php file; it will work in non-aliased environments as well, providing 45 | # a safe, one-size fits all solution. 46 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$ 47 | RewriteRule ^(.*) - [E=BASE:%1] 48 | RewriteRule .? %{ENV:BASE}index.php [L] 49 | -------------------------------------------------------------------------------- /src/Controller/ApiController.php: -------------------------------------------------------------------------------- 1 | requestBuilder = $requestBuilder; 34 | $this->responseBuilder = $responseBuilder; 35 | } 36 | 37 | /** 38 | * @Route("/datasets", name="get_all_datasets", methods={"GET"}) 39 | */ 40 | public function getAllDatasets(DatasetRepository $datasetRepository): Response 41 | { 42 | $datasets = $datasetRepository->findAll(); 43 | 44 | $response = new ApiDatasetsResponse(); 45 | $response->setDatasetCollection($datasets); 46 | 47 | return $this->responseBuilder->build($response); 48 | } 49 | 50 | /** 51 | * @Route("/datasets/{id}") 52 | * @ParamConverter("dataset", class="App\Entity\Dataset") 53 | */ 54 | public function getDataset(Dataset $dataset): Response 55 | { 56 | $response = new ApiGetDatasetResponse($dataset); 57 | 58 | return $this->responseBuilder->build($response); 59 | } 60 | 61 | /** 62 | * @Route("/datasets", name="get_dataset", methods={"PUT"}) 63 | */ 64 | public function saveDataset(Request $request, EntityManagerInterface $em): Response 65 | { 66 | $data = $request->getContent(); 67 | /** @var ApiDatasetRequest $requestObj */ 68 | $requestObj = $this->requestBuilder->build(ApiDatasetRequest::class, $data); 69 | $dataset = $requestObj->getDataset(); 70 | 71 | $em->persist($dataset); 72 | $em->flush(); 73 | 74 | $response = new ApiSuccessResponse(); 75 | 76 | return $this->responseBuilder->build($response); 77 | } 78 | } -------------------------------------------------------------------------------- /public/assets/js/import.js: -------------------------------------------------------------------------------- 1 | const perPage = 15; 2 | let page = 1; 3 | let query = null; 4 | 5 | $(document).ready(function () { 6 | $('#search-button').click(function () { 7 | query = $('#search-input').val(); 8 | loadDatasets(); 9 | }); 10 | 11 | $(document).on('click', '.dataset-import-button', function () { 12 | const externalId = $(this).closest('tr').data('external_id'); 13 | 14 | fetch('/api/import-dataset', { 15 | method: 'POST', 16 | headers: { 17 | 'Content-Type': 'application/json;charset=utf-8' 18 | }, 19 | body: JSON.stringify({ 20 | 'external_id': externalId 21 | }) 22 | }).then(function (response) { 23 | if (response.status === 200) { 24 | alert('Successfully imported') 25 | } 26 | }) 27 | }); 28 | 29 | $('#next-button').click(() => changePage(1)); 30 | $('#prev-button').click(() => changePage(-1)); 31 | }); 32 | 33 | async function loadDatasets() { 34 | const response = await fetch('/api/search', { 35 | method: 'POST', 36 | headers: { 37 | 'Content-Type': 'application/json;charset=utf-8' 38 | }, 39 | body: JSON.stringify({ 40 | 'query': query, 41 | 'page': page, 42 | 'per_page': perPage 43 | }) 44 | }); 45 | 46 | const result = await response.json(); 47 | const datasetsTbody = $('#datasets-table tbody'); 48 | datasetsTbody.empty(); 49 | result.dataset_collection.forEach(function (elem, index) { 50 | datasetsTbody.append(` 51 | ${index + 1} 52 | ${elem.country_code} 53 | ${elem.description_en || 'N/A'} 54 | ${elem.description_de || 'N/A'} 55 | ${elem.description_fr || 'N/A'} 56 | ${getDistributionsString(elem.distributions)} 57 | 58 | `); 59 | }); 60 | 61 | $('#page-number').html(page); 62 | } 63 | 64 | function getDistributionsString(distributions) { 65 | let result = []; 66 | 67 | distributions.forEach(function (elem) { 68 | result.push(`${elem.format}`); 69 | }) 70 | 71 | return result.join(', '); 72 | } 73 | 74 | function changePage(pageIncrement) { 75 | page += pageIncrement; 76 | loadDatasets(); 77 | } -------------------------------------------------------------------------------- /src/Entity/Distribution.php: -------------------------------------------------------------------------------- 1 | id; 54 | } 55 | 56 | public function getFormat(): ?string 57 | { 58 | return $this->format; 59 | } 60 | 61 | public function setFormat(string $format): self 62 | { 63 | $this->format = $format; 64 | 65 | return $this; 66 | } 67 | 68 | public function getDownloadUrl(): ?string 69 | { 70 | return $this->downloadUrl; 71 | } 72 | 73 | public function setDownloadUrl(string $downloadUrl): self 74 | { 75 | $this->downloadUrl = $downloadUrl; 76 | 77 | return $this; 78 | } 79 | 80 | public function getDataset(): Dataset 81 | { 82 | return $this->dataset; 83 | } 84 | 85 | public function setDataset(Dataset $dataset): self 86 | { 87 | $this->dataset = $dataset; 88 | 89 | return $this; 90 | } 91 | 92 | public function getPayload(): ?string 93 | { 94 | return $this->payload; 95 | } 96 | 97 | public function setPayload(string $payload): void 98 | { 99 | $this->payload = $payload; 100 | } 101 | 102 | public function getExternalId(): string 103 | { 104 | return $this->externalId; 105 | } 106 | 107 | public function setExternalId(string $externalId): void 108 | { 109 | $this->externalId = $externalId; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "license": "proprietary", 4 | "minimum-stability": "dev", 5 | "prefer-stable": true, 6 | "require": { 7 | "php": ">=7.2.5", 8 | "ext-ctype": "*", 9 | "ext-iconv": "*", 10 | "composer/package-versions-deprecated": "1.11.99.1", 11 | "doctrine/annotations": "^1.0", 12 | "doctrine/doctrine-bundle": "^2.3", 13 | "doctrine/doctrine-migrations-bundle": "^3.1", 14 | "doctrine/orm": "^2.8", 15 | "guzzlehttp/guzzle": "^7.3", 16 | "jms/serializer-bundle": "^3.9.0", 17 | "phpdocumentor/reflection-docblock": "^5.2", 18 | "sensio/framework-extra-bundle": "^5.1", 19 | "symfony/asset": "5.2.*", 20 | "symfony/console": "5.2.*", 21 | "symfony/dotenv": "5.2.*", 22 | "symfony/expression-language": "5.2.*", 23 | "symfony/flex": "^1.3.1", 24 | "symfony/form": "5.2.*", 25 | "symfony/framework-bundle": "5.2.*", 26 | "symfony/http-client": "5.2.*", 27 | "symfony/intl": "5.2.*", 28 | "symfony/mailer": "5.2.*", 29 | "symfony/mime": "5.2.*", 30 | "symfony/monolog-bundle": "^3.1", 31 | "symfony/notifier": "5.2.*", 32 | "symfony/process": "5.2.*", 33 | "symfony/property-access": "5.2.*", 34 | "symfony/property-info": "5.2.*", 35 | "symfony/proxy-manager-bridge": "5.2.*", 36 | "symfony/security-bundle": "5.2.*", 37 | "symfony/serializer": "5.2.*", 38 | "symfony/string": "5.2.*", 39 | "symfony/translation": "5.2.*", 40 | "symfony/twig-bundle": "^5.2", 41 | "symfony/validator": "5.2.*", 42 | "symfony/web-link": "5.2.*", 43 | "symfony/yaml": "5.2.*", 44 | "twig/extra-bundle": "^2.12|^3.0", 45 | "twig/twig": "^2.12|^3.0", 46 | "ext-json": "*" 47 | }, 48 | "require-dev": { 49 | "symfony/browser-kit": "^5.2", 50 | "symfony/css-selector": "^5.2", 51 | "symfony/debug-bundle": "^5.2", 52 | "symfony/maker-bundle": "^1.0", 53 | "symfony/phpunit-bridge": "^5.2", 54 | "symfony/stopwatch": "^5.2", 55 | "symfony/var-dumper": "^5.2", 56 | "symfony/web-profiler-bundle": "^5.2" 57 | }, 58 | "config": { 59 | "optimize-autoloader": true, 60 | "preferred-install": { 61 | "*": "dist" 62 | }, 63 | "sort-packages": true 64 | }, 65 | "autoload": { 66 | "psr-4": { 67 | "App\\": "src/" 68 | } 69 | }, 70 | "autoload-dev": { 71 | "psr-4": { 72 | "App\\Tests\\": "tests/" 73 | } 74 | }, 75 | "replace": { 76 | "symfony/polyfill-ctype": "*", 77 | "symfony/polyfill-iconv": "*", 78 | "symfony/polyfill-php72": "*" 79 | }, 80 | "scripts": { 81 | "auto-scripts": { 82 | "cache:clear": "symfony-cmd", 83 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 84 | }, 85 | "post-install-cmd": [ 86 | "@auto-scripts" 87 | ], 88 | "post-update-cmd": [ 89 | "@auto-scripts" 90 | ] 91 | }, 92 | "conflict": { 93 | "symfony/symfony": "*" 94 | }, 95 | "extra": { 96 | "symfony": { 97 | "allow-contrib": false, 98 | "require": "5.2.*" 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Entity/Dataset.php: -------------------------------------------------------------------------------- 1 | ") 58 | */ 59 | private Collection $distributions; 60 | 61 | public function __construct() 62 | { 63 | $this->distributions = new ArrayCollection(); 64 | } 65 | 66 | public function getId(): ?int 67 | { 68 | return $this->id; 69 | } 70 | 71 | public function getCountryCode(): ?string 72 | { 73 | return $this->countryCode; 74 | } 75 | 76 | public function setCountryCode(string $countryCode): self 77 | { 78 | $this->countryCode = $countryCode; 79 | 80 | return $this; 81 | } 82 | 83 | public function getDescriptionEn(): ?string 84 | { 85 | return $this->descriptionEn; 86 | } 87 | 88 | public function setDescriptionEn(?string $descriptionEn): self 89 | { 90 | $this->descriptionEn = $descriptionEn; 91 | 92 | return $this; 93 | } 94 | 95 | public function getDescriptionDe(): ?string 96 | { 97 | return $this->descriptionDe; 98 | } 99 | 100 | public function setDescriptionDe(?string $descriptionDe): self 101 | { 102 | $this->descriptionDe = $descriptionDe; 103 | 104 | return $this; 105 | } 106 | 107 | public function getDescriptionFr(): ?string 108 | { 109 | return $this->descriptionFr; 110 | } 111 | 112 | public function setDescriptionFr(?string $descriptionFr): self 113 | { 114 | $this->descriptionFr = $descriptionFr; 115 | 116 | return $this; 117 | } 118 | 119 | public function getExternalId(): string 120 | { 121 | return $this->externalId; 122 | } 123 | 124 | public function setExternalId(string $externalId): void 125 | { 126 | $this->externalId = $externalId; 127 | } 128 | 129 | /** 130 | * @return Collection|Distribution[] 131 | */ 132 | public function getDistributions(): Collection 133 | { 134 | return $this->distributions; 135 | } 136 | 137 | public function addDistribution(Distribution $distribution): self 138 | { 139 | if (!$this->distributions->contains($distribution)) { 140 | $this->distributions[] = $distribution; 141 | $distribution->setDataset($this); 142 | } 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * @ORM\PreFlush() 149 | */ 150 | public function doOnPreFlush(): void 151 | { 152 | foreach ($this->getDistributions() as $distribution) { 153 | $distribution->setDataset($this); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/SupplierFacade/DataEu/MethodFacade/DataEuGetDatasetMethodFacade.php: -------------------------------------------------------------------------------- 1 | 'PostmanRuntime/7.26.8']; 26 | 27 | private SerializerInterface $serializer; 28 | 29 | private DataEuGetDatasetResponseBridge $getDatasetResponseBridge; 30 | 31 | private EntityManagerInterface $entityManager; 32 | 33 | public function __construct( 34 | SerializerInterface $serializer, 35 | DataEuGetDatasetResponseBridge $getDatasetResponseBridge, 36 | EntityManagerInterface $entityManager 37 | ) { 38 | $this->serializer = $serializer; 39 | $this->getDatasetResponseBridge = $getDatasetResponseBridge; 40 | $this->entityManager = $entityManager; 41 | } 42 | 43 | public function commit(ApiGetDatasetRequest $getDatasetRequest): ApiSuccessResponse 44 | { 45 | $stringResponse = $this->sendAndGetStringResponse($getDatasetRequest); 46 | 47 | $response = $this->serializer->deserialize($stringResponse, DataEuGetDatasetResponse::class, 'json'); 48 | $dataset = $this->getDatasetResponseBridge->build($response); 49 | 50 | $this->loadDistributionPayloads($dataset); 51 | 52 | if ($getDatasetRequest->usePersist()) { 53 | $this->entityManager->persist($dataset); 54 | $this->entityManager->flush(); 55 | } 56 | 57 | return new ApiGetDatasetResponse($dataset); 58 | } 59 | 60 | private function sendAndGetStringResponse(ApiGetDatasetRequest $getDatasetRequest): string 61 | { 62 | $client = new Client(['base_uri' => DataEuConst::BASE_URI]); 63 | 64 | $uri = sprintf(self::URI_PATTERN, $getDatasetRequest->getExternalId()); 65 | $response = $client->get($uri); 66 | 67 | return $response->getBody(); 68 | } 69 | 70 | private function loadDistributionPayloads(Dataset $dataset): void 71 | { 72 | $client = new Client(); 73 | $promises = []; 74 | 75 | foreach ($dataset->getDistributions() as $distribution) { 76 | $promises[$distribution->getExternalId()] = $client->getAsync($distribution->getDownloadUrl(), ['headers' => self::DOWNLOAD_HEADERS]); 77 | } 78 | 79 | $responses = Promise\Utils::settle($promises)->wait(); 80 | 81 | foreach ($dataset->getDistributions() as $distribution) { 82 | try { 83 | $content = $this->getResponseContent($responses[$distribution->getExternalId()]); 84 | $distribution->setPayload($content); 85 | } catch (UnexpectedValueException $exception) { 86 | $dataset->getDistributions()->removeElement($distribution); 87 | } 88 | } 89 | } 90 | 91 | private function getResponseContent(array $response): string 92 | { 93 | $responseState = $response['state']; 94 | if ($responseState !== Promise\Promise::FULFILLED) { 95 | throw new UnexpectedValueException(); 96 | } 97 | 98 | /** @var Response $responseObj */ 99 | $responseObj = $response['value']; 100 | $body = $responseObj->getBody(); 101 | if (!$body) { 102 | throw new UnexpectedValueException(); 103 | } 104 | 105 | $content = $body->getContents(); 106 | if (mb_detect_encoding($content, null, true) === false) { 107 | throw new UnexpectedValueException(); 108 | } 109 | 110 | return $content; 111 | } 112 | } -------------------------------------------------------------------------------- /public/assets/lib/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.0.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /public/assets/lib/bootstrap/css/bootstrap-reboot.rtl.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.0.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */ -------------------------------------------------------------------------------- /src/Base/AbstractCollection.php: -------------------------------------------------------------------------------- 1 | validateElement($element); 24 | } 25 | $this->elements = $elements; 26 | } 27 | 28 | protected function validateElement($element): void 29 | { 30 | if (!$this->isValidElement($element)) { 31 | $this->throwInvalidObjectClassException($element); 32 | } 33 | } 34 | 35 | protected function isValidElement($element): bool 36 | { 37 | $typeName = $this->getTypeName(); 38 | 39 | if ($typeName === "string") { 40 | return is_string($element); 41 | } 42 | 43 | return $element instanceof $typeName; 44 | } 45 | 46 | protected function throwInvalidObjectClassException($invalidElementData): void 47 | { 48 | $errorMessage = sprintf( 49 | self::ELEMENT_TYPE_MISMATCHED_TEMPLATE, 50 | $this->getTypeName(), 51 | gettype($invalidElementData), 52 | var_export($invalidElementData, true) 53 | ); 54 | throw new InvalidArgumentException($errorMessage); 55 | } 56 | 57 | public function addCollection(AbstractCollection $collectionToMerge): void 58 | { 59 | foreach ($collectionToMerge as $item) { 60 | $this->add($item); 61 | } 62 | } 63 | 64 | public function add($element): void 65 | { 66 | $this->validateElement($element); 67 | $this->elements[] = $element; 68 | } 69 | 70 | public function set(string $key, $element): void 71 | { 72 | $this->validateElement($element); 73 | $this->elements[$key] = $element; 74 | } 75 | 76 | public function isValid(): bool 77 | { 78 | foreach ($this as $element) { 79 | if (!$this->isValidElement($element)) { 80 | return false; 81 | } 82 | } 83 | 84 | return true; 85 | } 86 | 87 | public function offsetSet($offset, $value): void 88 | { 89 | if (!isset($offset)) { 90 | $this->add($value); 91 | 92 | return; 93 | } 94 | 95 | $this->set($offset, $value); 96 | } 97 | 98 | public function clear(): void 99 | { 100 | $this->elements = []; 101 | } 102 | 103 | public function contains($element): bool 104 | { 105 | return in_array($element, $this->elements, true); 106 | } 107 | 108 | public function filter(Closure $p): AbstractCollection 109 | { 110 | return $this->createFrom(array_filter($this->elements, $p, ARRAY_FILTER_USE_BOTH)); 111 | } 112 | 113 | protected function createFrom(array $elements): AbstractCollection 114 | { 115 | return new static($elements); 116 | } 117 | 118 | public function isEmpty(): bool 119 | { 120 | return empty($this->elements); 121 | } 122 | 123 | public function remove($key) 124 | { 125 | if (!isset($this->elements[$key]) && !array_key_exists($key, $this->elements)) { 126 | return null; 127 | } 128 | 129 | $removed = $this->elements[$key]; 130 | unset($this->elements[$key]); 131 | 132 | return $removed; 133 | } 134 | 135 | public function removeElement($element): bool 136 | { 137 | $key = array_search($element, $this->elements, true); 138 | 139 | if ($key === false) { 140 | return false; 141 | } 142 | 143 | unset($this->elements[$key]); 144 | 145 | return true; 146 | } 147 | 148 | public function containsKey($key): bool 149 | { 150 | return isset($this->elements[$key]) || array_key_exists($key, $this->elements); 151 | } 152 | 153 | public function get($key) 154 | { 155 | return $this->elements[$key] ?? null; 156 | } 157 | 158 | public function getKeys(): array 159 | { 160 | return array_keys($this->elements); 161 | } 162 | 163 | public function getValues(): array 164 | { 165 | return array_values($this->elements); 166 | } 167 | 168 | public function toArray(): array 169 | { 170 | return $this->elements; 171 | } 172 | 173 | public function key() 174 | { 175 | return key($this->elements); 176 | } 177 | 178 | public function first() 179 | { 180 | $first = reset($this->elements); 181 | 182 | return $first !== false ? $first : null; 183 | } 184 | 185 | public function last() 186 | { 187 | $end = end($this->elements); 188 | 189 | return $end !== false ? $end : null; 190 | } 191 | 192 | public function current() 193 | { 194 | $current = current($this->elements); 195 | 196 | return $current !== false ? $current : null; 197 | } 198 | 199 | public function next() 200 | { 201 | $next = next($this->elements); 202 | 203 | return $next !== false ? $next : null; 204 | } 205 | 206 | public function getIterator(): ArrayIterator 207 | { 208 | return new ArrayIterator($this->elements); 209 | } 210 | 211 | public function offsetExists($offset): bool 212 | { 213 | return $this->containsKey($offset); 214 | } 215 | 216 | public function offsetGet($offset) 217 | { 218 | return $this->get($offset); 219 | } 220 | 221 | public function offsetUnset($offset): void 222 | { 223 | $this->remove($offset); 224 | } 225 | 226 | public function count(): int 227 | { 228 | return count($this->elements); 229 | } 230 | } -------------------------------------------------------------------------------- /public/assets/lib/bootstrap/css/bootstrap-reboot.rtl.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.0.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | @media (prefers-reduced-motion: no-preference) { 15 | :root { 16 | scroll-behavior: smooth; 17 | } 18 | } 19 | 20 | body { 21 | margin: 0; 22 | font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 23 | font-size: 1rem; 24 | font-weight: 400; 25 | line-height: 1.5; 26 | color: #212529; 27 | background-color: #fff; 28 | -webkit-text-size-adjust: 100%; 29 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 30 | } 31 | 32 | hr { 33 | margin: 1rem 0; 34 | color: inherit; 35 | background-color: currentColor; 36 | border: 0; 37 | opacity: 0.25; 38 | } 39 | 40 | hr:not([size]) { 41 | height: 1px; 42 | } 43 | 44 | h6, h5, h4, h3, h2, h1 { 45 | margin-top: 0; 46 | margin-bottom: 0.5rem; 47 | font-weight: 500; 48 | line-height: 1.2; 49 | } 50 | 51 | h1 { 52 | font-size: calc(1.375rem + 1.5vw); 53 | } 54 | @media (min-width: 1200px) { 55 | h1 { 56 | font-size: 2.5rem; 57 | } 58 | } 59 | 60 | h2 { 61 | font-size: calc(1.325rem + 0.9vw); 62 | } 63 | @media (min-width: 1200px) { 64 | h2 { 65 | font-size: 2rem; 66 | } 67 | } 68 | 69 | h3 { 70 | font-size: calc(1.3rem + 0.6vw); 71 | } 72 | @media (min-width: 1200px) { 73 | h3 { 74 | font-size: 1.75rem; 75 | } 76 | } 77 | 78 | h4 { 79 | font-size: calc(1.275rem + 0.3vw); 80 | } 81 | @media (min-width: 1200px) { 82 | h4 { 83 | font-size: 1.5rem; 84 | } 85 | } 86 | 87 | h5 { 88 | font-size: 1.25rem; 89 | } 90 | 91 | h6 { 92 | font-size: 1rem; 93 | } 94 | 95 | p { 96 | margin-top: 0; 97 | margin-bottom: 1rem; 98 | } 99 | 100 | abbr[title], 101 | abbr[data-bs-original-title] { 102 | -webkit-text-decoration: underline dotted; 103 | text-decoration: underline dotted; 104 | cursor: help; 105 | -webkit-text-decoration-skip-ink: none; 106 | text-decoration-skip-ink: none; 107 | } 108 | 109 | address { 110 | margin-bottom: 1rem; 111 | font-style: normal; 112 | line-height: inherit; 113 | } 114 | 115 | ol, 116 | ul { 117 | padding-right: 2rem; 118 | } 119 | 120 | ol, 121 | ul, 122 | dl { 123 | margin-top: 0; 124 | margin-bottom: 1rem; 125 | } 126 | 127 | ol ol, 128 | ul ul, 129 | ol ul, 130 | ul ol { 131 | margin-bottom: 0; 132 | } 133 | 134 | dt { 135 | font-weight: 700; 136 | } 137 | 138 | dd { 139 | margin-bottom: 0.5rem; 140 | margin-right: 0; 141 | } 142 | 143 | blockquote { 144 | margin: 0 0 1rem; 145 | } 146 | 147 | b, 148 | strong { 149 | font-weight: bolder; 150 | } 151 | 152 | small { 153 | font-size: 0.875em; 154 | } 155 | 156 | mark { 157 | padding: 0.2em; 158 | background-color: #fcf8e3; 159 | } 160 | 161 | sub, 162 | sup { 163 | position: relative; 164 | font-size: 0.75em; 165 | line-height: 0; 166 | vertical-align: baseline; 167 | } 168 | 169 | sub { 170 | bottom: -0.25em; 171 | } 172 | 173 | sup { 174 | top: -0.5em; 175 | } 176 | 177 | a { 178 | color: #0d6efd; 179 | text-decoration: underline; 180 | } 181 | a:hover { 182 | color: #0a58ca; 183 | } 184 | 185 | a:not([href]):not([class]), a:not([href]):not([class]):hover { 186 | color: inherit; 187 | text-decoration: none; 188 | } 189 | 190 | pre, 191 | code, 192 | kbd, 193 | samp { 194 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 195 | font-size: 1em; 196 | direction: ltr ; 197 | unicode-bidi: bidi-override; 198 | } 199 | 200 | pre { 201 | display: block; 202 | margin-top: 0; 203 | margin-bottom: 1rem; 204 | overflow: auto; 205 | font-size: 0.875em; 206 | } 207 | pre code { 208 | font-size: inherit; 209 | color: inherit; 210 | word-break: normal; 211 | } 212 | 213 | code { 214 | font-size: 0.875em; 215 | color: #d63384; 216 | word-wrap: break-word; 217 | } 218 | a > code { 219 | color: inherit; 220 | } 221 | 222 | kbd { 223 | padding: 0.2rem 0.4rem; 224 | font-size: 0.875em; 225 | color: #fff; 226 | background-color: #212529; 227 | border-radius: 0.2rem; 228 | } 229 | kbd kbd { 230 | padding: 0; 231 | font-size: 1em; 232 | font-weight: 700; 233 | } 234 | 235 | figure { 236 | margin: 0 0 1rem; 237 | } 238 | 239 | img, 240 | svg { 241 | vertical-align: middle; 242 | } 243 | 244 | table { 245 | caption-side: bottom; 246 | border-collapse: collapse; 247 | } 248 | 249 | caption { 250 | padding-top: 0.5rem; 251 | padding-bottom: 0.5rem; 252 | color: #6c757d; 253 | text-align: right; 254 | } 255 | 256 | th { 257 | text-align: inherit; 258 | text-align: -webkit-match-parent; 259 | } 260 | 261 | thead, 262 | tbody, 263 | tfoot, 264 | tr, 265 | td, 266 | th { 267 | border-color: inherit; 268 | border-style: solid; 269 | border-width: 0; 270 | } 271 | 272 | label { 273 | display: inline-block; 274 | } 275 | 276 | button { 277 | border-radius: 0; 278 | } 279 | 280 | button:focus:not(:focus-visible) { 281 | outline: 0; 282 | } 283 | 284 | input, 285 | button, 286 | select, 287 | optgroup, 288 | textarea { 289 | margin: 0; 290 | font-family: inherit; 291 | font-size: inherit; 292 | line-height: inherit; 293 | } 294 | 295 | button, 296 | select { 297 | text-transform: none; 298 | } 299 | 300 | [role=button] { 301 | cursor: pointer; 302 | } 303 | 304 | select { 305 | word-wrap: normal; 306 | } 307 | select:disabled { 308 | opacity: 1; 309 | } 310 | 311 | [list]::-webkit-calendar-picker-indicator { 312 | display: none; 313 | } 314 | 315 | button, 316 | [type=button], 317 | [type=reset], 318 | [type=submit] { 319 | -webkit-appearance: button; 320 | } 321 | button:not(:disabled), 322 | [type=button]:not(:disabled), 323 | [type=reset]:not(:disabled), 324 | [type=submit]:not(:disabled) { 325 | cursor: pointer; 326 | } 327 | 328 | ::-moz-focus-inner { 329 | padding: 0; 330 | border-style: none; 331 | } 332 | 333 | textarea { 334 | resize: vertical; 335 | } 336 | 337 | fieldset { 338 | min-width: 0; 339 | padding: 0; 340 | margin: 0; 341 | border: 0; 342 | } 343 | 344 | legend { 345 | float: right; 346 | width: 100%; 347 | padding: 0; 348 | margin-bottom: 0.5rem; 349 | font-size: calc(1.275rem + 0.3vw); 350 | line-height: inherit; 351 | } 352 | @media (min-width: 1200px) { 353 | legend { 354 | font-size: 1.5rem; 355 | } 356 | } 357 | legend + * { 358 | clear: right; 359 | } 360 | 361 | ::-webkit-datetime-edit-fields-wrapper, 362 | ::-webkit-datetime-edit-text, 363 | ::-webkit-datetime-edit-minute, 364 | ::-webkit-datetime-edit-hour-field, 365 | ::-webkit-datetime-edit-day-field, 366 | ::-webkit-datetime-edit-month-field, 367 | ::-webkit-datetime-edit-year-field { 368 | padding: 0; 369 | } 370 | 371 | ::-webkit-inner-spin-button { 372 | height: auto; 373 | } 374 | 375 | [type=search] { 376 | outline-offset: -2px; 377 | -webkit-appearance: textfield; 378 | } 379 | 380 | [type="tel"], 381 | [type="url"], 382 | [type="email"], 383 | [type="number"] { 384 | direction: ltr; 385 | } 386 | ::-webkit-search-decoration { 387 | -webkit-appearance: none; 388 | } 389 | 390 | ::-webkit-color-swatch-wrapper { 391 | padding: 0; 392 | } 393 | 394 | ::file-selector-button { 395 | font: inherit; 396 | } 397 | 398 | ::-webkit-file-upload-button { 399 | font: inherit; 400 | -webkit-appearance: button; 401 | } 402 | 403 | output { 404 | display: inline-block; 405 | } 406 | 407 | iframe { 408 | border: 0; 409 | } 410 | 411 | summary { 412 | display: list-item; 413 | cursor: pointer; 414 | } 415 | 416 | progress { 417 | vertical-align: baseline; 418 | } 419 | 420 | [hidden] { 421 | display: none !important; 422 | } 423 | /*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ -------------------------------------------------------------------------------- /public/assets/lib/bootstrap/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.0.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | @media (prefers-reduced-motion: no-preference) { 15 | :root { 16 | scroll-behavior: smooth; 17 | } 18 | } 19 | 20 | body { 21 | margin: 0; 22 | font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 23 | font-size: 1rem; 24 | font-weight: 400; 25 | line-height: 1.5; 26 | color: #212529; 27 | background-color: #fff; 28 | -webkit-text-size-adjust: 100%; 29 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 30 | } 31 | 32 | hr { 33 | margin: 1rem 0; 34 | color: inherit; 35 | background-color: currentColor; 36 | border: 0; 37 | opacity: 0.25; 38 | } 39 | 40 | hr:not([size]) { 41 | height: 1px; 42 | } 43 | 44 | h6, h5, h4, h3, h2, h1 { 45 | margin-top: 0; 46 | margin-bottom: 0.5rem; 47 | font-weight: 500; 48 | line-height: 1.2; 49 | } 50 | 51 | h1 { 52 | font-size: calc(1.375rem + 1.5vw); 53 | } 54 | @media (min-width: 1200px) { 55 | h1 { 56 | font-size: 2.5rem; 57 | } 58 | } 59 | 60 | h2 { 61 | font-size: calc(1.325rem + 0.9vw); 62 | } 63 | @media (min-width: 1200px) { 64 | h2 { 65 | font-size: 2rem; 66 | } 67 | } 68 | 69 | h3 { 70 | font-size: calc(1.3rem + 0.6vw); 71 | } 72 | @media (min-width: 1200px) { 73 | h3 { 74 | font-size: 1.75rem; 75 | } 76 | } 77 | 78 | h4 { 79 | font-size: calc(1.275rem + 0.3vw); 80 | } 81 | @media (min-width: 1200px) { 82 | h4 { 83 | font-size: 1.5rem; 84 | } 85 | } 86 | 87 | h5 { 88 | font-size: 1.25rem; 89 | } 90 | 91 | h6 { 92 | font-size: 1rem; 93 | } 94 | 95 | p { 96 | margin-top: 0; 97 | margin-bottom: 1rem; 98 | } 99 | 100 | abbr[title], 101 | abbr[data-bs-original-title] { 102 | -webkit-text-decoration: underline dotted; 103 | text-decoration: underline dotted; 104 | cursor: help; 105 | -webkit-text-decoration-skip-ink: none; 106 | text-decoration-skip-ink: none; 107 | } 108 | 109 | address { 110 | margin-bottom: 1rem; 111 | font-style: normal; 112 | line-height: inherit; 113 | } 114 | 115 | ol, 116 | ul { 117 | padding-left: 2rem; 118 | } 119 | 120 | ol, 121 | ul, 122 | dl { 123 | margin-top: 0; 124 | margin-bottom: 1rem; 125 | } 126 | 127 | ol ol, 128 | ul ul, 129 | ol ul, 130 | ul ol { 131 | margin-bottom: 0; 132 | } 133 | 134 | dt { 135 | font-weight: 700; 136 | } 137 | 138 | dd { 139 | margin-bottom: 0.5rem; 140 | margin-left: 0; 141 | } 142 | 143 | blockquote { 144 | margin: 0 0 1rem; 145 | } 146 | 147 | b, 148 | strong { 149 | font-weight: bolder; 150 | } 151 | 152 | small { 153 | font-size: 0.875em; 154 | } 155 | 156 | mark { 157 | padding: 0.2em; 158 | background-color: #fcf8e3; 159 | } 160 | 161 | sub, 162 | sup { 163 | position: relative; 164 | font-size: 0.75em; 165 | line-height: 0; 166 | vertical-align: baseline; 167 | } 168 | 169 | sub { 170 | bottom: -0.25em; 171 | } 172 | 173 | sup { 174 | top: -0.5em; 175 | } 176 | 177 | a { 178 | color: #0d6efd; 179 | text-decoration: underline; 180 | } 181 | a:hover { 182 | color: #0a58ca; 183 | } 184 | 185 | a:not([href]):not([class]), a:not([href]):not([class]):hover { 186 | color: inherit; 187 | text-decoration: none; 188 | } 189 | 190 | pre, 191 | code, 192 | kbd, 193 | samp { 194 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 195 | font-size: 1em; 196 | direction: ltr /* rtl:ignore */; 197 | unicode-bidi: bidi-override; 198 | } 199 | 200 | pre { 201 | display: block; 202 | margin-top: 0; 203 | margin-bottom: 1rem; 204 | overflow: auto; 205 | font-size: 0.875em; 206 | } 207 | pre code { 208 | font-size: inherit; 209 | color: inherit; 210 | word-break: normal; 211 | } 212 | 213 | code { 214 | font-size: 0.875em; 215 | color: #d63384; 216 | word-wrap: break-word; 217 | } 218 | a > code { 219 | color: inherit; 220 | } 221 | 222 | kbd { 223 | padding: 0.2rem 0.4rem; 224 | font-size: 0.875em; 225 | color: #fff; 226 | background-color: #212529; 227 | border-radius: 0.2rem; 228 | } 229 | kbd kbd { 230 | padding: 0; 231 | font-size: 1em; 232 | font-weight: 700; 233 | } 234 | 235 | figure { 236 | margin: 0 0 1rem; 237 | } 238 | 239 | img, 240 | svg { 241 | vertical-align: middle; 242 | } 243 | 244 | table { 245 | caption-side: bottom; 246 | border-collapse: collapse; 247 | } 248 | 249 | caption { 250 | padding-top: 0.5rem; 251 | padding-bottom: 0.5rem; 252 | color: #6c757d; 253 | text-align: left; 254 | } 255 | 256 | th { 257 | text-align: inherit; 258 | text-align: -webkit-match-parent; 259 | } 260 | 261 | thead, 262 | tbody, 263 | tfoot, 264 | tr, 265 | td, 266 | th { 267 | border-color: inherit; 268 | border-style: solid; 269 | border-width: 0; 270 | } 271 | 272 | label { 273 | display: inline-block; 274 | } 275 | 276 | button { 277 | border-radius: 0; 278 | } 279 | 280 | button:focus:not(:focus-visible) { 281 | outline: 0; 282 | } 283 | 284 | input, 285 | button, 286 | select, 287 | optgroup, 288 | textarea { 289 | margin: 0; 290 | font-family: inherit; 291 | font-size: inherit; 292 | line-height: inherit; 293 | } 294 | 295 | button, 296 | select { 297 | text-transform: none; 298 | } 299 | 300 | [role=button] { 301 | cursor: pointer; 302 | } 303 | 304 | select { 305 | word-wrap: normal; 306 | } 307 | select:disabled { 308 | opacity: 1; 309 | } 310 | 311 | [list]::-webkit-calendar-picker-indicator { 312 | display: none; 313 | } 314 | 315 | button, 316 | [type=button], 317 | [type=reset], 318 | [type=submit] { 319 | -webkit-appearance: button; 320 | } 321 | button:not(:disabled), 322 | [type=button]:not(:disabled), 323 | [type=reset]:not(:disabled), 324 | [type=submit]:not(:disabled) { 325 | cursor: pointer; 326 | } 327 | 328 | ::-moz-focus-inner { 329 | padding: 0; 330 | border-style: none; 331 | } 332 | 333 | textarea { 334 | resize: vertical; 335 | } 336 | 337 | fieldset { 338 | min-width: 0; 339 | padding: 0; 340 | margin: 0; 341 | border: 0; 342 | } 343 | 344 | legend { 345 | float: left; 346 | width: 100%; 347 | padding: 0; 348 | margin-bottom: 0.5rem; 349 | font-size: calc(1.275rem + 0.3vw); 350 | line-height: inherit; 351 | } 352 | @media (min-width: 1200px) { 353 | legend { 354 | font-size: 1.5rem; 355 | } 356 | } 357 | legend + * { 358 | clear: left; 359 | } 360 | 361 | ::-webkit-datetime-edit-fields-wrapper, 362 | ::-webkit-datetime-edit-text, 363 | ::-webkit-datetime-edit-minute, 364 | ::-webkit-datetime-edit-hour-field, 365 | ::-webkit-datetime-edit-day-field, 366 | ::-webkit-datetime-edit-month-field, 367 | ::-webkit-datetime-edit-year-field { 368 | padding: 0; 369 | } 370 | 371 | ::-webkit-inner-spin-button { 372 | height: auto; 373 | } 374 | 375 | [type=search] { 376 | outline-offset: -2px; 377 | -webkit-appearance: textfield; 378 | } 379 | 380 | /* rtl:raw: 381 | [type="tel"], 382 | [type="url"], 383 | [type="email"], 384 | [type="number"] { 385 | direction: ltr; 386 | } 387 | */ 388 | ::-webkit-search-decoration { 389 | -webkit-appearance: none; 390 | } 391 | 392 | ::-webkit-color-swatch-wrapper { 393 | padding: 0; 394 | } 395 | 396 | ::file-selector-button { 397 | font: inherit; 398 | } 399 | 400 | ::-webkit-file-upload-button { 401 | font: inherit; 402 | -webkit-appearance: button; 403 | } 404 | 405 | output { 406 | display: inline-block; 407 | } 408 | 409 | iframe { 410 | border: 0; 411 | } 412 | 413 | summary { 414 | display: list-item; 415 | cursor: pointer; 416 | } 417 | 418 | progress { 419 | vertical-align: baseline; 420 | } 421 | 422 | [hidden] { 423 | display: none !important; 424 | } 425 | 426 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "composer/package-versions-deprecated": { 3 | "version": "1.11.99.1" 4 | }, 5 | "doctrine/annotations": { 6 | "version": "1.0", 7 | "recipe": { 8 | "repo": "github.com/symfony/recipes", 9 | "branch": "master", 10 | "version": "1.0", 11 | "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" 12 | }, 13 | "files": [ 14 | "./config/routes/annotations.yaml" 15 | ] 16 | }, 17 | "doctrine/cache": { 18 | "version": "1.11.0" 19 | }, 20 | "doctrine/collections": { 21 | "version": "1.6.7" 22 | }, 23 | "doctrine/common": { 24 | "version": "3.1.2" 25 | }, 26 | "doctrine/dbal": { 27 | "version": "2.13.1" 28 | }, 29 | "doctrine/deprecations": { 30 | "version": "v0.5.3" 31 | }, 32 | "doctrine/doctrine-bundle": { 33 | "version": "2.3", 34 | "recipe": { 35 | "repo": "github.com/symfony/recipes", 36 | "branch": "master", 37 | "version": "2.3", 38 | "ref": "8a111cea2eeca8b427ae227bbbf35f368327a664" 39 | }, 40 | "files": [ 41 | "./config/packages/doctrine.yaml", 42 | "./config/packages/prod/doctrine.yaml", 43 | "./config/packages/test/doctrine.yaml", 44 | "./src/Entity/.gitignore", 45 | "./src/Repository/.gitignore" 46 | ] 47 | }, 48 | "doctrine/doctrine-migrations-bundle": { 49 | "version": "3.1", 50 | "recipe": { 51 | "repo": "github.com/symfony/recipes", 52 | "branch": "master", 53 | "version": "3.1", 54 | "ref": "ee609429c9ee23e22d6fa5728211768f51ed2818" 55 | }, 56 | "files": [ 57 | "./config/packages/doctrine_migrations.yaml", 58 | "./migrations/.gitignore" 59 | ] 60 | }, 61 | "doctrine/event-manager": { 62 | "version": "1.1.1" 63 | }, 64 | "doctrine/inflector": { 65 | "version": "2.0.3" 66 | }, 67 | "doctrine/instantiator": { 68 | "version": "1.4.0" 69 | }, 70 | "doctrine/lexer": { 71 | "version": "1.2.1" 72 | }, 73 | "doctrine/migrations": { 74 | "version": "3.1.2" 75 | }, 76 | "doctrine/orm": { 77 | "version": "2.8.4" 78 | }, 79 | "doctrine/persistence": { 80 | "version": "2.1.0" 81 | }, 82 | "doctrine/sql-formatter": { 83 | "version": "1.1.1" 84 | }, 85 | "egulias/email-validator": { 86 | "version": "3.1.1" 87 | }, 88 | "friendsofphp/proxy-manager-lts": { 89 | "version": "v1.0.3" 90 | }, 91 | "guzzlehttp/guzzle": { 92 | "version": "7.3.0" 93 | }, 94 | "guzzlehttp/promises": { 95 | "version": "1.4.1" 96 | }, 97 | "guzzlehttp/psr7": { 98 | "version": "1.8.2" 99 | }, 100 | "jms/metadata": { 101 | "version": "2.5.0" 102 | }, 103 | "jms/serializer": { 104 | "version": "3.12.3" 105 | }, 106 | "jms/serializer-bundle": { 107 | "version": "3.0", 108 | "recipe": { 109 | "repo": "github.com/symfony/recipes-contrib", 110 | "branch": "master", 111 | "version": "3.0", 112 | "ref": "384cec52df45f3bfd46a09930d6960a58872b268" 113 | }, 114 | "files": [ 115 | "./config/packages/dev/jms_serializer.yaml", 116 | "./config/packages/jms_serializer.yaml", 117 | "./config/packages/prod/jms_serializer.yaml" 118 | ] 119 | }, 120 | "laminas/laminas-code": { 121 | "version": "4.2.2" 122 | }, 123 | "laminas/laminas-eventmanager": { 124 | "version": "3.3.1" 125 | }, 126 | "laminas/laminas-zendframework-bridge": { 127 | "version": "1.2.0" 128 | }, 129 | "monolog/monolog": { 130 | "version": "2.2.0" 131 | }, 132 | "nikic/php-parser": { 133 | "version": "v4.10.5" 134 | }, 135 | "php": { 136 | "version": "7.4" 137 | }, 138 | "phpdocumentor/reflection-common": { 139 | "version": "2.2.0" 140 | }, 141 | "phpdocumentor/reflection-docblock": { 142 | "version": "5.2.2" 143 | }, 144 | "phpdocumentor/type-resolver": { 145 | "version": "1.4.0" 146 | }, 147 | "phpstan/phpdoc-parser": { 148 | "version": "0.5.4" 149 | }, 150 | "psr/cache": { 151 | "version": "1.0.1" 152 | }, 153 | "psr/container": { 154 | "version": "1.1.1" 155 | }, 156 | "psr/event-dispatcher": { 157 | "version": "1.0.0" 158 | }, 159 | "psr/http-client": { 160 | "version": "1.0.1" 161 | }, 162 | "psr/http-message": { 163 | "version": "1.0.1" 164 | }, 165 | "psr/link": { 166 | "version": "1.0.0" 167 | }, 168 | "psr/log": { 169 | "version": "1.1.4" 170 | }, 171 | "ralouphie/getallheaders": { 172 | "version": "3.0.3" 173 | }, 174 | "sensio/framework-extra-bundle": { 175 | "version": "5.2", 176 | "recipe": { 177 | "repo": "github.com/symfony/recipes", 178 | "branch": "master", 179 | "version": "5.2", 180 | "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b" 181 | }, 182 | "files": [ 183 | "./config/packages/sensio_framework_extra.yaml" 184 | ] 185 | }, 186 | "symfony/asset": { 187 | "version": "v5.2.7" 188 | }, 189 | "symfony/browser-kit": { 190 | "version": "v5.2.7" 191 | }, 192 | "symfony/cache": { 193 | "version": "v5.2.7" 194 | }, 195 | "symfony/cache-contracts": { 196 | "version": "v2.4.0" 197 | }, 198 | "symfony/config": { 199 | "version": "v5.2.7" 200 | }, 201 | "symfony/console": { 202 | "version": "5.1", 203 | "recipe": { 204 | "repo": "github.com/symfony/recipes", 205 | "branch": "master", 206 | "version": "5.1", 207 | "ref": "c6d02bdfba9da13c22157520e32a602dbee8a75c" 208 | }, 209 | "files": [ 210 | "./bin/console" 211 | ] 212 | }, 213 | "symfony/css-selector": { 214 | "version": "v5.2.7" 215 | }, 216 | "symfony/debug-bundle": { 217 | "version": "4.1", 218 | "recipe": { 219 | "repo": "github.com/symfony/recipes", 220 | "branch": "master", 221 | "version": "4.1", 222 | "ref": "0ce7a032d344fb7b661cd25d31914cd703ad445b" 223 | }, 224 | "files": [ 225 | "./config/packages/dev/debug.yaml" 226 | ] 227 | }, 228 | "symfony/debug-pack": { 229 | "version": "v1.0.9" 230 | }, 231 | "symfony/dependency-injection": { 232 | "version": "v5.2.7" 233 | }, 234 | "symfony/deprecation-contracts": { 235 | "version": "v2.4.0" 236 | }, 237 | "symfony/doctrine-bridge": { 238 | "version": "v5.2.7" 239 | }, 240 | "symfony/dom-crawler": { 241 | "version": "v5.2.4" 242 | }, 243 | "symfony/dotenv": { 244 | "version": "v5.2.4" 245 | }, 246 | "symfony/error-handler": { 247 | "version": "v5.2.7" 248 | }, 249 | "symfony/event-dispatcher": { 250 | "version": "v5.2.4" 251 | }, 252 | "symfony/event-dispatcher-contracts": { 253 | "version": "v2.4.0" 254 | }, 255 | "symfony/expression-language": { 256 | "version": "v5.2.7" 257 | }, 258 | "symfony/filesystem": { 259 | "version": "v5.2.7" 260 | }, 261 | "symfony/finder": { 262 | "version": "v5.2.4" 263 | }, 264 | "symfony/flex": { 265 | "version": "1.0", 266 | "recipe": { 267 | "repo": "github.com/symfony/recipes", 268 | "branch": "master", 269 | "version": "1.0", 270 | "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" 271 | }, 272 | "files": [ 273 | "./.env" 274 | ] 275 | }, 276 | "symfony/form": { 277 | "version": "v5.2.7" 278 | }, 279 | "symfony/framework-bundle": { 280 | "version": "5.2", 281 | "recipe": { 282 | "repo": "github.com/symfony/recipes", 283 | "branch": "master", 284 | "version": "5.2", 285 | "ref": "6ec87563dcc85cd0c48856dcfbfc29610506d250" 286 | }, 287 | "files": [ 288 | "./config/packages/cache.yaml", 289 | "./config/packages/framework.yaml", 290 | "./config/packages/test/framework.yaml", 291 | "./config/preload.php", 292 | "./config/routes/dev/framework.yaml", 293 | "./config/services.yaml", 294 | "./public/index.php", 295 | "./src/Controller/.gitignore", 296 | "./src/Kernel.php" 297 | ] 298 | }, 299 | "symfony/http-client": { 300 | "version": "v5.2.7" 301 | }, 302 | "symfony/http-client-contracts": { 303 | "version": "v2.4.0" 304 | }, 305 | "symfony/http-foundation": { 306 | "version": "v5.2.7" 307 | }, 308 | "symfony/http-kernel": { 309 | "version": "v5.2.7" 310 | }, 311 | "symfony/intl": { 312 | "version": "v5.2.7" 313 | }, 314 | "symfony/mailer": { 315 | "version": "4.3", 316 | "recipe": { 317 | "repo": "github.com/symfony/recipes", 318 | "branch": "master", 319 | "version": "4.3", 320 | "ref": "15658c2a0176cda2e7dba66276a2030b52bd81b2" 321 | }, 322 | "files": [ 323 | "./config/packages/mailer.yaml" 324 | ] 325 | }, 326 | "symfony/maker-bundle": { 327 | "version": "1.0", 328 | "recipe": { 329 | "repo": "github.com/symfony/recipes", 330 | "branch": "master", 331 | "version": "1.0", 332 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" 333 | } 334 | }, 335 | "symfony/mime": { 336 | "version": "v5.2.7" 337 | }, 338 | "symfony/monolog-bridge": { 339 | "version": "v5.2.7" 340 | }, 341 | "symfony/monolog-bundle": { 342 | "version": "3.7", 343 | "recipe": { 344 | "repo": "github.com/symfony/recipes", 345 | "branch": "master", 346 | "version": "3.7", 347 | "ref": "329f6a5ef2e7aa033f802be833ef8d1268dd0848" 348 | }, 349 | "files": [ 350 | "./config/packages/dev/monolog.yaml", 351 | "./config/packages/prod/deprecations.yaml", 352 | "./config/packages/prod/monolog.yaml", 353 | "./config/packages/test/monolog.yaml" 354 | ] 355 | }, 356 | "symfony/notifier": { 357 | "version": "5.0", 358 | "recipe": { 359 | "repo": "github.com/symfony/recipes", 360 | "branch": "master", 361 | "version": "5.0", 362 | "ref": "c31585e252b32fe0e1f30b1f256af553f4a06eb9" 363 | }, 364 | "files": [ 365 | "./config/packages/notifier.yaml" 366 | ] 367 | }, 368 | "symfony/options-resolver": { 369 | "version": "v5.2.4" 370 | }, 371 | "symfony/orm-pack": { 372 | "version": "v2.1.0" 373 | }, 374 | "symfony/phpunit-bridge": { 375 | "version": "5.1", 376 | "recipe": { 377 | "repo": "github.com/symfony/recipes", 378 | "branch": "master", 379 | "version": "5.1", 380 | "ref": "bf16921ef8309a81d9f046e9b6369c46bcbd031f" 381 | }, 382 | "files": [ 383 | "./.env.test", 384 | "./bin/phpunit", 385 | "./phpunit.xml.dist", 386 | "./tests/bootstrap.php" 387 | ] 388 | }, 389 | "symfony/polyfill-intl-grapheme": { 390 | "version": "v1.22.1" 391 | }, 392 | "symfony/polyfill-intl-icu": { 393 | "version": "v1.22.1" 394 | }, 395 | "symfony/polyfill-intl-idn": { 396 | "version": "v1.22.1" 397 | }, 398 | "symfony/polyfill-intl-normalizer": { 399 | "version": "v1.22.1" 400 | }, 401 | "symfony/polyfill-mbstring": { 402 | "version": "v1.22.1" 403 | }, 404 | "symfony/polyfill-php73": { 405 | "version": "v1.22.1" 406 | }, 407 | "symfony/polyfill-php80": { 408 | "version": "v1.22.1" 409 | }, 410 | "symfony/process": { 411 | "version": "v5.2.7" 412 | }, 413 | "symfony/profiler-pack": { 414 | "version": "v1.0.5" 415 | }, 416 | "symfony/property-access": { 417 | "version": "v5.2.4" 418 | }, 419 | "symfony/property-info": { 420 | "version": "v5.2.7" 421 | }, 422 | "symfony/proxy-manager-bridge": { 423 | "version": "v5.2.4" 424 | }, 425 | "symfony/routing": { 426 | "version": "5.1", 427 | "recipe": { 428 | "repo": "github.com/symfony/recipes", 429 | "branch": "master", 430 | "version": "5.1", 431 | "ref": "b4f3e7c95e38b606eef467e8a42a8408fc460c43" 432 | }, 433 | "files": [ 434 | "./config/packages/prod/routing.yaml", 435 | "./config/packages/routing.yaml", 436 | "./config/routes.yaml" 437 | ] 438 | }, 439 | "symfony/security-bundle": { 440 | "version": "5.1", 441 | "recipe": { 442 | "repo": "github.com/symfony/recipes", 443 | "branch": "master", 444 | "version": "5.1", 445 | "ref": "0a4bae19389d3b9cba1ca0102e3b2bccea724603" 446 | }, 447 | "files": [ 448 | "./config/packages/security.yaml" 449 | ] 450 | }, 451 | "symfony/security-core": { 452 | "version": "v5.2.7" 453 | }, 454 | "symfony/security-csrf": { 455 | "version": "v5.2.7" 456 | }, 457 | "symfony/security-guard": { 458 | "version": "v5.2.4" 459 | }, 460 | "symfony/security-http": { 461 | "version": "v5.2.7" 462 | }, 463 | "symfony/serializer": { 464 | "version": "v5.2.7" 465 | }, 466 | "symfony/serializer-pack": { 467 | "version": "v1.0.4" 468 | }, 469 | "symfony/service-contracts": { 470 | "version": "v2.4.0" 471 | }, 472 | "symfony/stopwatch": { 473 | "version": "v5.2.7" 474 | }, 475 | "symfony/string": { 476 | "version": "v5.2.6" 477 | }, 478 | "symfony/test-pack": { 479 | "version": "v1.0.7" 480 | }, 481 | "symfony/translation": { 482 | "version": "3.3", 483 | "recipe": { 484 | "repo": "github.com/symfony/recipes", 485 | "branch": "master", 486 | "version": "3.3", 487 | "ref": "2ad9d2545bce8ca1a863e50e92141f0b9d87ffcd" 488 | }, 489 | "files": [ 490 | "./config/packages/translation.yaml", 491 | "./translations/.gitignore" 492 | ] 493 | }, 494 | "symfony/translation-contracts": { 495 | "version": "v2.4.0" 496 | }, 497 | "symfony/twig-bridge": { 498 | "version": "v5.2.7" 499 | }, 500 | "symfony/twig-bundle": { 501 | "version": "5.0", 502 | "recipe": { 503 | "repo": "github.com/symfony/recipes", 504 | "branch": "master", 505 | "version": "5.0", 506 | "ref": "fab9149bbaa4d5eca054ed93f9e1b66cc500895d" 507 | }, 508 | "files": [ 509 | "./config/packages/test/twig.yaml", 510 | "./config/packages/twig.yaml", 511 | "./templates/base.html.twig" 512 | ] 513 | }, 514 | "symfony/twig-pack": { 515 | "version": "v1.0.1" 516 | }, 517 | "symfony/validator": { 518 | "version": "4.3", 519 | "recipe": { 520 | "repo": "github.com/symfony/recipes", 521 | "branch": "master", 522 | "version": "4.3", 523 | "ref": "d902da3e4952f18d3bf05aab29512eb61cabd869" 524 | }, 525 | "files": [ 526 | "./config/packages/test/validator.yaml", 527 | "./config/packages/validator.yaml" 528 | ] 529 | }, 530 | "symfony/var-dumper": { 531 | "version": "v5.2.7" 532 | }, 533 | "symfony/var-exporter": { 534 | "version": "v5.2.7" 535 | }, 536 | "symfony/web-link": { 537 | "version": "v5.2.5" 538 | }, 539 | "symfony/web-profiler-bundle": { 540 | "version": "3.3", 541 | "recipe": { 542 | "repo": "github.com/symfony/recipes", 543 | "branch": "master", 544 | "version": "3.3", 545 | "ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6" 546 | }, 547 | "files": [ 548 | "./config/packages/dev/web_profiler.yaml", 549 | "./config/packages/test/web_profiler.yaml", 550 | "./config/routes/dev/web_profiler.yaml" 551 | ] 552 | }, 553 | "symfony/yaml": { 554 | "version": "v5.2.7" 555 | }, 556 | "twbs/bootstrap": { 557 | "version": "v5.0.0" 558 | }, 559 | "twig/extra-bundle": { 560 | "version": "v3.3.0" 561 | }, 562 | "twig/twig": { 563 | "version": "v3.3.0" 564 | }, 565 | "webmozart/assert": { 566 | "version": "1.10.0" 567 | } 568 | } 569 | -------------------------------------------------------------------------------- /public/assets/lib/bootstrap/css/bootstrap-reboot.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../../scss/bootstrap-reboot.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss"],"names":[],"mappings":"AAAA;;;;;;ACeA,ECNA,QADA,SDUE,WAAA,WAaE,8CAJJ,MAKM,gBAAA,QAaN,KACE,OAAA,EACA,YAAA,SAAA,CAAA,aAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,iBAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBE4MI,UAAA,KF1MJ,YAAA,IACA,YAAA,IACA,MAAA,QAEA,iBAAA,KACA,yBAAA,KACA,4BAAA,YASF,GACE,OAAA,KAAA,EACA,MAAA,QACA,iBAAA,aACA,OAAA,EACA,QAAA,IAGF,eACE,OAAA,IAUF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAGA,YAAA,IACA,YAAA,IAIF,GEkKQ,UAAA,uBAlKJ,0BFAJ,GEyKQ,UAAA,QFpKR,GE6JQ,UAAA,sBAlKJ,0BFKJ,GEoKQ,UAAA,MF/JR,GEwJQ,UAAA,oBAlKJ,0BFUJ,GE+JQ,UAAA,SF1JR,GEmJQ,UAAA,sBAlKJ,0BFeJ,GE0JQ,UAAA,QFrJR,GE0IM,UAAA,QFrIN,GEqIM,UAAA,KF1HN,EACE,WAAA,EACA,cAAA,KC/BF,6BD0CA,YAEE,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,iCAAA,KAAA,yBAAA,KAMF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QAMF,GC9CA,GDgDE,aAAA,KC1CF,GD6CA,GC9CA,GDiDE,WAAA,EACA,cAAA,KAGF,MC7CA,MACA,MAFA,MDkDE,cAAA,EAGF,GACE,YAAA,IAKF,GACE,cAAA,MACA,YAAA,EAMF,WACE,OAAA,EAAA,EAAA,KAQF,ECxDA,OD0DE,YAAA,OAQF,MEsCM,UAAA,OF/BN,KACE,QAAA,KACA,iBAAA,QASF,ICtEA,IDwEE,SAAA,SEkBI,UAAA,MFhBJ,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAKN,EACE,MAAA,QACA,gBAAA,UAEA,QACE,MAAA,QAWF,2BAAA,iCAEE,MAAA,QACA,gBAAA,KC1EJ,KACA,IDgFA,IC/EA,KDmFE,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UExBI,UAAA,IF0BJ,UAAA,IACA,aAAA,cAOF,IACE,QAAA,MACA,WAAA,EACA,cAAA,KACA,SAAA,KEtCI,UAAA,OF2CJ,SE3CI,UAAA,QF6CF,MAAA,QACA,WAAA,OAIJ,KElDM,UAAA,OFoDJ,MAAA,QACA,UAAA,WAGA,OACE,MAAA,QAIJ,IACE,QAAA,MAAA,ME9DI,UAAA,OFgEJ,MAAA,KACA,iBAAA,QGzSE,cAAA,MH4SF,QACE,QAAA,EErEE,UAAA,IFuEF,YAAA,IASJ,OACE,OAAA,EAAA,EAAA,KAMF,ICnGA,IDqGE,eAAA,OAQF,MACE,aAAA,OACA,gBAAA,SAGF,QACE,YAAA,MACA,eAAA,MACA,MAAA,QACA,WAAA,KAOF,GAEE,WAAA,QACA,WAAA,qBC1GF,MAGA,GAFA,MAGA,GDyGA,MC3GA,GDiHE,aAAA,QACA,aAAA,MACA,aAAA,EAQF,MACE,QAAA,aAMF,OAEE,cAAA,EAQF,iCACE,QAAA,ECxHF,OD6HA,MC3HA,SADA,OAEA,SD+HE,OAAA,EACA,YAAA,QEpKI,UAAA,QFsKJ,YAAA,QAIF,OC9HA,ODgIE,eAAA,KAKF,cACE,OAAA,QAGF,OAGE,UAAA,OAGA,gBACE,QAAA,EAOJ,0CACE,QAAA,KCpIF,cACA,aACA,cD0IA,OAIE,mBAAA,OC1IF,6BACA,4BACA,6BD2II,sBACE,OAAA,QAON,mBACE,QAAA,EACA,aAAA,KAKF,SACE,OAAA,SAUF,SACE,UAAA,EACA,QAAA,EACA,OAAA,EACA,OAAA,EAQF,OACE,MAAA,KACA,MAAA,KACA,QAAA,EACA,cAAA,MEzPM,UAAA,sBF4PN,YAAA,QE9ZE,0BFuZJ,OE9OQ,UAAA,QFuPN,SACE,MAAA,KClJJ,kCDyJA,uCC1JA,mCADA,+BAGA,oCAJA,6BAKA,mCD8JE,QAAA,EAGF,4BACE,OAAA,KASF,cACE,eAAA,KACA,mBAAA,UAmBF,4BACE,mBAAA,KAKF,+BACE,QAAA,EAMF,uBACE,KAAA,QAMF,6BACE,KAAA,QACA,mBAAA,OAKF,OACE,QAAA,aAKF,OACE,OAAA,EAOF,QACE,QAAA,UACA,OAAA,QAQF,SACE,eAAA,SAQF,SACE,QAAA","sourcesContent":["/*!\n * Bootstrap Reboot v5.0.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n// Prevent the usage of custom properties since we don't add them to `:root` in reboot\n$font-family-base: $font-family-sans-serif; // stylelint-disable-line scss/dollar-variable-default\n$font-family-code: $font-family-monospace; // stylelint-disable-line scss/dollar-variable-default\n@import \"mixins\";\n@import \"reboot\";\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n font-size: $font-size-root;\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: $body-text-align;\n background-color: $body-bg; // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`