├── .env ├── .env.integration ├── .env.test ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── quality.yaml │ └── test.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── assets ├── images │ └── .gitkeep ├── js │ └── app.js └── scss │ ├── abstracts │ ├── .gitkeep │ ├── _alerts.scss │ ├── _animations.scss │ ├── _breakpoints.scss │ ├── _buttons.scss │ ├── _colors.scss │ ├── _fields.scss │ ├── _flash_bag.scss │ ├── _flex.scss │ ├── _grid.scss │ ├── _icons.scss │ ├── _layout.scss │ ├── _list_groups.scss │ ├── _responsive.scss │ ├── _round.scss │ ├── _shadows.scss │ └── _texts.scss │ ├── base │ ├── .gitkeep │ ├── _container.scss │ ├── _flex.scss │ ├── _grid.scss │ ├── _shadow.scss │ └── _text.scss │ ├── components │ ├── .gitkeep │ ├── _alert.scss │ ├── _button.scss │ ├── _card.scss │ ├── _field.scss │ ├── _flash_bag.scss │ └── _list_group.scss │ ├── layout │ ├── .gitkeep │ ├── _footer.scss │ ├── _global.scss │ ├── _header.scss │ ├── _main.scss │ └── _main_nav.scss │ ├── main.scss │ ├── pages │ └── .gitkeep │ ├── themes │ └── .gitkeep │ └── vendors │ ├── .gitkeep │ ├── _font_awesome.scss │ ├── _normalize.scss │ └── _roboto.scss ├── bin ├── console └── phpunit ├── composer.json ├── composer.lock ├── config ├── bootstrap.php ├── bundles.php ├── packages │ ├── assets.yaml │ ├── cache.yaml │ ├── dev │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── enqueue.yaml │ ├── framework.yaml │ ├── integration │ │ ├── dama_doctrine_test_bundle.yaml │ │ ├── framework.yaml │ │ ├── twig.yaml │ │ ├── validator.yaml │ │ └── webpack_encore.yaml │ ├── messenger.yaml │ ├── prod │ │ ├── doctrine.yaml │ │ ├── messenger.yaml │ │ ├── routing.yaml │ │ └── webpack_encore.yaml │ ├── ramsey_uuid_doctrine.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── sensio_framework_extra.yaml │ ├── test │ │ ├── dama_doctrine_test_bundle.yaml │ │ ├── enqueue.yaml │ │ ├── framework.yaml │ │ ├── twig.yaml │ │ ├── validator.yaml │ │ ├── web_profiler.yaml │ │ └── webpack_encore.yaml │ ├── twig.yaml │ ├── validator.yaml │ └── webpack_encore.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ └── dev │ │ ├── framework.yaml │ │ └── web_profiler.yaml ├── services.yaml └── services_integration.yaml ├── docs ├── front.md ├── img │ ├── alert_danger.png │ ├── alert_dark.png │ ├── alert_icons.png │ ├── alert_info.png │ ├── alert_light.png │ ├── alert_primary.png │ ├── alert_secondary.png │ ├── alert_success.png │ ├── alert_warning.png │ ├── authentication.png │ ├── button_danger.png │ ├── button_dark.png │ ├── button_icon.png │ ├── button_info.png │ ├── button_light.png │ ├── button_outline.png │ ├── button_primary.png │ ├── button_secondary.png │ ├── button_sizes.png │ ├── button_success.png │ ├── button_warning.png │ ├── card.png │ ├── card_danger.png │ ├── card_dark.png │ ├── card_info.png │ ├── card_light.png │ ├── card_primary.png │ ├── card_secondary.png │ ├── card_success.png │ ├── card_warning.png │ ├── clean_architecture.jpg │ ├── dashboard.png │ ├── grid.png │ ├── grid_lg.png │ ├── grid_md.png │ ├── grid_sm.png │ ├── grid_xl.png │ ├── grid_xs.png │ ├── manage_questions.png │ ├── manage_users.png │ ├── packages.png │ ├── ranking.png │ ├── registration.png │ ├── reply_quiz.png │ ├── request.png │ ├── sign_out.png │ ├── sitemap.png │ ├── update_password.png │ └── update_profile.png ├── index.md ├── tdd.md └── uml │ ├── authentication.puml │ ├── dashboard.puml │ ├── manage_questions.puml │ ├── manage_users.puml │ ├── packages.puml │ ├── ranking.puml │ ├── registration.puml │ ├── reply_quiz.puml │ ├── request.puml │ ├── sign_out.puml │ ├── sitemap.puml │ ├── update_password.puml │ └── update_profile.puml ├── domain ├── src │ ├── Quiz │ │ ├── Entity │ │ │ ├── Answer.php │ │ │ └── Question.php │ │ ├── Gateway │ │ │ └── QuestionGateway.php │ │ ├── Presenter │ │ │ ├── CreatePresenterInterface.php │ │ │ └── UpdatePresenterInterface.php │ │ ├── Request │ │ │ ├── CreateRequest.php │ │ │ └── UpdateRequest.php │ │ ├── Response │ │ │ ├── CreateResponse.php │ │ │ └── UpdateResponse.php │ │ └── UseCase │ │ │ ├── Create.php │ │ │ └── Update.php │ ├── Security │ │ ├── Assert │ │ │ └── Assertion.php │ │ ├── Entity │ │ │ └── Participant.php │ │ ├── Exception │ │ │ ├── NonUniqueEmailException.php │ │ │ ├── NonUniquePseudoException.php │ │ │ ├── ParticipantNotFoundException.php │ │ │ └── PasswordRecoveryInvalidTokenException.php │ │ ├── Gateway │ │ │ └── ParticipantGateway.php │ │ ├── Presenter │ │ │ ├── AskPasswordResetPresenterInterface.php │ │ │ ├── LoginPresenterInterface.php │ │ │ ├── RecoverPasswordPresenterInterface.php │ │ │ └── RegistrationPresenterInterface.php │ │ ├── Provider │ │ │ └── MailProviderInterface.php │ │ ├── Request │ │ │ ├── AskPasswordResetRequest.php │ │ │ ├── LoginRequest.php │ │ │ ├── RecoverPasswordRequest.php │ │ │ └── RegistrationRequest.php │ │ ├── Response │ │ │ ├── AskPasswordResetResponse.php │ │ │ ├── LoginResponse.php │ │ │ ├── RecoverPasswordResponse.php │ │ │ └── RegistrationResponse.php │ │ └── UseCase │ │ │ ├── AskPasswordReset.php │ │ │ ├── Login.php │ │ │ ├── RecoverPassword.php │ │ │ └── Registration.php │ └── System │ │ ├── Entity │ │ └── Log.php │ │ ├── Gateway │ │ └── LogGateway.php │ │ ├── Presenter │ │ └── TrackPresenterInterface.php │ │ ├── Request │ │ └── TrackRequest.php │ │ ├── Response │ │ └── TrackResponse.php │ │ └── UseCase │ │ └── Track.php └── tests │ ├── Fixtures │ └── Adapter │ │ ├── LogRepository.php │ │ ├── ParticipantRepository.php │ │ └── QuestionRepository.php │ ├── Quiz │ ├── CreateTest.php │ └── UpdateTest.php │ ├── Security │ ├── AskPasswordResetTest.php │ ├── LoginTest.php │ ├── RecoverPasswordTest.php │ └── RegistrationTest.php │ ├── System │ └── TrackTest.php │ └── bootstrap.php ├── package-lock.json ├── package.json ├── phpcs.xml.dist ├── phpunit.xml.dist ├── public └── index.php ├── src ├── Infrastructure │ ├── Adapter │ │ ├── Provider │ │ │ └── MailProvider.php │ │ └── Repository │ │ │ ├── LogRepository.php │ │ │ ├── ParticipantRepository.php │ │ │ └── QuestionRepository.php │ ├── Doctrine │ │ ├── DataFixtures │ │ │ ├── QuestionFixtures.php │ │ │ └── UserFixtures.php │ │ ├── Entity │ │ │ ├── .gitignore │ │ │ ├── DoctrineAnswer.php │ │ │ ├── DoctrineLog.php │ │ │ ├── DoctrineParticipant.php │ │ │ └── DoctrineQuestion.php │ │ └── Migrations │ │ │ └── .gitignore │ ├── EventSubscriber │ │ └── KernelSubscriber.php │ ├── Maker │ │ └── MakeUseCase.php │ ├── ParamConverter │ │ └── QuestionConverter.php │ ├── Resources │ │ └── skeleton │ │ │ ├── presenter.tpl.php │ │ │ ├── request.tpl.php │ │ │ ├── response.tpl.php │ │ │ ├── test.tpl.php │ │ │ └── use_case.tpl.php │ ├── Security │ │ ├── Guard │ │ │ └── WebAuthenticator.php │ │ ├── Provider │ │ │ └── UserProvider.php │ │ └── User.php │ ├── Test │ │ ├── Adapter │ │ │ ├── MailProvider.php │ │ │ └── Repository │ │ │ │ ├── LogRepository.php │ │ │ │ ├── ParticipantRepository.php │ │ │ │ └── QuestionRepository.php │ │ └── IntegrationTestCase.php │ └── Validator │ │ ├── NonUniqueEmail.php │ │ ├── NonUniqueEmailValidator.php │ │ ├── NonUniquePseudo.php │ │ └── NonUniquePseudoValidator.php ├── Kernel.php └── UserInterface │ ├── Controller │ ├── .gitkeep │ ├── LoginController.php │ ├── LogoutController.php │ ├── Question │ │ ├── CreateController.php │ │ └── UpdateController.php │ ├── RegistrationController.php │ └── Security │ │ ├── AskPasswordResetController.php │ │ └── RecoverPasswordController.php │ ├── DataTransferObject │ ├── Answer.php │ ├── Question.php │ ├── RecoverPasswordData.php │ ├── Registration.php │ └── ResetPasswordData.php │ ├── Form │ ├── AnswerType.php │ ├── QuestionType.php │ ├── RecoverPasswordType.php │ ├── RegistrationType.php │ └── ResetPasswordType.php │ ├── MessageHandler │ └── TrackHandler.php │ ├── Presenter │ ├── .gitkeep │ ├── Question │ │ ├── CreatePresenter.php │ │ └── UpdatePresenter.php │ ├── RegistrationPresenter.php │ ├── Security │ │ ├── AskPasswordResetPresenter.php │ │ └── RecoverPasswordPresenter.php │ └── TrackPresenter.php │ └── ViewModel │ ├── .gitkeep │ ├── LoginViewModel.php │ ├── RegistrationViewModel.php │ └── Security │ └── AskPasswordResetViewModel.php ├── symfony.lock ├── templates ├── base.html.twig ├── components │ ├── flash_messages.html.twig │ └── form.html.twig ├── emails │ └── password_reset_request.html.twig ├── home.html.twig ├── login.html.twig ├── question │ ├── _form.html.twig │ ├── _prototype.html.twig │ ├── create.html.twig │ └── update.html.twig ├── registration.html.twig └── security │ ├── change_password.html.twig │ └── reset_password.html.twig ├── tests ├── EndToEndTests │ ├── ParticipantTest.php │ └── VisitorTest.php ├── IntegrationTests │ ├── AskPasswordResetTest.php │ ├── CreateQuestionTest.php │ ├── LoginTest.php │ ├── RecoverPasswordTest.php │ ├── RegistrationTest.php │ └── UpdateQuestionTest.php ├── SystemTests │ ├── AskPasswordResetTest.php │ ├── CreateQuestionTest.php │ ├── LoginTest.php │ ├── RecoverPasswordTest.php │ ├── RegistrationTest.php │ └── UpdateQuestionTest.php └── bootstrap.php ├── tsconfig.json └── webpack.config.js /.env: -------------------------------------------------------------------------------- 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=7ea6cff520c89e6e0653a832b6382ade 19 | #TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 20 | #TRUSTED_HOSTS='^(localhost|example\.com)$' 21 | ###< symfony/framework-bundle ### 22 | 23 | ###> doctrine/doctrine-bundle ### 24 | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 25 | # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" 26 | # For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8" 27 | # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml 28 | DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7 29 | ###< doctrine/doctrine-bundle ### 30 | 31 | ###> symfony/messenger ### 32 | # Choose one of the transports below 33 | # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages 34 | # MESSENGER_TRANSPORT_DSN=doctrine://default 35 | # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages 36 | ###< symfony/messenger ### 37 | 38 | ###> enqueue/enqueue-bundle ### 39 | ENQUEUE_DSN=null:// 40 | ###< enqueue/enqueue-bundle ### 41 | -------------------------------------------------------------------------------- /.env.integration: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/quality.yaml: -------------------------------------------------------------------------------- 1 | name: Code quality 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | phpcs: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Validate composer.json and composer.lock 14 | run: composer validate 15 | 16 | - name: Install dependencies 17 | run: composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader 18 | 19 | - name: Detecting PHP Code Standards Violations 20 | run: vendor/bin/phpcs --standard=PSR12 tests src domain 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Testing project 2 | on: [push, pull_request] 3 | jobs: 4 | symfony: 5 | name: Symfony (PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }}) 6 | runs-on: ${{ matrix.operating-system }} 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | operating-system: [ubuntu-latest] 11 | php-versions: ['7.4'] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Setup PHP, with composer and extensions 16 | uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php 17 | with: 18 | php-version: ${{ matrix.php-versions }} 19 | extensions: mbstring, xml, ctype, iconv, intl 20 | coverage: xdebug #optional 21 | - name: Get composer cache directory 22 | id: composer-cache 23 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 24 | - name: Cache composer dependencies 25 | uses: actions/cache@v1 26 | with: 27 | path: ${{ steps.composer-cache.outputs.dir }} 28 | # Use composer.json for key, if composer.lock is not committed. 29 | # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 30 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 31 | restore-keys: ${{ runner.os }}-composer- 32 | - name: Install Composer dependencies 33 | run: | 34 | composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader 35 | - name: Run Tests 36 | run: php bin/phpunit --testsuite unit,integration --coverage-text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | /.env.local 3 | /.env.local.php 4 | /.env.*.local 5 | /config/secrets/prod/prod.decrypt.private.php 6 | /public/bundles/ 7 | /var/ 8 | /vendor/ 9 | ###< symfony/framework-bundle ### 10 | 11 | ###> symfony/phpunit-bridge ### 12 | .phpunit 13 | .phpunit.result.cache 14 | /phpunit.xml 15 | ###< symfony/phpunit-bridge ### 16 | 17 | ###> phpunit/phpunit ### 18 | /phpunit.xml 19 | .phpunit.result.cache 20 | ###< phpunit/phpunit ### 21 | /.idea 22 | /build 23 | .php_cs.cache 24 | *.sh 25 | debug.log 26 | ###> squizlabs/php_codesniffer ### 27 | /.phpcs-cache 28 | /phpcs.xml 29 | ###< squizlabs/php_codesniffer ### 30 | 31 | ###> symfony/webpack-encore-bundle ### 32 | /node_modules/ 33 | /public/build/ 34 | npm-debug.log 35 | yarn-error.log 36 | ###< symfony/webpack-encore-bundle ### 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Thomas Boileau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | unit-tests: 2 | bin/phpunit --testsuite unit 3 | 4 | integration-tests: 5 | bin/phpunit --testsuite integration 6 | 7 | system-tests: 8 | composer database-test 9 | bin/phpunit --testsuite system 10 | 11 | e2e-tests: 12 | composer database-panther 13 | bin/phpunit --testsuite end_to_end 14 | 15 | .PHONY: tests 16 | tests: 17 | composer database 18 | bin/phpunit --testsuite unit,integration,system,end_to_end 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CODE CHALLENGE 2 | ============== 3 | 4 | Rejoignez-nous sur : 5 | * [Twitch](https://www.twitch.tv/toham) 6 | * [Youtube](https://www.youtube.com/c/ThomasBoileau) 7 | * [Discord](https://discord.gg/AMd6d4a) 8 | 9 | # Sommaire 10 | * [Concept](#concept) 11 | * [Participer](#comment-participer-au-développement-) 12 | * [Projet](#projet) 13 | * [Contribution](CONTRIBUTING.md) 14 | * [Documentation](docs/index.md) 15 | 16 | # Concept 17 | **Code challenge** est une application web, qui a pour objectif de proposer aux utilisateurs inscrits de s'entraîner sur **PHP** et **Symfony**. 18 | 19 | L'objectif est de proposer un projet communautaire, ou chacun sera libre de participer à la conception de l'application. 20 | 21 | L'intérêt est de monter en compétence sur **PHP** et **Symfony** ensemble, autour d'un projet fédérateur. Ce n'es pas tout, le but à long terme est de passer la certification **Symfony**. 22 | Quoi de plus vertueux que de monter en compétence en concevant ensemble une application pour s'entraîner sur **Symfony**, pour enfin décrocher la certification. 23 | 24 | Le projet vous tente ? Rejoignez-nous sur Twitch [www.twitch.tv/toham](https://www.twitch.tv/toham) ! 25 | 26 | # Comment participer au développement ? 27 | Tout d'abord, vous n'avez aucune obligation, cependant si vous vous engagez pour implémenter une fonctionnalité, on compte sur vous ! 28 | 29 | *Je veux participer au projet, comment je fais ?* C'est très simple, rejoignez nous sur [discord](https://discord.gg/AMd6d4a), et contactez-moi en m'envoyant un message avec votre identifiant **Github** pour que je puisse vous ajouter en tant que collaborateur. 30 | 31 | # Projet 32 | Comme je l'ai expliqué plus haut, le but de se projet est de monter en compétence,pas seulement sur **PHP** et **Symfony**, mais aussi sur l'architecture logiciel comme la **Clean architecture**, en suivant les bonnes pratiques. 33 | 34 | Plus concrètement, nous allons mettre en place : 35 | * Clean architecture 36 | * **T**est **D**riven **D**evelopment 37 | 38 | Je vous invite à lire le fichier de [contribution](CONTRIBUTION.md); 39 | 40 | Pour en savoir plus, jetez un oeil à la [documentation](docs/index.md) 41 | 42 | # Crédits 43 | 44 | * [Thomas Boileau](https://github.com/TBoileau) 45 | -------------------------------------------------------------------------------- /assets/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/assets/images/.gitkeep -------------------------------------------------------------------------------- /assets/scss/abstracts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/assets/scss/abstracts/.gitkeep -------------------------------------------------------------------------------- /assets/scss/abstracts/_alerts.scss: -------------------------------------------------------------------------------- 1 | $alert-colors: ( 2 | primary: ( 3 | color: #004085, 4 | background: #cce5ff, 5 | border: #b8daff 6 | ), 7 | secondary: ( 8 | color: #383d41, 9 | background: #e2e3e5, 10 | border: #d6d8db 11 | ), 12 | success: ( 13 | color: #155724, 14 | background: #d4edda, 15 | border: #c3e6cb 16 | ), 17 | danger: ( 18 | color: #721c24, 19 | background: #f8d7da, 20 | border: #f5c6cb 21 | ), 22 | warning: ( 23 | color: #856404, 24 | background: #fff3cd, 25 | border: #ffeeba 26 | ), 27 | info: ( 28 | color: #0c5460, 29 | background: #d1ecf1, 30 | border: #bee5eb 31 | ), 32 | light: ( 33 | color: #818182, 34 | background: #fefefe, 35 | border: #fdfdfe 36 | ), 37 | dark: ( 38 | color: #1b1e21, 39 | background: #d6d8d9, 40 | border: #c6c8ca 41 | ) 42 | ); 43 | $alert-icon-left: .75rem; 44 | $alert-icon-top: calc(.75rem + 1px); 45 | $alert-padding-with-icon: .75rem 1.25rem .75rem 2.5rem; 46 | $alert-margin-bottom: 1rem; 47 | $alert-padding: .75rem 1.25rem; 48 | $alert-radius: .25rem; 49 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes fold { 2 | from { 3 | top: 0; 4 | opacity: 1; 5 | } 6 | to { 7 | top: -150px; 8 | opacity: 0; 9 | } 10 | } 11 | 12 | @mixin animation($type, $duration, $count) { 13 | animation-duration: $duration; 14 | animation-name: $type; 15 | animation-iteration-count: $count; 16 | animation-fill-mode: forwards; 17 | } 18 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | $breakpoints: ( 2 | xs: (max-width: 575.98px), 3 | sm: (min-width: 576px, max-width: 767.98px), 4 | md: (min-width: 768px, max-width: 991.98px), 5 | lg: (min-width: 992px, max-width: 1199.98px), 6 | xl: (min-width: 1200px) 7 | ); 8 | 9 | $container: ( 10 | xs: 95%, 11 | sm: 540px, 12 | md: 720px, 13 | lg: 960px, 14 | xl: 1140px 15 | ) 16 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_buttons.scss: -------------------------------------------------------------------------------- 1 | $button-sizes: (small, medium, large ); 2 | $button-paddings: ( 3 | small: .25rem .5rem, 4 | medium: .5rem .75rem, 5 | large: .75rem 1rem, 6 | ); 7 | $button-padding-icons: ( 8 | small: .25rem .5rem .25rem 1.75rem, 9 | medium: .5rem .75rem .5rem 2rem, 10 | large: .75rem 1rem .75rem 2.5rem 11 | ); 12 | $button-top-icons: ( 13 | small: calc(.085rem + 1px), 14 | medium: calc(.45rem + 1px), 15 | large: calc(.75rem + 1px) 16 | ); 17 | $button-left-icons: ( 18 | small: .5rem, 19 | medium: .5rem, 20 | large: .75rem 21 | ); 22 | $button-font-sizes: ( 23 | small: .75rem, 24 | medium: 1rem, 25 | large: 1.25rem, 26 | ); 27 | $button-radius: ( 28 | small: .2rem, 29 | medium: .25rem, 30 | large: .3rem, 31 | ); -------------------------------------------------------------------------------- /assets/scss/abstracts/_colors.scss: -------------------------------------------------------------------------------- 1 | $primary: #007bff; 2 | $secondary: #6c757d; 3 | $success: #28a745; 4 | $danger: #dc3545; 5 | $warning: #ffc107; 6 | $info: #17a2b8; 7 | $light: #f8f9fa; 8 | $dark: #343a40; 9 | $muted: #6c757d; 10 | $white: #fff; 11 | $grey: #ced4da; 12 | $black: #000; 13 | 14 | $contrasts: ( 15 | primary: $white, 16 | secondary: $white, 17 | success: $white, 18 | danger: $white, 19 | warning: $dark, 20 | info: $white, 21 | light: $dark, 22 | dark: $white, 23 | muted: $dark, 24 | white: $dark, 25 | grey: $white, 26 | black: $white, 27 | ); 28 | 29 | $colors: ( 30 | primary: $primary, 31 | secondary: $secondary, 32 | success: $success, 33 | danger: $danger, 34 | warning: $warning, 35 | info: $info, 36 | light: $light, 37 | dark: $dark, 38 | muted: $muted, 39 | white: $white, 40 | grey: $grey, 41 | black: $black, 42 | ) 43 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_fields.scss: -------------------------------------------------------------------------------- 1 | $field-margin-bottom: 1rem; 2 | $field-help-error-margin: .25rem 0 0; 3 | $widget-padding-with-icon: .375rem 2.5rem; 4 | $widget-choice-padding: .5rem 0; 5 | $widget-padding: .375rem 2.5rem .375rem .75rem; 6 | $widget-border: 1px solid $grey; 7 | $widget-height: calc(2rem + 2px); 8 | $widget-textarea-min-height: calc(4rem + 2px); 9 | $label-height: 1rem; 10 | $label-margin-bottom: .375rem; 11 | $widget-icon-top: calc(1.875rem + 1px); 12 | $widget-select-icon-right: 1.25rem; 13 | $widget-input-icon-right: .75rem; 14 | $widget-icon-left: .75rem; 15 | $widget-icon-color: rgba($black, .375); 16 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_flash_bag.scss: -------------------------------------------------------------------------------- 1 | $flash-bag-icons: ( 2 | success: ( 3 | color: $success, 4 | content: "\f058" 5 | ), 6 | danger: ( 7 | color: $danger, 8 | content: "\f057" 9 | ), 10 | info: ( 11 | color: $info, 12 | content: "\f05a" 13 | ), 14 | warning: ( 15 | color: $warning, 16 | content: "\f06a" 17 | ) 18 | ); 19 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_flex.scss: -------------------------------------------------------------------------------- 1 | $flex-directions: (row, column); 2 | $flex-justify: ( 3 | start: flex-start, 4 | end: flex-end, 5 | center: center, 6 | between: space-between, 7 | around: space-around, 8 | evenly: space-evenly 9 | ); 10 | $flex-aligns: ( 11 | start: flex-start, 12 | end: flex-end, 13 | center: center, 14 | stretch: stretch, 15 | baseline: baseline 16 | ); 17 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_grid.scss: -------------------------------------------------------------------------------- 1 | @mixin grid($columns) { 2 | flex: 0 0 percentage($columns/12); 3 | max-width: percentage($columns/12); 4 | } 5 | 6 | @mixin responsive-grid($columns, $size) { 7 | @include respond-from($size) { 8 | flex: 0 0 percentage($columns/12); 9 | max-width: percentage($columns/12); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_layout.scss: -------------------------------------------------------------------------------- 1 | $header-padding: 1rem 0; 2 | $header-background: $light; 3 | $header-border-bottom: 1px solid rgba(0,0,0,.125); 4 | $footer-padding: 1rem 0; 5 | $footer-background: $light; 6 | $footer-border-top: 1px solid rgba(0,0,0,.125); 7 | $main-padding: 2rem 0; 8 | $main-background: $white; 9 | $main-scrollbar-color: $grey; 10 | $main-scrollbar-background: $light; 11 | $main-scrollbar-width: .5rem; 12 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_list_groups.scss: -------------------------------------------------------------------------------- 1 | $list-group-item-padding: .5rem 0; 2 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_responsive.scss: -------------------------------------------------------------------------------- 1 | @mixin respond-to($size) { 2 | @if map-has_key(map-get($breakpoints, $size), max-width) { 3 | @media (max-width: #{map-get(map-get($breakpoints, $size), max-width)}) { 4 | @content; 5 | } 6 | } 7 | } 8 | 9 | @mixin respond-from($size) { 10 | @if map-has_key(map-get($breakpoints, $size), min-width) { 11 | @media (min-width: #{map-get(map-get($breakpoints, $size), min-width)}) { 12 | @content; 13 | } 14 | } 15 | } 16 | 17 | @mixin respond-range($from, $to) { 18 | @if map-has_key(map-get($breakpoints, $size), min-width) and map-has_key(map-get($breakpoints, $size), max-width) { 19 | @media (min-width: #{map-get(map-get($breakpoints, $from), min-width)}) and (max-width: #{map-get(map-get($breakpoints, $to), max-width)}) { 20 | @content; 21 | } 22 | } 23 | } 24 | 25 | @mixin respond-on($size) { 26 | @if map-has_key($breakpoints, $size) { 27 | @if map-has_key(map-get($breakpoints, $size), min-width) and map-has_key(map-get($breakpoints, $size), max-width) { 28 | @media (min-width: #{map-get(map-get($breakpoints, $size), min-width)}) and (max-width: #{map-get(map-get($breakpoints, $size), max-width)}) { 29 | @content; 30 | } 31 | } @else if map-has_key(map-get($breakpoints, $size), min-width) { 32 | @media (min-width: #{map-get(map-get($breakpoints, $size), min-width)}) { 33 | @content; 34 | } 35 | } @else { 36 | @media (max-width: #{map-get(map-get($breakpoints, $size), max-width)}) { 37 | @content; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_round.scss: -------------------------------------------------------------------------------- 1 | $round-size: .25rem; 2 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_shadows.scss: -------------------------------------------------------------------------------- 1 | $shadows: ( 2 | bottom: ( 3 | small: 0 .125rem .25rem, 4 | medium: 0 .5rem 1rem, 5 | large: 0 1rem 3rem 6 | ), 7 | center: ( 8 | small: 0 0 .25rem, 9 | medium: 0 0 1rem, 10 | large: 0 0 3rem 11 | ), 12 | top: ( 13 | small: 0 -.125rem .25rem, 14 | medium: 0 -.5rem 1rem, 15 | large: 0 -1rem 3rem 16 | ) 17 | ); 18 | 19 | @mixin shadow($size, $color: $black, $direction: bottom) { 20 | box-shadow: map-get(map-get($shadows, $direction), $size) rgba($color, .1); 21 | } 22 | -------------------------------------------------------------------------------- /assets/scss/abstracts/_texts.scss: -------------------------------------------------------------------------------- 1 | $base-font-size: 16px; 2 | 3 | $text-small: .8rem; 4 | $text-normal: 1rem; 5 | 6 | $heading-1: 2.5rem; 7 | $heading-2: 2rem; 8 | $heading-3: 1.75rem; 9 | $heading-4: 1.5rem; 10 | $heading-5: 1.25rem; 11 | $heading-6: 1rem; 12 | 13 | $headings: ( 14 | 1: $heading-1, 15 | 2: $heading-2, 16 | 3: $heading-3, 17 | 4: $heading-4, 18 | 5: $heading-5, 19 | 6: $heading-6 20 | ); 21 | -------------------------------------------------------------------------------- /assets/scss/base/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/assets/scss/base/.gitkeep -------------------------------------------------------------------------------- /assets/scss/base/_container.scss: -------------------------------------------------------------------------------- 1 | @use "sass:list"; 2 | 3 | .Container { 4 | width: 100%; 5 | padding-right: 15px; 6 | padding-left: 15px; 7 | margin-right: auto; 8 | margin-left: auto; 9 | box-sizing: border-box; 10 | 11 | @each $size, $width in $container { 12 | @include respond-from($size) { 13 | max-width: $width; 14 | } 15 | &.Container--#{$size} { 16 | @each $subsize, $subwidth in $container { 17 | @if index(map-keys($container), $subsize) >= index(map-keys($container), $size) { 18 | @include respond-from($subsize) { 19 | max-width: $subwidth; 20 | } 21 | } @else { 22 | max-width: 100%; 23 | } 24 | } 25 | } 26 | } 27 | 28 | &.Container--fluid { 29 | max-width: 100%; 30 | } 31 | } -------------------------------------------------------------------------------- /assets/scss/base/_flex.scss: -------------------------------------------------------------------------------- 1 | .Flex { 2 | display: flex; 3 | 4 | @each $direction in $flex-directions { 5 | &.Flex--#{$direction} { 6 | flex-direction: $direction; 7 | } 8 | 9 | @each $size in map-keys($breakpoints) { 10 | &.Flex--#{$size}-#{$direction} { 11 | @include respond-from($size) { 12 | flex-direction: $direction; 13 | } 14 | } 15 | } 16 | } 17 | } 18 | 19 | @each $name, $justify in $flex-justify { 20 | &.Justify--#{$name} { 21 | justify-content: $justify; 22 | } 23 | @each $size in map-keys($breakpoints) { 24 | &.Justify--#{$size}-#{$name} { 25 | @include respond-from($size) { 26 | justify-content: $justify; 27 | } 28 | } 29 | } 30 | } 31 | 32 | @each $name, $align in $flex-aligns { 33 | &.Align--#{$name} { 34 | align-items: $align; 35 | } 36 | @each $size in map-keys($breakpoints) { 37 | &.Align--#{$size}-#{$name} { 38 | @include respond-from($size) { 39 | align-items: $align; 40 | } 41 | } 42 | } 43 | } 44 | 45 | @each $type in (grow, shrink) { 46 | @each $i in (0, 1) { 47 | .Flex--#{$type}-#{$i} { 48 | flex-#{$type}: $i; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /assets/scss/base/_grid.scss: -------------------------------------------------------------------------------- 1 | .Row { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | min-width: 100%; 6 | margin-right: -15px; 7 | margin-left: -15px; 8 | 9 | .Column { 10 | position: relative; 11 | width: 100%; 12 | padding-right: 15px; 13 | padding-left: 15px; 14 | box-sizing: border-box; 15 | @for $columns from 1 through 12 { 16 | &.Column--#{$columns} { 17 | @include grid($columns); 18 | } 19 | } 20 | 21 | @each $size, $breakpoint in $breakpoints { 22 | @for $columns from 1 through 12 { 23 | &.Column--#{$size}-#{$columns} { 24 | @include responsive-grid($columns, $size); 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/scss/base/_shadow.scss: -------------------------------------------------------------------------------- 1 | .Shadow { 2 | @each $size, $shadow in $shadows { 3 | &.Shadow--#{$size} { 4 | @include shadow($size); 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /assets/scss/base/_text.scss: -------------------------------------------------------------------------------- 1 | .Text { 2 | &.Text--small { 3 | font-size: $text-small; 4 | } 5 | 6 | &.Text--normal { 7 | font-size: $text-normal; 8 | } 9 | 10 | @each $name, $color in $colors { 11 | &.Text--#{$name} { 12 | color: $color; 13 | } 14 | } 15 | } 16 | 17 | .Heading { 18 | @each $size, $heading in $headings { 19 | &.Heading--#{$size} { 20 | font-size: $heading; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/scss/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/assets/scss/components/.gitkeep -------------------------------------------------------------------------------- /assets/scss/components/_alert.scss: -------------------------------------------------------------------------------- 1 | .Alert { 2 | margin-bottom: $alert-margin-bottom; 3 | padding: $alert-padding; 4 | border: 1px solid transparent; 5 | border-radius: $alert-radius; 6 | min-height: 1rem; 7 | 8 | @each $name, $color in $alert-colors { 9 | &.Alert--#{$name} { 10 | color: map-get($color, color); 11 | background-color: map-get($color, background); 12 | border-color: map-get($color, border); 13 | } 14 | } 15 | 16 | &.Alert--icon { 17 | padding: $alert-padding-with-icon; 18 | position: relative; 19 | 20 | &:before { 21 | position: absolute; 22 | top: $alert-icon-top; 23 | left: $alert-icon-left; 24 | width: 1rem; 25 | height: 1rem; 26 | line-height: 1rem; 27 | text-align: center; 28 | vertical-align: middle; 29 | } 30 | 31 | @each $type in map_keys($icon-types) { 32 | @each $icon in map_keys($icons) { 33 | &.Alert--icon-#{$type}-#{$icon}:before { 34 | @include icon($type, $icon); 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /assets/scss/components/_card.scss: -------------------------------------------------------------------------------- 1 | .Card { 2 | @include shadow(medium); 3 | display: flex; 4 | flex-direction: column; 5 | border-radius: $round-size; 6 | border: 1px solid rgba(0,0,0,.125); 7 | 8 | @each $name, $color in $colors { 9 | &.Card--#{$name} { 10 | background: $color; 11 | color: map-get($contrasts, $name); 12 | } 13 | } 14 | 15 | > .Card__Header, > .Card__Footer { 16 | background-color: rgba(0,0,0,.03); 17 | } 18 | 19 | > .Card__Body { 20 | padding: 1.25rem; 21 | } 22 | 23 | > .Card__Header { 24 | border-bottom: 1px solid rgba(0,0,0,.125); 25 | padding: .75rem 1.25rem; 26 | } 27 | 28 | > .Card__Header { 29 | border-radius: calc($round-size - 1px) calc($round-size - 1px) 0 0; 30 | } 31 | 32 | > .Card__Footer { 33 | border-top: 1px solid rgba(0,0,0,.125); 34 | padding: .75rem 1.25rem; 35 | border-radius: 0 0 calc($round-size - 1px) calc($round-size - 1px); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /assets/scss/components/_flash_bag.scss: -------------------------------------------------------------------------------- 1 | .FlashBag { 2 | position: fixed; 3 | bottom: 30px; 4 | right: 30px; 5 | width: 250px; 6 | z-index: 10000; 7 | display: flex; 8 | flex-direction: column; 9 | 10 | .FlashBag__Message { 11 | padding: 1rem 1rem 1rem calc(2rem + 30px); 12 | position: relative; 13 | margin-bottom: 1rem; 14 | min-height: 30px; 15 | font-size: $text-small; 16 | background: $white; 17 | @include shadow(small); 18 | 19 | &:last-child { 20 | margin-bottom: 0 !important; 21 | } 22 | 23 | &:before { 24 | font-family: "Font Awesome 5 Free"; 25 | font-weight: bold; 26 | font-size: 30px; 27 | position: absolute; 28 | top: 1rem; 29 | left: 1rem; 30 | } 31 | 32 | @each $type, $icon in $flash-bag-icons { 33 | &.FlashBag__Message--#{$type}:before { 34 | color: map-get($icon, color); 35 | content: map-get($icon, content); 36 | } 37 | } 38 | 39 | &.FlashBag__Message--hide { 40 | @include animation(fold, 1s, 1); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /assets/scss/components/_list_group.scss: -------------------------------------------------------------------------------- 1 | .List-Group { 2 | margin: 0; 3 | padding: 0; 4 | .List-Group__Item { 5 | padding: $list-group-item-padding; 6 | border-bottom: 1px solid $grey; 7 | 8 | &:last-child { 9 | border-bottom: none; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /assets/scss/layout/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/assets/scss/layout/.gitkeep -------------------------------------------------------------------------------- /assets/scss/layout/_footer.scss: -------------------------------------------------------------------------------- 1 | .Footer { 2 | padding: $footer-padding; 3 | background: $footer-background; 4 | border-top: $footer-border-top; 5 | @include shadow(small, $black, top); 6 | z-index: 10; 7 | position: relative; 8 | } 9 | -------------------------------------------------------------------------------- /assets/scss/layout/_global.scss: -------------------------------------------------------------------------------- 1 | *, ::after, ::before { 2 | box-sizing: content-box; 3 | } 4 | 5 | body { 6 | font-size: $base-font-size; 7 | font-family: 'Roboto', sans-serif; 8 | display: flex; 9 | flex-direction: column; 10 | height: 100vh; 11 | color: $dark; 12 | } 13 | 14 | header { 15 | 16 | } 17 | 18 | main { 19 | flex-grow: 1; 20 | } 21 | 22 | footer { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /assets/scss/layout/_header.scss: -------------------------------------------------------------------------------- 1 | .Header { 2 | padding: $header-padding; 3 | background: $header-background; 4 | border-bottom: $header-border-bottom; 5 | @include shadow(small); 6 | z-index: 10; 7 | position: relative; 8 | 9 | nav { 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | 14 | .Header__Title { 15 | text-decoration: none; 16 | color: inherit; 17 | margin: 0; 18 | font-weight: 400; 19 | font-size: $heading-2; 20 | text-transform: uppercase; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/scss/layout/_main.scss: -------------------------------------------------------------------------------- 1 | .Main { 2 | padding: $main-padding; 3 | background: $main-background; 4 | overflow-y: auto; 5 | 6 | &::-webkit-scrollbar{ 7 | width: $main-scrollbar-width; 8 | } 9 | &::-webkit-scrollbar-track{ 10 | background: $main-scrollbar-background; 11 | border-radius: 0px 12 | } 13 | &::-webkit-scrollbar-thumb{ 14 | background: $main-scrollbar-color; 15 | border-radius: 0px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /assets/scss/layout/_main_nav.scss: -------------------------------------------------------------------------------- 1 | .Main-Nav { 2 | flex-grow: 1; 3 | display: flex; 4 | justify-content: flex-end; 5 | align-items: center; 6 | list-style: none; 7 | margin: 0; 8 | padding: 0; 9 | 10 | .Main-Nav__Item { 11 | margin: 0 1rem; 12 | 13 | &:first-child { 14 | margin-left: 0; 15 | } 16 | 17 | &:last-child { 18 | margin-right: 0; 19 | } 20 | 21 | > a { 22 | text-decoration: none; 23 | color: inherit; 24 | 25 | &:hover { 26 | font-weight: 500; 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 2 | "vendors/normalize", 3 | "vendors/roboto", 4 | "vendors/font_awesome"; 5 | 6 | @import 7 | "abstracts/animations", 8 | "abstracts/colors", 9 | "abstracts/buttons", 10 | "abstracts/shadows", 11 | "abstracts/flex", 12 | "abstracts/layout", 13 | "abstracts/alerts", 14 | "abstracts/list_groups", 15 | "abstracts/breakpoints", 16 | "abstracts/responsive", 17 | "abstracts/flash_bag", 18 | "abstracts/grid", 19 | "abstracts/round", 20 | "abstracts/fields", 21 | "abstracts/texts", 22 | "abstracts/icons"; 23 | 24 | @import 25 | "base/shadow", 26 | "base/grid", 27 | "base/flex", 28 | "base/container", 29 | "base/text"; 30 | 31 | @import 32 | "layout/header", 33 | "layout/main", 34 | "layout/main_nav", 35 | "layout/footer", 36 | "layout/global"; 37 | 38 | @import 39 | "components/card", 40 | "components/field", 41 | "components/alert", 42 | "components/list_group", 43 | "components/button", 44 | "components/flash_bag"; 45 | -------------------------------------------------------------------------------- /assets/scss/pages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/assets/scss/pages/.gitkeep -------------------------------------------------------------------------------- /assets/scss/themes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/assets/scss/themes/.gitkeep -------------------------------------------------------------------------------- /assets/scss/vendors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/assets/scss/vendors/.gitkeep -------------------------------------------------------------------------------- /assets/scss/vendors/_font_awesome.scss: -------------------------------------------------------------------------------- 1 | @import "~@fortawesome/fontawesome-free/scss/fontawesome"; 2 | @import "~@fortawesome/fontawesome-free/scss/solid"; 3 | @import "~@fortawesome/fontawesome-free/scss/regular"; 4 | @import "~@fortawesome/fontawesome-free/scss/brands"; -------------------------------------------------------------------------------- /assets/scss/vendors/_normalize.scss: -------------------------------------------------------------------------------- 1 | @import "~normalize.css/normalize.css"; -------------------------------------------------------------------------------- /assets/scss/vendors/_roboto.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap'); 2 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 23 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 24 | } 25 | 26 | if ($input->hasParameterOption('--no-debug', true)) { 27 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 28 | } 29 | 30 | require dirname(__DIR__).'/config/bootstrap.php'; 31 | 32 | if ($_SERVER['APP_DEBUG']) { 33 | umask(0000); 34 | 35 | if (class_exists(Debug::class)) { 36 | Debug::enable(); 37 | } 38 | } 39 | 40 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 41 | $application = new Application($kernel); 42 | $application->run($input); 43 | -------------------------------------------------------------------------------- /bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =1.2) 9 | if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) { 10 | foreach ($env as $k => $v) { 11 | $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v); 12 | } 13 | } elseif (!class_exists(Dotenv::class)) { 14 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 15 | } else { 16 | // load all the .env files 17 | (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); 18 | } 19 | 20 | $_SERVER += $_ENV; 21 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 22 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 23 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 24 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 6 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 7 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 8 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 9 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 10 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true, 'panther' => true], 11 | DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true, 'integration' => true], 12 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true, 'integration' => true], 13 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 14 | Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], 15 | Enqueue\Bundle\EnqueueBundle::class => ['all' => true], 16 | Enqueue\MessengerAdapter\Bundle\EnqueueAdapterBundle::class => ['all' => true], 17 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 18 | ]; 19 | -------------------------------------------------------------------------------- /config/packages/assets.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | assets: 3 | json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/packages/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { only_exceptions: false } 7 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(resolve:DATABASE_URL)%' 4 | 5 | # IMPORTANT: You MUST configure your server version, 6 | # either here or in the DATABASE_URL env var (see .env file) 7 | #server_version: '5.7' 8 | orm: 9 | auto_generate_proxy_classes: true 10 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 11 | auto_mapping: true 12 | mappings: 13 | App: 14 | is_bundle: false 15 | type: annotation 16 | dir: '%kernel.project_dir%/src/Infrastructure/Doctrine/Entity' 17 | prefix: 'App\Infrastructure\Doctrine\Entity' 18 | alias: App 19 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | dir_name: '%kernel.project_dir%/src/Infrastructure/Doctrine/Migrations' 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | namespace: DoctrineMigrations 6 | -------------------------------------------------------------------------------- /config/packages/enqueue.yaml: -------------------------------------------------------------------------------- 1 | enqueue: 2 | default: 3 | transport: '%env(resolve:ENQUEUE_DSN)%' 4 | client: null 5 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | #csrf_protection: true 4 | #http_method_override: true 5 | 6 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 7 | # Remove or comment this section to explicitly disable session support. 8 | session: 9 | handler_id: null 10 | cookie_secure: auto 11 | cookie_samesite: lax 12 | serializer: 13 | enabled: true 14 | #esi: true 15 | #fragments: true 16 | php_errors: 17 | log: true 18 | -------------------------------------------------------------------------------- /config/packages/integration/dama_doctrine_test_bundle.yaml: -------------------------------------------------------------------------------- 1 | dama_doctrine_test: 2 | enable_static_connection: true 3 | enable_static_meta_data_cache: true 4 | enable_static_query_cache: true 5 | -------------------------------------------------------------------------------- /config/packages/integration/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/integration/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | strict_variables: true 3 | -------------------------------------------------------------------------------- /config/packages/integration/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | not_compromised_password: false 4 | -------------------------------------------------------------------------------- /config/packages/integration/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | webpack_encore: 2 | strict_mode: false 3 | -------------------------------------------------------------------------------- /config/packages/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. 4 | failure_transport: failed 5 | 6 | transports: 7 | # https://symfony.com/doc/current/messenger.html#transport-configuration 8 | mailer: 9 | dsn: 'doctrine://default?queue_name=mailer' 10 | retry_strategy: 11 | max_retries: 10 12 | # milliseconds delay 13 | delay: 1000 14 | # causes the delay to be higher before each retry 15 | # e.g. 1 second delay, 2 seconds, 4 seconds 16 | multiplier: 5 17 | max_delay: 0 18 | # async: '%env(MESSENGER_TRANSPORT_DSN)%' 19 | failed: 'doctrine://default?queue_name=failed' 20 | # sync: 'sync://' 21 | 22 | routing: 23 | # Route your messages to the transports 24 | 'Symfony\Component\Mailer\Messenger\SendEmailMessage': mailer 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/packages/prod/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | transports: 4 | sqs: 'enqueue://default?topic[name]=code-challenge&queue[name]=code-challenge&receiveTimeout=3' 5 | routing: 6 | TBoileau\CodeChallenge\Domain\System\Request\TrackRequest: sqs 7 | -------------------------------------------------------------------------------- /config/packages/prod/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: null 4 | -------------------------------------------------------------------------------- /config/packages/prod/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | #webpack_encore: 2 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 3 | # Available in version 1.2 4 | #cache: true 5 | -------------------------------------------------------------------------------- /config/packages/ramsey_uuid_doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | types: 4 | uuid: 'Ramsey\Uuid\Doctrine\UuidType' 5 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | encoders: 3 | App\Infrastructure\Security\User: 4 | algorithm: argon2i 5 | providers: 6 | user_provider: 7 | id: App\Infrastructure\Security\Provider\UserProvider 8 | firewalls: 9 | dev: 10 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 11 | security: false 12 | main: 13 | anonymous: lazy 14 | provider: user_provider 15 | pattern: ^/ 16 | logout: 17 | path: logout 18 | guard: 19 | authenticators: 20 | - App\Infrastructure\Security\Guard\WebAuthenticator 21 | access_control: 22 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/test/dama_doctrine_test_bundle.yaml: -------------------------------------------------------------------------------- 1 | dama_doctrine_test: 2 | enable_static_connection: true 3 | enable_static_meta_data_cache: true 4 | enable_static_query_cache: true 5 | -------------------------------------------------------------------------------- /config/packages/test/enqueue.yaml: -------------------------------------------------------------------------------- 1 | enqueue: 2 | default: 3 | transport: 'null:' 4 | client: 5 | traceable_producer: true 6 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/test/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | strict_variables: true 3 | -------------------------------------------------------------------------------- /config/packages/test/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | not_compromised_password: false 4 | -------------------------------------------------------------------------------- /config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /config/packages/test/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | webpack_encore: 2 | strict_mode: false 3 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | form_themes: 4 | - components/form.html.twig 5 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | 5 | # Enables validator auto-mapping support. 6 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 7 | #auto_mapping: 8 | # App\Entity\: [] 9 | -------------------------------------------------------------------------------- /config/packages/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | webpack_encore: 2 | # The path where Encore is building the assets - i.e. Encore.setOutputPath() 3 | output_path: '%kernel.project_dir%/public/build' 4 | # If multiple builds are defined (as shown below), you can disable the default build: 5 | # output_path: false 6 | 7 | # if using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials') 8 | # crossorigin: 'anonymous' 9 | 10 | # preload all rendered script and link tags automatically via the http2 Link header 11 | # preload: true 12 | 13 | # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data 14 | # strict_mode: false 15 | 16 | # if you have multiple builds: 17 | # builds: 18 | # pass "frontend" as the 3rg arg to the Twig functions 19 | # {{ encore_entry_script_tags('entry1', null, 'frontend') }} 20 | 21 | # frontend: '%kernel.project_dir%/public/frontend/build' 22 | 23 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 24 | # Put in config/packages/prod/webpack_encore.yaml 25 | # cache: true 26 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | question_create: 2 | path: /questions/create 3 | methods: GET|POST 4 | controller: App\UserInterface\Controller\Question\CreateController 5 | 6 | question_update: 7 | path: /questions/{id}/update 8 | methods: GET|POST 9 | controller: App\UserInterface\Controller\Question\UpdateController 10 | 11 | registration: 12 | path: /registration 13 | methods: GET|POST 14 | controller: App\UserInterface\Controller\RegistrationController 15 | 16 | ask_password_reset: 17 | path: /reset-password 18 | methods: GET|POST 19 | controller: App\UserInterface\Controller\Security\AskPasswordResetController 20 | 21 | recover_password: 22 | path: /handle-reset-password/{email}/{token} 23 | methods: GET|POST 24 | controller: App\UserInterface\Controller\Security\RecoverPasswordController 25 | 26 | login: 27 | path: /login 28 | methods: GET|POST 29 | controller: App\UserInterface\Controller\LoginController 30 | 31 | logout: 32 | path: /logout 33 | methods: GET 34 | controller: App\UserInterface\Controller\LogoutController 35 | 36 | home: 37 | path: / 38 | methods: GET 39 | controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController 40 | defaults: 41 | template: home.html.twig 42 | -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: ../../src/UserInterface/Controller/ 3 | type: annotation 4 | 5 | kernel: 6 | resource: ../../src/Kernel.php 7 | type: annotation 8 | -------------------------------------------------------------------------------- /config/routes/dev/framework.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /config/routes/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler_wdt: 2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 3 | prefix: /_wdt 4 | 5 | web_profiler_profiler: 6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 7 | prefix: /_profiler 8 | -------------------------------------------------------------------------------- /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 | app.from_email: 'no-reply@email.com' 8 | app.display_name: 'Code Challenge' 9 | 10 | services: 11 | # default configuration for services in *this* file 12 | _defaults: 13 | autowire: true # Automatically injects dependencies in your services. 14 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 15 | bind: 16 | $fromEmail: '%app.from_email%' 17 | $displayName: '%app.display_name%' 18 | 19 | # makes classes in src/ available to be used as services 20 | # this creates a service per class whose id is the fully-qualified class name 21 | 22 | App\Infrastructure\: 23 | resource: '../src/Infrastructure' 24 | exclude: '../src/Infrastructure/{Test}' 25 | 26 | App\UserInterface\: 27 | resource: '../src/UserInterface' 28 | 29 | TBoileau\CodeChallenge\Domain\: 30 | resource: '../domain/src' 31 | 32 | # controllers are imported separately to make sure services can be injected 33 | # as action arguments even if you don't extend any base controller class 34 | App\UserInterface\Controller\: 35 | resource: '../src/UserInterface/Controller' 36 | tags: ['controller.service_arguments'] 37 | -------------------------------------------------------------------------------- /config/services_integration.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 | app.from_email: 'no-reply@email.com' 8 | app.display_name: 'Code Challenge' 9 | 10 | services: 11 | # default configuration for services in *this* file 12 | _defaults: 13 | autowire: true # Automatically injects dependencies in your services. 14 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 15 | bind: 16 | $fromEmail: '%app.from_email%' 17 | $displayName: '%app.display_name%' 18 | 19 | # makes classes in src/ available to be used as services 20 | # this creates a service per class whose id is the fully-qualified class name 21 | 22 | App\Infrastructure\: 23 | resource: '../src/Infrastructure' 24 | exclude: '../src/Infrastructure/Adapter' 25 | 26 | App\UserInterface\: 27 | resource: '../src/UserInterface' 28 | 29 | TBoileau\CodeChallenge\Domain\: 30 | resource: '../domain/src' 31 | 32 | # controllers are imported separately to make sure services can be injected 33 | # as action arguments even if you don't extend any base controller class 34 | App\UserInterface\Controller\: 35 | resource: '../src/UserInterface/Controller' 36 | tags: ['controller.service_arguments'] 37 | -------------------------------------------------------------------------------- /docs/img/alert_danger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/alert_danger.png -------------------------------------------------------------------------------- /docs/img/alert_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/alert_dark.png -------------------------------------------------------------------------------- /docs/img/alert_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/alert_icons.png -------------------------------------------------------------------------------- /docs/img/alert_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/alert_info.png -------------------------------------------------------------------------------- /docs/img/alert_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/alert_light.png -------------------------------------------------------------------------------- /docs/img/alert_primary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/alert_primary.png -------------------------------------------------------------------------------- /docs/img/alert_secondary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/alert_secondary.png -------------------------------------------------------------------------------- /docs/img/alert_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/alert_success.png -------------------------------------------------------------------------------- /docs/img/alert_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/alert_warning.png -------------------------------------------------------------------------------- /docs/img/authentication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/authentication.png -------------------------------------------------------------------------------- /docs/img/button_danger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_danger.png -------------------------------------------------------------------------------- /docs/img/button_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_dark.png -------------------------------------------------------------------------------- /docs/img/button_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_icon.png -------------------------------------------------------------------------------- /docs/img/button_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_info.png -------------------------------------------------------------------------------- /docs/img/button_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_light.png -------------------------------------------------------------------------------- /docs/img/button_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_outline.png -------------------------------------------------------------------------------- /docs/img/button_primary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_primary.png -------------------------------------------------------------------------------- /docs/img/button_secondary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_secondary.png -------------------------------------------------------------------------------- /docs/img/button_sizes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_sizes.png -------------------------------------------------------------------------------- /docs/img/button_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_success.png -------------------------------------------------------------------------------- /docs/img/button_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/button_warning.png -------------------------------------------------------------------------------- /docs/img/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/card.png -------------------------------------------------------------------------------- /docs/img/card_danger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/card_danger.png -------------------------------------------------------------------------------- /docs/img/card_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/card_dark.png -------------------------------------------------------------------------------- /docs/img/card_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/card_info.png -------------------------------------------------------------------------------- /docs/img/card_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/card_light.png -------------------------------------------------------------------------------- /docs/img/card_primary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/card_primary.png -------------------------------------------------------------------------------- /docs/img/card_secondary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/card_secondary.png -------------------------------------------------------------------------------- /docs/img/card_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/card_success.png -------------------------------------------------------------------------------- /docs/img/card_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/card_warning.png -------------------------------------------------------------------------------- /docs/img/clean_architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/clean_architecture.jpg -------------------------------------------------------------------------------- /docs/img/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/dashboard.png -------------------------------------------------------------------------------- /docs/img/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/grid.png -------------------------------------------------------------------------------- /docs/img/grid_lg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/grid_lg.png -------------------------------------------------------------------------------- /docs/img/grid_md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/grid_md.png -------------------------------------------------------------------------------- /docs/img/grid_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/grid_sm.png -------------------------------------------------------------------------------- /docs/img/grid_xl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/grid_xl.png -------------------------------------------------------------------------------- /docs/img/grid_xs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/grid_xs.png -------------------------------------------------------------------------------- /docs/img/manage_questions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/manage_questions.png -------------------------------------------------------------------------------- /docs/img/manage_users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/manage_users.png -------------------------------------------------------------------------------- /docs/img/packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/packages.png -------------------------------------------------------------------------------- /docs/img/ranking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/ranking.png -------------------------------------------------------------------------------- /docs/img/registration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/registration.png -------------------------------------------------------------------------------- /docs/img/reply_quiz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/reply_quiz.png -------------------------------------------------------------------------------- /docs/img/request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/request.png -------------------------------------------------------------------------------- /docs/img/sign_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/sign_out.png -------------------------------------------------------------------------------- /docs/img/sitemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/sitemap.png -------------------------------------------------------------------------------- /docs/img/update_password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/update_password.png -------------------------------------------------------------------------------- /docs/img/update_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/docs/img/update_profile.png -------------------------------------------------------------------------------- /docs/uml/authentication.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Visiteur 5 | database Database 6 | database Mailer 7 | rectangle Authentification { 8 | Visiteur -- (Se connecter) 9 | (Se connecter) .> (Inscription) : include 10 | (Se connecter) -- Database 11 | (Oubi de mot de passe) .> (Se connecter) : extends 12 | (Oubi de mot de passe) -- Mailer 13 | } 14 | @enduml -------------------------------------------------------------------------------- /docs/uml/dashboard.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Utilisateur 5 | database Database 6 | rectangle Dashboard { 7 | Utilisateur -- (Visualiser ses statistiques) 8 | Utilisateur -- (Activités récentes) 9 | Utilisateur -- (Rang) 10 | (Visualiser ses statistiques) -- Database 11 | (Activités récentes) -- Database 12 | (Rang) -- Database 13 | } 14 | @enduml -------------------------------------------------------------------------------- /docs/uml/manage_questions.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Contributeur 5 | actor Administrateur 6 | database Database 7 | rectangle "Gestion des questions" { 8 | Contributeur -- (Lister ses\npropres question) 9 | Contributeur -- (Ajouter une\nquestion) 10 | Contributeur -- (Modifier une\nquestion) 11 | Contributeur -- (Supprimer une\nquestion) 12 | (Ajouter une\nquestion) -- Database 13 | (Lister ses\npropres question) -- Database 14 | (Modifier une\nquestion) -- Database 15 | (Supprimer une\nquestion) -- Database 16 | (Valider une\nquestion) -- Database 17 | Administrateur -- (Valider une\nquestion) 18 | } 19 | Administrateur -> Contributeur 20 | @enduml -------------------------------------------------------------------------------- /docs/uml/manage_users.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Administrateur 5 | database Database 6 | rectangle "Gestion des utilisateurs" { 7 | Administrateur -- (Lister les\nutilisateurs) 8 | Administrateur -- (Ajouter un\nutilisateur) 9 | Administrateur -- (Modifier un\nutilisateur) 10 | Administrateur -- (Supprimer un\nutilisateur) 11 | (Lister les\nutilisateurs) -- Database 12 | (Ajouter un\nutilisateur) -- Database 13 | (Modifier un\nutilisateur) -- Database 14 | (Supprimer un\nutilisateur) -- Database 15 | } 16 | @enduml -------------------------------------------------------------------------------- /docs/uml/packages.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Visiteur 5 | actor Utilisateur 6 | actor Contributeur 7 | actor Administrateur 8 | database Database 9 | database Mailer 10 | database Stockage 11 | rectangle "Code Challenge" { 12 | Visiteur -- [Se connecter] 13 | Visiteur -- [S'inscrire] 14 | [Se connecter] -- Database 15 | [Se connecter] -- Mailer 16 | [S'inscrire] -- Database 17 | Utilisateur -- [Se déconnecter] 18 | Utilisateur -- [Répondre à\nun quiz] 19 | Utilisateur -- [Dashboard] 20 | Utilisateur -- [Modifier son\nmot de passe] 21 | Utilisateur -- [Modifier son\nprofil] 22 | Utilisateur -- [Classement] 23 | [Répondre à\nun quiz] -- Database 24 | [Dashboard] -- Database 25 | [Modifier son\nmot de passe] -- Database 26 | [Modifier son\nprofil] -- Database 27 | [Modifier son\nprofil] -- Stockage 28 | [Classement] -- Database 29 | Contributeur -- [Gestion des\nquestions] 30 | [Gestion des\nquestions] -- Database 31 | Administrateur -- [Gestion des\nutilisateurs] 32 | Administrateur -- [Gestion des\nquestions] 33 | [Gestion des\nutilisateurs] -- Database 34 | [Gestion des\nquestions] -- Database 35 | } 36 | Contributeur -> Utilisateur 37 | Administrateur -> Contributeur 38 | @enduml -------------------------------------------------------------------------------- /docs/uml/ranking.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Utilisateur 5 | database Database 6 | rectangle "Classement" { 7 | Utilisateur -- (Visualiser la liste des\nutilisateurs classés par point) 8 | (Rang) .> (Visualiser la liste des\nutilisateurs classés par point) : include 9 | (Visualiser la liste des\nutilisateurs classés par point) -- Database 10 | } 11 | @enduml -------------------------------------------------------------------------------- /docs/uml/registration.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Visiteur 5 | database Database 6 | rectangle Inscription { 7 | Visiteur -- (S'inscrire) 8 | (S'inscrire) .> (Accepter les CGU) : include 9 | (S'inscrire) -- Database 10 | } 11 | @enduml -------------------------------------------------------------------------------- /docs/uml/reply_quiz.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Utilisateur 5 | database Database 6 | rectangle "Répondre à un quiz" { 7 | Utilisateur -- (Répondre aux\nquestions) 8 | (Répondre aux\nquestions) .> (Visualiser les\nrésultats) : include 9 | (Répondre aux\nquestions) -- Database 10 | (Répondre aux\nquestions) .> (Mise à jour\ndes résultats) : include 11 | (Répondre aux\nquestions) .> (Mise à jour\ndu classement) : include 12 | } 13 | @enduml -------------------------------------------------------------------------------- /docs/uml/request.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | box "Symfony" #LightGrey 3 | participant HTTPFoundation 4 | participant Kernel 5 | participant Router 6 | end box 7 | box "UserInterface" #LightBlue 8 | participant BlogController 9 | participant ReadPostPresenter 10 | participant PostViewModel 11 | end box 12 | box "Domain" #LightRed 13 | participant ReadPost 14 | participant ReadPostRequest 15 | participant ReadPostPresenterInterface 16 | participant ReadPostResponse 17 | participant Post 18 | participant PostGateway 19 | end box 20 | box "Infrastructure" #LightGreen 21 | participant PostRepository 22 | end box 23 | 24 | HTTPFoundation -> Kernel : Envoie de la requête 25 | Kernel ->Router : Récupère la route 26 | Router -> Kernel : Renvoie les informations\nde la route 27 | Kernel -> BlogController : Appelle la méthode read() 28 | BlogController -> ReadPostRequest : Instancie la requête 29 | note right of BlogController 30 | En lui passant le **slug** 31 | récupérer dans l'url 32 | end note 33 | BlogController -> ReadPostPresenter : Instancie le présenteur 34 | BlogController -> ReadPost : Appelle la méthode execute() 35 | ReadPost -> PostGateway : Récupère l'article 36 | note right of ReadPost 37 | En lui passant le **slug** 38 | récupérer dans **ReadPostRequest** 39 | end note 40 | PostGateway -> PostRepository : Requête DQL pour\npour récupérer l'article 41 | note left of PostRepository 42 | PostRepository implémentant l'interface PostGateway, 43 | en injectant seulement l'interface 44 | Symfony vous injectera une instance de PostRepository. 45 | end note 46 | PostRepository -> Post : Instancie et hydrate un article 47 | PostRepository -> ReadPost : Renvoie un article 48 | ReadPost -> ReadPostResponse : Instancie et hydrate la réponse\navec l'article récupéré 49 | ReadPost -> ReadPostPresenterInterface : Présente la réponse 50 | ReadPostPresenterInterface -> ReadPostPresenter : Prépare la vue à retourner 51 | ReadPostPresenter -> PostViewModel : Instancie et hydrate un PostViewModel\navec les données de la réponse 52 | BlogController -> HTTPFoundation : Créer et renvoie la réponse\nen passant à la vue la vue modèle\nprésenter ultérieurement 53 | 54 | @enduml -------------------------------------------------------------------------------- /docs/uml/sign_out.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Utilisateur 5 | actor Visiteur 6 | rectangle Déconnexion { 7 | Utilisateur -- (Se déconnecter) 8 | (Se déconnecter) .> (Connexion) : include 9 | Visiteur -- (Connexion) 10 | } 11 | @enduml -------------------------------------------------------------------------------- /docs/uml/sitemap.puml: -------------------------------------------------------------------------------- 1 | @startwbs 2 | * Page d'accueil 3 | ** Authentification 4 | *** Oubli de mot de passe 5 | ** Inscription 6 | ** Gestion des utilisateurs 7 | *** Ajouter un utilisateur 8 | *** Modifier un utilisateur 9 | *** Supprimer un utilisateur 10 | ** Gestion des questions 11 | *** Valider une question 12 | *** Ajouter une question 13 | *** Modifier une question 14 | *** Supprimer une question 15 | ** Quiz 16 | *** Répondre au quiz 17 | *** Résultats du quiz 18 | *** Mise à jour du classement 19 | ** Classement 20 | ** Dashboard 21 | *** Modifier mon profil 22 | *** Modifier mon mot de passe 23 | 24 | @endwbs -------------------------------------------------------------------------------- /docs/uml/update_password.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Utilisateur 5 | database Database 6 | rectangle "Modifier son mot de passe" { 7 | Utilisateur -- (Modifier son mot de passe) 8 | (Modifier son mot de passe) -- Database 9 | } 10 | @enduml -------------------------------------------------------------------------------- /docs/uml/update_profile.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | left to right direction 3 | skinparam packageStyle rectangle 4 | actor Utilisateur 5 | database Database 6 | database Stockage 7 | rectangle "Modifier son profil" { 8 | Utilisateur -- (Modifier son profil) 9 | (Uploader un avatar) .> (Modifier son profil) : extends 10 | (Modifier son profil) -- Database 11 | (Uploader un avatar) -- Stockage 12 | } 13 | @enduml -------------------------------------------------------------------------------- /domain/src/Quiz/Entity/Answer.php: -------------------------------------------------------------------------------- 1 | id = $id; 48 | $this->title = $title; 49 | $this->good = $good; 50 | } 51 | 52 | /** 53 | * @return UuidInterface 54 | */ 55 | public function getId(): UuidInterface 56 | { 57 | return $this->id; 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getTitle(): string 64 | { 65 | return $this->title; 66 | } 67 | 68 | /** 69 | * @return bool 70 | */ 71 | public function isGood(): bool 72 | { 73 | return $this->good; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /domain/src/Quiz/Entity/Question.php: -------------------------------------------------------------------------------- 1 | getTitle(), 40 | array_map(fn (array $answer) => Answer::fromArray($answer), $createRequest->getAnswers()) 41 | ); 42 | } 43 | 44 | /** 45 | * Question constructor. 46 | * @param UuidInterface $id 47 | * @param string $title 48 | * @param Answer[] $answers 49 | */ 50 | public function __construct(UuidInterface $id, string $title, array $answers) 51 | { 52 | $this->id = $id; 53 | $this->title = $title; 54 | $this->answers = $answers; 55 | } 56 | 57 | /** 58 | * @return UuidInterface 59 | */ 60 | public function getId(): UuidInterface 61 | { 62 | return $this->id; 63 | } 64 | 65 | /** 66 | * @return string 67 | */ 68 | public function getTitle(): string 69 | { 70 | return $this->title; 71 | } 72 | 73 | /** 74 | * @param string $title 75 | */ 76 | public function setTitle(string $title): void 77 | { 78 | $this->title = $title; 79 | } 80 | 81 | /** 82 | * @return Answer[] 83 | */ 84 | public function getAnswers(): array 85 | { 86 | return $this->answers; 87 | } 88 | 89 | /** 90 | * @param Answer[] $answers 91 | */ 92 | public function setAnswers(array $answers): void 93 | { 94 | $this->answers = $answers; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /domain/src/Quiz/Gateway/QuestionGateway.php: -------------------------------------------------------------------------------- 1 | title = $title; 42 | $this->answers = $answers; 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function getTitle(): string 49 | { 50 | return $this->title; 51 | } 52 | 53 | /** 54 | * @return array 55 | */ 56 | public function getAnswers(): array 57 | { 58 | return $this->answers; 59 | } 60 | 61 | /** 62 | * @throws AssertionFailedException 63 | */ 64 | public function validate(): void 65 | { 66 | Assertion::notBlank($this->title); 67 | Assertion::minCount($this->answers, 2); 68 | Assertion::allNotBlank(array_map(fn (array $answer) => $answer["title"], $this->answers)); 69 | Assertion::allBoolean(array_map(fn (array $answer) => $answer["good"], $this->answers)); 70 | Assertion::minCount(array_filter($this->answers, fn (array $answer) => $answer["good"]), 1); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /domain/src/Quiz/Request/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | id = $id; 50 | $this->title = $title; 51 | $this->answers = $answers; 52 | } 53 | 54 | /** 55 | * @return UuidInterface 56 | */ 57 | public function getId(): UuidInterface 58 | { 59 | return $this->id; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getTitle(): string 66 | { 67 | return $this->title; 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | public function getAnswers(): array 74 | { 75 | return $this->answers; 76 | } 77 | 78 | /** 79 | * @throws AssertionFailedException 80 | */ 81 | public function validate(): void 82 | { 83 | Assertion::notBlank($this->title); 84 | Assertion::minCount($this->answers, 2); 85 | Assertion::allNotBlank(array_map(fn (array $answer) => $answer["title"], $this->answers)); 86 | Assertion::allBoolean(array_map(fn (array $answer) => $answer["good"], $this->answers)); 87 | Assertion::minCount(array_filter($this->answers, fn (array $answer) => $answer["good"]), 1); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /domain/src/Quiz/Response/CreateResponse.php: -------------------------------------------------------------------------------- 1 | question = $question; 25 | } 26 | 27 | /** 28 | * @return Question 29 | */ 30 | public function getQuestion(): Question 31 | { 32 | return $this->question; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /domain/src/Quiz/Response/UpdateResponse.php: -------------------------------------------------------------------------------- 1 | question = $question; 25 | } 26 | 27 | /** 28 | * @return Question 29 | */ 30 | public function getQuestion(): Question 31 | { 32 | return $this->question; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /domain/src/Quiz/UseCase/Create.php: -------------------------------------------------------------------------------- 1 | questionGateway = $questionGateway; 30 | } 31 | 32 | /** 33 | * @param CreateRequest $request 34 | * @param CreatePresenterInterface $presenter 35 | * @throws AssertionFailedException 36 | */ 37 | public function execute(CreateRequest $request, CreatePresenterInterface $presenter) 38 | { 39 | $request->validate(); 40 | 41 | $question = Question::fromCreate($request); 42 | 43 | $this->questionGateway->create($question); 44 | 45 | $presenter->present(new CreateResponse($question)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /domain/src/Quiz/UseCase/Update.php: -------------------------------------------------------------------------------- 1 | questionGateway = $questionGateway; 31 | } 32 | 33 | /** 34 | * @param UpdateRequest $request 35 | * @param UpdatePresenterInterface $presenter 36 | * @throws AssertionFailedException 37 | */ 38 | public function execute(UpdateRequest $request, UpdatePresenterInterface $presenter) 39 | { 40 | $request->validate(); 41 | 42 | $question = $this->questionGateway->getQuestionById($request->getId()); 43 | $question->setTitle($request->getTitle()); 44 | $question->setAnswers( 45 | array_map(fn (array $answer) => Answer::fromArray($answer), $request->getAnswers()) 46 | ); 47 | 48 | $this->questionGateway->update($question); 49 | 50 | $presenter->present(new UpdateResponse($question)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /domain/src/Security/Assert/Assertion.php: -------------------------------------------------------------------------------- 1 | isPseudoUnique($pseudo)) { 26 | throw new NonUniqueEmailException("This email should be unique !", self::EXISTING_PSEUDO); 27 | } 28 | } 29 | 30 | /** 31 | * @param string $email 32 | * @param ParticipantGateway $participantGateway 33 | */ 34 | public static function nonUniqueEmail(string $email, ParticipantGateway $participantGateway): void 35 | { 36 | if (!$participantGateway->isEmailUnique($email)) { 37 | throw new NonUniqueEmailException("This email should be unique !", self::EXISTING_EMAIL); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /domain/src/Security/Exception/NonUniqueEmailException.php: -------------------------------------------------------------------------------- 1 | email = $email; 27 | } 28 | 29 | public static function create(string $email): self 30 | { 31 | return new self($email); 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getEmail(): string 38 | { 39 | return $this->email; 40 | } 41 | 42 | /** 43 | * @throws AssertionFailedException 44 | */ 45 | public function validate(): void 46 | { 47 | Assertion::notBlank($this->email); 48 | Assertion::email($this->email); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /domain/src/Security/Request/LoginRequest.php: -------------------------------------------------------------------------------- 1 | email = $email; 41 | $this->password = $password; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getEmail(): string 48 | { 49 | return $this->email; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getPassword(): string 56 | { 57 | return $this->password; 58 | } 59 | 60 | public function validate(): void 61 | { 62 | Assertion::notBlank($this->email, "Email should not be blank."); 63 | Assertion::notBlank($this->password, "Password should not be blank."); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /domain/src/Security/Request/RecoverPasswordRequest.php: -------------------------------------------------------------------------------- 1 | email = $email; 39 | $this->newPlainPassword = $newPlainPassword; 40 | $this->token = $token; 41 | } 42 | 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getEmail(): string 48 | { 49 | return $this->email; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getNewPlainPassword(): string 56 | { 57 | return $this->newPlainPassword; 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getToken(): string 64 | { 65 | return $this->token; 66 | } 67 | 68 | /** 69 | * @throws AssertionFailedException 70 | */ 71 | public function validate(): void 72 | { 73 | Assertion::notBlank($this->email); 74 | //Assertion::email($this->email); 75 | Assertion::notBlank($this->token); 76 | Assertion::notBlank($this->newPlainPassword); 77 | Assertion::minLength($this->newPlainPassword, 8); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /domain/src/Security/Response/AskPasswordResetResponse.php: -------------------------------------------------------------------------------- 1 | participant = $participant; 25 | } 26 | 27 | /** 28 | * @return Participant 29 | */ 30 | public function getParticipant(): Participant 31 | { 32 | return $this->participant; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /domain/src/Security/Response/LoginResponse.php: -------------------------------------------------------------------------------- 1 | participant = $participant; 31 | $this->passwordValid = $passwordValid; 32 | } 33 | 34 | /** 35 | * @return Participant|null 36 | */ 37 | public function getParticipant(): ?Participant 38 | { 39 | return $this->participant; 40 | } 41 | 42 | /** 43 | * @return bool 44 | */ 45 | public function isPasswordValid(): bool 46 | { 47 | return $this->passwordValid; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /domain/src/Security/Response/RecoverPasswordResponse.php: -------------------------------------------------------------------------------- 1 | participant = $participant; 25 | } 26 | 27 | /** 28 | * @return Participant 29 | */ 30 | public function getParticipant(): Participant 31 | { 32 | return $this->participant; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /domain/src/Security/Response/RegistrationResponse.php: -------------------------------------------------------------------------------- 1 | email = $email; 26 | } 27 | 28 | /** 29 | * @return string 30 | */ 31 | public function getEmail(): string 32 | { 33 | return $this->email; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /domain/src/Security/UseCase/Login.php: -------------------------------------------------------------------------------- 1 | participant = $participant; 29 | } 30 | 31 | /** 32 | * @param LoginRequest $request 33 | * @param LoginPresenterInterface $presenter 34 | */ 35 | public function execute(LoginRequest $request, LoginPresenterInterface $presenter) 36 | { 37 | $request->validate(); 38 | 39 | $participant = $this->participant->getParticipantByEmail($request->getEmail()); 40 | 41 | if ($participant) { 42 | $passwordValid = password_verify($request->getPassword(), $participant->getPassword()); 43 | } 44 | 45 | $presenter->present(new LoginResponse($participant, $passwordValid ?? false)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /domain/src/Security/UseCase/Registration.php: -------------------------------------------------------------------------------- 1 | userGateway = $participantGateway; 32 | } 33 | 34 | /** 35 | * @param RegistrationRequest $request 36 | * @param RegistrationPresenterInterface $presenter 37 | */ 38 | public function execute(RegistrationRequest $request, RegistrationPresenterInterface $presenter) 39 | { 40 | $request->validate($this->userGateway); 41 | $user = Participant::fromRegistration($request); 42 | $this->userGateway->register($user); 43 | $presenter->present(new RegistrationResponse($user->getEmail())); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /domain/src/System/Gateway/LogGateway.php: -------------------------------------------------------------------------------- 1 | log = $log; 25 | } 26 | 27 | /** 28 | * @return Log 29 | */ 30 | public function getLog(): Log 31 | { 32 | return $this->log; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /domain/src/System/UseCase/Track.php: -------------------------------------------------------------------------------- 1 | participantGateway = $participantGateway; 37 | $this->logGateway = $logGateway; 38 | } 39 | 40 | /** 41 | * @param TrackRequest $request 42 | * @param TrackPresenterInterface $presenter 43 | */ 44 | public function execute(TrackRequest $request, TrackPresenterInterface $presenter) 45 | { 46 | $participant = $request->getEmail() === null 47 | ? null 48 | : $this->participantGateway->getParticipantByEmail($request->getEmail()) 49 | ; 50 | 51 | $log = Log::fromTrack($request, $participant); 52 | 53 | $this->logGateway->insert($log); 54 | 55 | $presenter->present(new TrackResponse($log)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /domain/tests/Fixtures/Adapter/LogRepository.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | domain/ 14 | src/ 15 | tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | domain/tests 22 | 23 | 24 | tests/IntegrationTests 25 | 26 | 27 | tests/SystemTests 28 | 29 | 30 | tests/EndToEndTests 31 | 32 | 33 | 34 | 35 | 36 | domain/src 37 | src 38 | 39 | src 40 | src/Infrastructure/Maker 41 | src/Kernel.php 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handle($request); 26 | $response->send(); 27 | $kernel->terminate($request, $response); 28 | -------------------------------------------------------------------------------- /src/Infrastructure/Adapter/Provider/MailProvider.php: -------------------------------------------------------------------------------- 1 | bus = $bus; 41 | $this->fromEmail = $fromEmail; 42 | $this->displayName = $displayName; 43 | } 44 | 45 | /** 46 | * @param string $email 47 | * @param string $pseudo 48 | * @param string $link 49 | */ 50 | public function sendPasswordResetLink(string $email, string $pseudo, string $link): void 51 | { 52 | $email = (new NotificationEmail()) 53 | ->from( 54 | new Address($this->fromEmail, $this->displayName) 55 | ) 56 | ->to( 57 | new Address($email, $pseudo) 58 | ) 59 | ->subject("[ Code Challenge ] Réinitialisation de mot de passe") 60 | ->htmlTemplate('emails/password_reset_request.html.twig') 61 | ->context([ 62 | 'pseudo' => $pseudo, 63 | 'link' => $link, 64 | ]) 65 | ; 66 | 67 | $message = new SendEmailMessage($email); 68 | 69 | $this->bus->dispatch($message); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Infrastructure/Adapter/Repository/LogRepository.php: -------------------------------------------------------------------------------- 1 | setIp($log->getIp()); 34 | $doctrineLog->setMethod($log->getMethod()); 35 | if ($log->getParticipant() !== null) { 36 | $doctrineLog->setParticipant( 37 | $this->_em->getRepository(DoctrineParticipant::class)->find($log->getParticipant()->getId()) 38 | ); 39 | } 40 | $doctrineLog->setQueryData($log->getQueryData()); 41 | $doctrineLog->setRequestData($log->getRequestData()); 42 | $doctrineLog->setRoute($log->getRoute()); 43 | $doctrineLog->setRouteParams($log->getRouteParams()); 44 | $doctrineLog->setLoggedAt($log->getLoggedAt()); 45 | $this->_em->persist($doctrineLog); 46 | $this->_em->flush(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/DataFixtures/QuestionFixtures.php: -------------------------------------------------------------------------------- 1 | setId(Uuid::uuid4()); 25 | $question->setTitle("title"); 26 | $answers = []; 27 | for ($i = 1; $i <= 4; $i++) { 28 | $answer = new DoctrineAnswer(); 29 | $answer->setId(Uuid::uuid4()); 30 | $answer->setTitle("title"); 31 | $answer->setGood($i % 2); 32 | $answer->setQuestion($question); 33 | $answers[] = $answer; 34 | } 35 | $question->setAnswers($answers); 36 | $manager->persist($question); 37 | $manager->flush(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/DataFixtures/UserFixtures.php: -------------------------------------------------------------------------------- 1 | setId(Uuid::uuid4()); 23 | $user->setPseudo("used_pseudo"); 24 | $user->setEmail("used@email.com"); 25 | $user->setPassword(password_hash("password", PASSWORD_ARGON2I)); 26 | // $user->setPasswordResetToken('bb4b5730-6057-4fa1-a27b-692b9ba8c14a'); 27 | // $user->setPasswordResetRequestedAt(new \DateTimeImmutable()); 28 | $manager->persist($user); 29 | $manager->flush(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/Entity/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/src/Infrastructure/Doctrine/Entity/.gitignore -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/Entity/DoctrineAnswer.php: -------------------------------------------------------------------------------- 1 | id; 50 | } 51 | 52 | /** 53 | * @param UuidInterface $id 54 | */ 55 | public function setId(UuidInterface $id): void 56 | { 57 | $this->id = $id; 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getTitle(): string 64 | { 65 | return $this->title; 66 | } 67 | 68 | /** 69 | * @param string $title 70 | */ 71 | public function setTitle(string $title): void 72 | { 73 | $this->title = $title; 74 | } 75 | 76 | /** 77 | * @return bool 78 | */ 79 | public function isGood(): bool 80 | { 81 | return $this->good; 82 | } 83 | 84 | /** 85 | * @param bool $good 86 | */ 87 | public function setGood(bool $good): void 88 | { 89 | $this->good = $good; 90 | } 91 | 92 | /** 93 | * @return DoctrineQuestion|null 94 | */ 95 | public function getQuestion(): ?DoctrineQuestion 96 | { 97 | return $this->question; 98 | } 99 | 100 | /** 101 | * @param null|DoctrineQuestion $question 102 | */ 103 | public function setQuestion(?DoctrineQuestion $question): void 104 | { 105 | $this->question = $question; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/Entity/DoctrineQuestion.php: -------------------------------------------------------------------------------- 1 | answers = new ArrayCollection(); 45 | } 46 | 47 | 48 | /** 49 | * @return UuidInterface 50 | */ 51 | public function getId(): UuidInterface 52 | { 53 | return $this->id; 54 | } 55 | 56 | /** 57 | * @param UuidInterface $id 58 | */ 59 | public function setId(UuidInterface $id): void 60 | { 61 | $this->id = $id; 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function getTitle(): string 68 | { 69 | return $this->title; 70 | } 71 | 72 | /** 73 | * @param string $title 74 | */ 75 | public function setTitle(string $title): void 76 | { 77 | $this->title = $title; 78 | } 79 | 80 | /** 81 | * @return Collection 82 | */ 83 | public function getAnswers(): Collection 84 | { 85 | return $this->answers; 86 | } 87 | 88 | /** 89 | * @param array $answers 90 | */ 91 | public function setAnswers(array $answers): void 92 | { 93 | $this->answers = new ArrayCollection($answers); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/Migrations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/src/Infrastructure/Doctrine/Migrations/.gitignore -------------------------------------------------------------------------------- /src/Infrastructure/EventSubscriber/KernelSubscriber.php: -------------------------------------------------------------------------------- 1 | messageBus = $messageBus; 36 | $this->security = $security; 37 | } 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | public static function getSubscribedEvents() 43 | { 44 | return [ 45 | KernelEvents::REQUEST => "onRequest" 46 | ]; 47 | } 48 | 49 | /** 50 | * @param RequestEvent $event 51 | */ 52 | public function onRequest(RequestEvent $event): void 53 | { 54 | if ( 55 | !$event->isMasterRequest() 56 | || $event->getRequest()->attributes->get("_route") === null 57 | || $event->getRequest()->attributes->get("_route") === "_wdt" 58 | || $event->getRequest()->attributes->get("_route") === "_profiler" 59 | ) { 60 | return; 61 | } 62 | 63 | $trackRequest = new TrackRequest( 64 | $event->getRequest()->getMethod(), 65 | $event->getRequest()->attributes->get("_route"), 66 | $event->getRequest()->attributes->get("_route_params", []), 67 | $event->getRequest()->request->all(), 68 | $event->getRequest()->query->all(), 69 | $event->getRequest()->getClientIp(), 70 | $this->security->isGranted("ROLE_USER") ? $this->security->getUser()->getUsername() : null 71 | ); 72 | 73 | $this->messageBus->dispatch($trackRequest); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Infrastructure/ParamConverter/QuestionConverter.php: -------------------------------------------------------------------------------- 1 | questionGateway = $questionGateway; 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | public function apply(Request $request, ParamConverter $configuration) 36 | { 37 | $request->attributes->set( 38 | "domainQuestion", 39 | $this->questionGateway->getQuestionById(Uuid::fromString($request->get("id"))) 40 | ); 41 | } 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | public function supports(ParamConverter $configuration) 47 | { 48 | return $configuration->getClass() === Question::class; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Infrastructure/Resources/skeleton/presenter.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace ; 4 | 5 | use \; 6 | 7 | /** 8 | * Interface 9 | * @package 10 | */ 11 | interface 12 | { 13 | /** 14 | * @param $response 15 | */ 16 | public function present( $response): void; 17 | } 18 | -------------------------------------------------------------------------------- /src/Infrastructure/Resources/skeleton/request.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace ; 4 | 5 | /** 6 | * Class 7 | * @package 8 | */ 9 | class 10 | { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/Infrastructure/Resources/skeleton/response.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace ; 4 | 5 | /** 6 | * Class 7 | * @package 8 | */ 9 | class 10 | { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/Infrastructure/Resources/skeleton/test.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace ; 4 | 5 | use ; 6 | use ; 7 | use ; 8 | use ; 9 | use PHPUnit\Framework\TestCase; 10 | 11 | /** 12 | * Class 13 | * @package 14 | */ 15 | class extends TestCase 16 | { 17 | public function test(): void 18 | { 19 | $request = new (); 20 | 21 | $presenter = new class() implements { 22 | public $response; 23 | 24 | public function present( $response): void 25 | { 26 | $this->response = $response; 27 | } 28 | }; 29 | 30 | $useCase = new (); 31 | 32 | $useCase->execute($request, $presenter); 33 | 34 | $this->assertInstanceOf(::class, $presenter->response); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Infrastructure/Resources/skeleton/use_case.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace ; 4 | 5 | use \; 6 | use \; 7 | use \; 8 | 9 | /** 10 | * Class 11 | * @package 12 | */ 13 | class 14 | { 15 | /** 16 | * @param $request 17 | * @param $presenter 18 | */ 19 | public function execute( $request, $presenter) 20 | { 21 | $presenter->present(new ()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Infrastructure/Security/Provider/UserProvider.php: -------------------------------------------------------------------------------- 1 | participantGateway = $participantGateway; 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | public function loadUserByUsername(string $username) 36 | { 37 | return $this->getUserByUsername($username); 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public function refreshUser(UserInterface $user) 44 | { 45 | return $this->getUserByUsername($user->getUsername()); 46 | } 47 | 48 | /** 49 | * @param string $username 50 | * @return User 51 | */ 52 | private function getUserByUsername(string $username): User 53 | { 54 | $participant = $this->participantGateway->getParticipantByEmail($username); 55 | if ($participant === null) { 56 | throw new UsernameNotFoundException(); 57 | } 58 | 59 | return new User($participant); 60 | } 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | public function supportsClass(string $class) 66 | { 67 | return $class === User::class; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Infrastructure/Security/User.php: -------------------------------------------------------------------------------- 1 | participant = $participant; 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function getRoles() 32 | { 33 | return ['ROLE_USER']; 34 | } 35 | 36 | /** 37 | * @inheritDoc 38 | */ 39 | public function getPassword() 40 | { 41 | return $this->participant->getPassword(); 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | public function getSalt() 48 | { 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | public function getUsername() 55 | { 56 | return $this->participant->getEmail(); 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | public function eraseCredentials() 63 | { 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Infrastructure/Test/Adapter/MailProvider.php: -------------------------------------------------------------------------------- 1 | "integration"]), $server); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Infrastructure/Validator/NonUniqueEmail.php: -------------------------------------------------------------------------------- 1 | userGateway = $participantGateway; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function validate($value, Constraint $constraint) 33 | { 34 | if (!$this->userGateway->isEmailUnique($value)) { 35 | $this->context->buildViolation($constraint->message)->addViolation(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Infrastructure/Validator/NonUniquePseudo.php: -------------------------------------------------------------------------------- 1 | userGateway = $participantGateway; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function validate($value, Constraint $constraint) 33 | { 34 | if (!$this->userGateway->isPseudoUnique($value)) { 35 | $this->context->buildViolation($constraint->message)->addViolation(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/UserInterface/Controller/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/src/UserInterface/Controller/.gitkeep -------------------------------------------------------------------------------- /src/UserInterface/Controller/LoginController.php: -------------------------------------------------------------------------------- 1 | twig = $twig; 28 | } 29 | 30 | /** 31 | * @param AuthenticationUtils $authenticationUtils 32 | * @return Response 33 | * @throws \Twig\Error\LoaderError 34 | * @throws \Twig\Error\RuntimeError 35 | * @throws \Twig\Error\SyntaxError 36 | */ 37 | public function __invoke(AuthenticationUtils $authenticationUtils): Response 38 | { 39 | return new Response($this->twig->render("login.html.twig", [ 40 | "vm" => LoginViewModel::fromAuthenticationUtils($authenticationUtils) 41 | ])); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/UserInterface/Controller/LogoutController.php: -------------------------------------------------------------------------------- 1 | title = $answer->getTitle(); 31 | $newAnswer->good = $answer->isGood(); 32 | 33 | return $newAnswer; 34 | } 35 | 36 | /** 37 | * @return string|null 38 | */ 39 | public function getTitle(): ?string 40 | { 41 | return $this->title; 42 | } 43 | 44 | /** 45 | * @param string|null $title 46 | */ 47 | public function setTitle(?string $title): void 48 | { 49 | $this->title = $title; 50 | } 51 | 52 | /** 53 | * @return bool 54 | */ 55 | public function isGood(): bool 56 | { 57 | return $this->good; 58 | } 59 | 60 | /** 61 | * @param bool $good 62 | */ 63 | public function setGood(bool $good): void 64 | { 65 | $this->good = $good; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/UserInterface/DataTransferObject/Question.php: -------------------------------------------------------------------------------- 1 | title = $question->getTitle(); 32 | $newQuestion->answers = array_map( 33 | fn (DomainAnswer $answer) => Answer::fromDomainAnswer($answer), 34 | $question->getAnswers() 35 | ); 36 | 37 | return $newQuestion; 38 | } 39 | 40 | /** 41 | * @return string|null 42 | */ 43 | public function getTitle(): ?string 44 | { 45 | return $this->title; 46 | } 47 | 48 | /** 49 | * @param string|null $title 50 | */ 51 | public function setTitle(?string $title): void 52 | { 53 | $this->title = $title; 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function getAnswers(): array 60 | { 61 | return $this->answers; 62 | } 63 | 64 | /** 65 | * @param array $answers 66 | */ 67 | public function setAnswers(array $answers): void 68 | { 69 | $this->answers = $answers; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/UserInterface/DataTransferObject/RecoverPasswordData.php: -------------------------------------------------------------------------------- 1 | newPlainPassword; 22 | } 23 | 24 | /** 25 | * @param string $newPlainPassword 26 | */ 27 | public function setNewPlainPassword(string $newPlainPassword): void 28 | { 29 | $this->newPlainPassword = $newPlainPassword; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/UserInterface/DataTransferObject/Registration.php: -------------------------------------------------------------------------------- 1 | email; 32 | } 33 | 34 | /** 35 | * @param string|null $email 36 | */ 37 | public function setEmail(?string $email): void 38 | { 39 | $this->email = $email; 40 | } 41 | 42 | /** 43 | * @return string|null 44 | */ 45 | public function getPseudo(): ?string 46 | { 47 | return $this->pseudo; 48 | } 49 | 50 | /** 51 | * @param string|null $pseudo 52 | */ 53 | public function setPseudo(?string $pseudo): void 54 | { 55 | $this->pseudo = $pseudo; 56 | } 57 | 58 | /** 59 | * @return string|null 60 | */ 61 | public function getPlainPassword(): ?string 62 | { 63 | return $this->plainPassword; 64 | } 65 | 66 | /** 67 | * @param string|null $plainPassword 68 | */ 69 | public function setPlainPassword(?string $plainPassword): void 70 | { 71 | $this->plainPassword = $plainPassword; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/UserInterface/DataTransferObject/ResetPasswordData.php: -------------------------------------------------------------------------------- 1 | email; 22 | } 23 | 24 | /** 25 | * @param string $email 26 | */ 27 | public function setEmail(string $email): void 28 | { 29 | $this->email = $email; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/UserInterface/Form/AnswerType.php: -------------------------------------------------------------------------------- 1 | add("good", CheckboxType::class, [ 26 | "required" => false 27 | ]) 28 | ->add("title", TextType::class, [ 29 | "label" => false, 30 | 'attr' => [ 31 | "placeholder" => "Votre réponse..." 32 | ], 33 | "constraints" => [ 34 | new NotBlank() 35 | ] 36 | ]) 37 | ; 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public function configureOptions(OptionsResolver $resolver) 44 | { 45 | $resolver->setDefault("data_class", Answer::class); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/UserInterface/Form/QuestionType.php: -------------------------------------------------------------------------------- 1 | add("title", TextType::class, [ 32 | "label" => "Titre de la question", 33 | "constraints" => [ 34 | new NotBlank() 35 | ] 36 | ]) 37 | ->add("answers", CollectionType::class, [ 38 | "label" => "Liste des réponses", 39 | "entry_type" => AnswerType::class, 40 | "allow_add" => true, 41 | "allow_delete" => true, 42 | "constraints" => [ 43 | new Valid(), 44 | new Count(["min" => 2]), 45 | new Callback(["callback" => function ($values, ExecutionContextInterface $context) { 46 | $goodAnswers = array_filter( 47 | $values, 48 | fn (Answer $answer) => $answer->isGood() 49 | ); 50 | 51 | if (count($goodAnswers) < 1) { 52 | $context 53 | ->buildViolation('Vous devez sélectionner au moins une bonne réponse.') 54 | ->addViolation() 55 | ; 56 | } 57 | }]) 58 | ] 59 | ]) 60 | ; 61 | } 62 | 63 | /** 64 | * @inheritDoc 65 | */ 66 | public function configureOptions(OptionsResolver $resolver) 67 | { 68 | $resolver->setDefault("data_class", Question::class); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/UserInterface/Form/RecoverPasswordType.php: -------------------------------------------------------------------------------- 1 | add("newPlainPassword", RepeatedType::class, [ 22 | "type" => PasswordType::class, 23 | "first_options" => [ 24 | "label" => "Mot de passe :", 25 | "help" => "Votre mot de passe doit contenir au minimum 8 caractères." 26 | ], 27 | "second_options" => [ 28 | "label" => "Confirmez votre mot de passe :", 29 | "help" => "Saisissez de nouveau votre mot de passe." 30 | ], 31 | "invalid_message" => "La confirmation doit être similaire au mot de passe", 32 | "constraints" => [ 33 | new NotBlank(), 34 | new Length(["min" => 8]) 35 | ] 36 | ]) 37 | ; 38 | } 39 | 40 | /** 41 | * @param OptionsResolver $resolver 42 | */ 43 | public function configureOptions(OptionsResolver $resolver) 44 | { 45 | $resolver->setDefault("data_class", RecoverPasswordData::class); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/UserInterface/Form/ResetPasswordType.php: -------------------------------------------------------------------------------- 1 | add("email", EmailType::class, [ 27 | "label" => "Email :", 28 | "constraints" => [ 29 | new NotBlank(), 30 | new Email() 31 | ], 32 | "help" => "Veuillez saisir une adresse email valide, ex: xyz@email.com." 33 | ]) 34 | ; 35 | } 36 | 37 | /** 38 | * @param OptionsResolver $resolver 39 | */ 40 | public function configureOptions(OptionsResolver $resolver) 41 | { 42 | $resolver->setDefault("data_class", ResetPasswordData::class); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/UserInterface/MessageHandler/TrackHandler.php: -------------------------------------------------------------------------------- 1 | track = $track; 28 | } 29 | 30 | /** 31 | * @param TrackRequest $trackRequest 32 | */ 33 | public function __invoke(TrackRequest $trackRequest) 34 | { 35 | $this->track->execute($trackRequest, new TrackPresenter()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/UserInterface/Presenter/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBoileau/code-challenge/d9f33144f1c1d85093e004d048de58a1d3637e16/src/UserInterface/Presenter/.gitkeep -------------------------------------------------------------------------------- /src/UserInterface/Presenter/Question/CreatePresenter.php: -------------------------------------------------------------------------------- 1 | flashBag = $flashBag; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function present(CreateResponse $response): void 33 | { 34 | $this->flashBag->add( 35 | "success", 36 | sprintf("Votre question '%s' a été ajoutée avec succès !", $response->getQuestion()->getTitle()) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/UserInterface/Presenter/Question/UpdatePresenter.php: -------------------------------------------------------------------------------- 1 | flashBag = $flashBag; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | public function present(UpdateResponse $response): void 33 | { 34 | $this->flashBag->add( 35 | "success", 36 | sprintf("Votre question '%s' a été modifiée avec succès !", $response->getQuestion()->getTitle()) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/UserInterface/Presenter/RegistrationPresenter.php: -------------------------------------------------------------------------------- 1 | flashBag = $flashBag; 40 | $this->userProvider = $userProvider; 41 | } 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | public function present(RegistrationResponse $response): void 47 | { 48 | $this->viewModel = new RegistrationViewModel($this->userProvider->loadUserByUsername($response->getEmail())); 49 | 50 | $this->flashBag->add( 51 | "success", 52 | "Bienvenue sur Code Challenge ! Votre inscription a été effectuée avec succès !" 53 | ); 54 | } 55 | 56 | /** 57 | * @return RegistrationViewModel 58 | */ 59 | public function getViewModel(): RegistrationViewModel 60 | { 61 | return $this->viewModel; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/UserInterface/Presenter/Security/AskPasswordResetPresenter.php: -------------------------------------------------------------------------------- 1 | flashBag = $flashBag; 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | public function present(AskPasswordResetResponse $response): void 30 | { 31 | $this->flashBag->add( 32 | "success", 33 | "Un lien de réinitialisation de votre mot de passe a été envoyé à l'adresse email fournie !" 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/UserInterface/Presenter/Security/RecoverPasswordPresenter.php: -------------------------------------------------------------------------------- 1 | flashBag = $flashBag; 20 | } 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | public function present(RecoverPasswordResponse $response): void 26 | { 27 | $this->flashBag->add( 28 | "success", 29 | "Mot de passe changer avec succès !" 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/UserInterface/Presenter/TrackPresenter.php: -------------------------------------------------------------------------------- 1 | getLastUsername(), 32 | $authenticationUtils->getLastAuthenticationError() 33 | ); 34 | } 35 | 36 | /** 37 | * LoginViewModel constructor. 38 | * @param string $lastUsername 39 | * @param AuthenticationException|null $exception 40 | */ 41 | public function __construct(string $lastUsername, ?AuthenticationException $exception) 42 | { 43 | $this->lastUsername = $lastUsername; 44 | $this->errorMessage = $exception !== null ? $exception->getMessage() : null; 45 | } 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function getLastUsername(): string 51 | { 52 | return $this->lastUsername; 53 | } 54 | 55 | /** 56 | * @return string|null 57 | */ 58 | public function getErrorMessage(): ?string 59 | { 60 | return $this->errorMessage; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/UserInterface/ViewModel/RegistrationViewModel.php: -------------------------------------------------------------------------------- 1 | user = $user; 25 | } 26 | 27 | /** 28 | * @return UserInterface 29 | */ 30 | public function getUser(): UserInterface 31 | { 32 | return $this->user; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/UserInterface/ViewModel/Security/AskPasswordResetViewModel.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Code Challenge{% endblock %} 6 | {% block stylesheets %} 7 | {{ encore_entry_link_tags('app') }} 8 | {% endblock %} 9 | 10 | 11 |
12 |
13 | 38 |
39 |
40 |
41 | {% block body %}{% endblock %} 42 |
43 | 50 | {% include 'components/flash_messages.html.twig' %} 51 | {% block javascripts %} 52 | {{ encore_entry_script_tags('app') }} 53 | {% endblock %} 54 | 55 | 56 | -------------------------------------------------------------------------------- /templates/components/flash_messages.html.twig: -------------------------------------------------------------------------------- 1 | {% set flahses = app.flashes %} 2 | {% if flahses|length > 0 %} 3 |
4 | {% set delay = 5000 %} 5 | {% for type, messages in flahses %} 6 | {% for message in messages %} 7 |
8 | {{ message }} 9 |
10 | {% set delay = delay + 1000 %} 11 | {% endfor %} 12 | {% endfor %} 13 |
14 | {% endif %} -------------------------------------------------------------------------------- /templates/emails/password_reset_request.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@email/default/notification/body.html.twig' %} 2 | 3 | {% block content %} 4 |

Bonjour {{ pseudo }}.

5 | 6 |

Une demande de réinitialisation de ton mot de passe a été initée. Si tu en es l'auteur, clique sur le lien ci-dessous pour changer 7 | ton mot de passe. Sinon, prière ignorer cet email et notifier l'équipe de Code Challenge.

8 | 9 |

Cordialement,

10 | {% endblock %} 11 | 12 | {% block action %} 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/home.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "base.html.twig" %} 2 | 3 | {% block body %} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /templates/login.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "base.html.twig" %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 |
8 |
Connexion
9 |
10 | {% if vm.errorMessage is not null %} 11 |
12 | {{ vm.errorMessage }} 13 |
14 | {% endif %} 15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 | 26 |
27 | 30 |
31 | 32 |
33 |
34 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /templates/question/_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_errors(form) }} 2 | {{ form_widget(form._token) }} 3 | {{ form_row(form.title) }} 4 |
5 | {{ form_label(form.answers) }} 6 | 7 |
8 | -------------------------------------------------------------------------------- /templates/question/_prototype.html.twig: -------------------------------------------------------------------------------- 1 |
  • 2 | 5 | {{ form_widget(form.title, { attr: { class: "Flex--grow-1" }}) }} 6 | 7 | {{ form_errors(form.title) }} 8 |
  • -------------------------------------------------------------------------------- /templates/question/create.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "base.html.twig" %} 2 | 3 | {% block body %} 4 |
    5 |
    6 |
    7 | {{ form_start(form) }} 8 |
    9 |
    Créer une question
    10 |
    11 | {% include "question/_form.html.twig" with { form: form } %} 12 |
    13 | 16 |
    17 | {{ form_end(form, { render_rest: false }) }} 18 |
    19 |
    20 |
    21 | {% endblock %} -------------------------------------------------------------------------------- /templates/question/update.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "base.html.twig" %} 2 | 3 | {% block body %} 4 |
    5 |
    6 |
    7 | {{ form_start(form) }} 8 |
    9 |
    Modifier une question
    10 |
    11 | {% include "question/_form.html.twig" with { form: form } %} 12 |
    13 | 16 |
    17 | {{ form_end(form, { render_rest: false }) }} 18 |
    19 |
    20 |
    21 | {% endblock %} -------------------------------------------------------------------------------- /templates/registration.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "base.html.twig" %} 2 | 3 | {% block body %} 4 |
    5 |
    6 |
    7 | {{ form_start(form) }} 8 |
    9 |
    Inscription
    10 |
    11 | {{ form_rest(form) }} 12 |
    13 | 16 |
    17 | {{ form_end(form) }} 18 |
    19 |
    20 |
    21 | {% endblock %} -------------------------------------------------------------------------------- /templates/security/change_password.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "base.html.twig" %} 2 | 3 | {% block body %} 4 |
    5 |
    6 |
    7 | {{ form_start(form) }} 8 |
    9 |
    Changer de mot de passe
    10 |
    11 | {{ form_rest(form) }} 12 |
    13 | 16 |
    17 | {{ form_end(form) }} 18 |
    19 |
    20 |
    21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/security/reset_password.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "base.html.twig" %} 2 | 3 | {% block body %} 4 |
    5 |
    6 |
    7 | {{ form_start(form) }} 8 |
    9 |
    Mot de passe oublié ?
    10 |
    11 | {{ form_rest(form) }} 12 |
    13 | 16 |
    17 | {{ form_end(form) }} 18 |
    19 |
    20 |
    21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /tests/EndToEndTests/ParticipantTest.php: -------------------------------------------------------------------------------- 1 | request(Request::METHOD_GET, '/login'); 19 | 20 | $form = $crawler->filter("form")->form([ 21 | "username" => "used@email.com", 22 | "password" => "password" 23 | ]); 24 | 25 | $client->submit($form); 26 | 27 | $this->assertSelectorTextContains( 28 | '.FlashBag', 29 | 'Bon retour sur Code Challenge !' 30 | ); 31 | 32 | $crawler = $client->request(Request::METHOD_GET, '/questions/create'); 33 | 34 | $client->executeScript("document.querySelector('.Collection__Add').click()"); 35 | 36 | $client->executeScript("document.querySelector('.Collection__Add').click()"); 37 | 38 | $form = $crawler->filter("form")->form([ 39 | "question[title]" => "title", 40 | "question[answers][0][title]" => "title", 41 | "question[answers][0][good]" => 1, 42 | "question[answers][1][title]" => "title" 43 | ]); 44 | 45 | $client->submit($form); 46 | 47 | $this->assertSelectorTextContains( 48 | '.FlashBag', 49 | "Votre question 'title' a été ajoutée avec succès !" 50 | ); 51 | 52 | $client->clickLink("Déconnexion"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/EndToEndTests/VisitorTest.php: -------------------------------------------------------------------------------- 1 | request(Request::METHOD_GET, '/registration'); 19 | 20 | $form = $crawler->filter("form")->form([ 21 | "registration[email]" => "email@email.com", 22 | "registration[pseudo]" => "pseudo", 23 | "registration[plainPassword][first]" => "password", 24 | "registration[plainPassword][second]" => "password" 25 | ]); 26 | 27 | $client->submit($form); 28 | 29 | $this->assertSelectorTextContains( 30 | '.FlashBag', 31 | 'Bienvenue sur Code Challenge ! Votre inscription a été effectuée avec succès !' 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/IntegrationTests/AskPasswordResetTest.php: -------------------------------------------------------------------------------- 1 | request(Request::METHOD_GET, '/reset-password'); 21 | 22 | $this->assertResponseIsSuccessful(); 23 | 24 | $form = $crawler->filter("form")->form([ 25 | "reset_password[email]" => "used@email.com", 26 | ]); 27 | 28 | $client->submit($form); 29 | 30 | $this->assertResponseStatusCodeSame(Response::HTTP_FOUND); 31 | } 32 | 33 | /** 34 | * @dataProvider provideFormData 35 | * @param string $email 36 | * @param string $errorMessage 37 | */ 38 | public function testFailed(string $email, string $errorMessage) 39 | { 40 | $client = static::createClient(); 41 | 42 | $crawler = $client->request(Request::METHOD_GET, '/reset-password'); 43 | 44 | $this->assertResponseIsSuccessful(); 45 | 46 | $form = $crawler->filter("form")->form([ 47 | "reset_password[email]" => $email, 48 | ]); 49 | 50 | $client->submit($form); 51 | 52 | $this->assertResponseStatusCodeSame(Response::HTTP_OK); 53 | 54 | $this->assertSelectorTextContains('html', $errorMessage); 55 | } 56 | 57 | /** 58 | * @return Generator 59 | */ 60 | public function provideFormData(): Generator 61 | { 62 | yield [ 63 | "", 64 | "This value should not be blank." 65 | ]; 66 | 67 | yield [ 68 | "fail", 69 | "This value is not a valid email address." 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/IntegrationTests/LoginTest.php: -------------------------------------------------------------------------------- 1 | request(Request::METHOD_GET, '/login'); 21 | 22 | $this->assertResponseIsSuccessful(); 23 | 24 | $form = $crawler->filter("form")->form([ 25 | "username" => "used@email.com", 26 | "password" => "password" 27 | ]); 28 | 29 | $client->submit($form); 30 | 31 | $this->assertResponseStatusCodeSame(Response::HTTP_FOUND); 32 | } 33 | 34 | /** 35 | * @dataProvider provideFormData 36 | * @param string $email 37 | * @param string $password 38 | * @param string $errorMessage 39 | */ 40 | public function testFailed(string $email, string $password, string $errorMessage) 41 | { 42 | $client = static::createClient(); 43 | 44 | $crawler = $client->request(Request::METHOD_GET, '/login'); 45 | 46 | $this->assertResponseIsSuccessful(); 47 | 48 | $form = $crawler->filter("form")->form([ 49 | "username" => $email, 50 | "password" => $password 51 | ]); 52 | 53 | $client->submit($form); 54 | 55 | $this->assertResponseStatusCodeSame(Response::HTTP_FOUND); 56 | 57 | $client->followRedirect(); 58 | 59 | $this->assertSelectorTextContains('html', $errorMessage); 60 | } 61 | 62 | /** 63 | * @return Generator 64 | */ 65 | public function provideFormData(): Generator 66 | { 67 | yield [ 68 | "used@email.com", 69 | "fail", 70 | "Wrong credentials !" 71 | ]; 72 | 73 | yield [ 74 | "fail@email.com", 75 | "password", 76 | "User not found !" 77 | ]; 78 | 79 | yield [ 80 | "", 81 | "password", 82 | "Email should not be blank." 83 | ]; 84 | 85 | yield [ 86 | "fail@email.com", 87 | "", 88 | "Password should not be blank." 89 | ]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/SystemTests/AskPasswordResetTest.php: -------------------------------------------------------------------------------- 1 | request(Request::METHOD_GET, '/reset-password'); 21 | 22 | $this->assertResponseIsSuccessful(); 23 | 24 | $form = $crawler->filter("form")->form([ 25 | "reset_password[email]" => "used@email.com", 26 | ]); 27 | 28 | $client->submit($form); 29 | 30 | $this->assertResponseStatusCodeSame(Response::HTTP_FOUND); 31 | } 32 | 33 | /** 34 | * @dataProvider provideFormData 35 | * @param string $email 36 | * @param string $errorMessage 37 | */ 38 | public function testFailed(string $email, string $errorMessage) 39 | { 40 | $client = static::createClient(); 41 | 42 | $crawler = $client->request(Request::METHOD_GET, '/reset-password'); 43 | 44 | $this->assertResponseIsSuccessful(); 45 | 46 | $form = $crawler->filter("form")->form([ 47 | "reset_password[email]" => $email, 48 | ]); 49 | 50 | $client->submit($form); 51 | 52 | $this->assertResponseStatusCodeSame(Response::HTTP_OK); 53 | 54 | $this->assertSelectorTextContains('html', $errorMessage); 55 | } 56 | 57 | /** 58 | * @return Generator 59 | */ 60 | public function provideFormData(): Generator 61 | { 62 | yield [ 63 | "", 64 | "This value should not be blank." 65 | ]; 66 | 67 | yield [ 68 | "fail", 69 | "This value is not a valid email address." 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/SystemTests/LoginTest.php: -------------------------------------------------------------------------------- 1 | request(Request::METHOD_GET, '/login'); 22 | 23 | $this->assertResponseIsSuccessful(); 24 | 25 | $form = $crawler->filter("form")->form([ 26 | "username" => "used@email.com", 27 | "password" => "password" 28 | ]); 29 | 30 | $client->submit($form); 31 | 32 | $this->assertResponseStatusCodeSame(Response::HTTP_FOUND); 33 | } 34 | 35 | /** 36 | * @dataProvider provideFormData 37 | * @param string $email 38 | * @param string $password 39 | * @param string $errorMessage 40 | */ 41 | public function testFailed(string $email, string $password, string $errorMessage) 42 | { 43 | $client = static::createClient(); 44 | 45 | $crawler = $client->request(Request::METHOD_GET, '/login'); 46 | 47 | $this->assertResponseIsSuccessful(); 48 | 49 | $form = $crawler->filter("form")->form([ 50 | "username" => $email, 51 | "password" => $password 52 | ]); 53 | 54 | $client->submit($form); 55 | 56 | $this->assertResponseStatusCodeSame(Response::HTTP_FOUND); 57 | 58 | $client->followRedirect(); 59 | 60 | $this->assertSelectorTextContains('html', $errorMessage); 61 | } 62 | 63 | /** 64 | * @return Generator 65 | */ 66 | public function provideFormData(): Generator 67 | { 68 | yield [ 69 | "used@email.com", 70 | "fail", 71 | "Wrong credentials !" 72 | ]; 73 | 74 | yield [ 75 | "fail@email.com", 76 | "password", 77 | "User not found !" 78 | ]; 79 | 80 | yield [ 81 | "", 82 | "password", 83 | "Email should not be blank." 84 | ]; 85 | 86 | yield [ 87 | "fail@email.com", 88 | "", 89 | "Password should not be blank." 90 | ]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__) . '/.env'); 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "es6", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "declaration": false, 8 | "noImplicitAny": false, 9 | "jsx": "react", 10 | "sourceMap": true, 11 | "noLib": false, 12 | "suppressImplicitAnyIndexErrors": true 13 | }, 14 | "compileOnSave": true, 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var Encore = require('@symfony/webpack-encore'); 2 | 3 | // Manually configure the runtime environment if not already configured yet by the "encore" command. 4 | // It's useful when you use tools that rely on webpack.config.js file. 5 | if (!Encore.isRuntimeEnvironmentConfigured()) { 6 | Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); 7 | } 8 | 9 | Encore 10 | .setOutputPath('public/build/') 11 | .setPublicPath('/build') 12 | .addEntry('app', './assets/js/app.js') 13 | .splitEntryChunks() 14 | .enableSingleRuntimeChunk() 15 | .cleanupOutputBeforeBuild() 16 | .enableBuildNotifications() 17 | .enableSourceMaps(!Encore.isProduction()) 18 | .enableVersioning(Encore.isProduction()) 19 | .configureBabelPresetEnv((config) => { 20 | config.useBuiltIns = 'usage'; 21 | config.corejs = 3; 22 | }) 23 | .enableSassLoader() 24 | .enableTypeScriptLoader() 25 | .copyFiles({ 26 | from: './assets/images' 27 | }) 28 | ; 29 | 30 | module.exports = Encore.getWebpackConfig(); 31 | --------------------------------------------------------------------------------