Compare commits
	
		
			122 commits
		
	
	
		
			translatio
			...
			develop
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 80ef8caf20 | ||
| cc5e0b6b27 | |||
|  | a6e4b205f7 | ||
| 4e5b69450e | |||
|  | a543fd2a4a | ||
| e70157067c | |||
| 2d31f7cd76 | |||
| 1c172ec454 | |||
| 90d6fd0770 | |||
| 5eeb662d6f | |||
| 220d6a1f83 | |||
| 92c07b02cb | |||
| 48bf4f26c4 | |||
| c7b400cec3 | |||
| f86319f23c | |||
| 7b9542f0bd | |||
| 3d0ffa3311 | |||
| 42564e085c | |||
| 304c0d4ba6 | |||
| f87b3a0115 | |||
| e327966ef0 | |||
| 0c5f93bc36 | |||
| 54fa878115 | |||
| 34e5132c3d | |||
| 61b861b510 | |||
| a034a6118a | |||
| 3135c26345 | |||
| af12833642 | |||
| 1f140475b9 | |||
| d4a7bd4228 | |||
| f0c2bfae3a | |||
| 66e5631c16 | |||
| ee36881c49 | |||
| e4972d1b32 | |||
| 1296ca7cfb | |||
| 6b1406fde9 | |||
| 775dcfa681 | |||
| b47e2c1b14 | |||
| 93eef67209 | |||
| 865f8ec94f | |||
| 71ca261cab | |||
| f54f663fb0 | |||
| a7b4bf09e4 | |||
| 50e0245d4e | |||
| 46b5d4c107 | |||
| 38b6016e0d | |||
| 661c519439 | |||
| ed481d9083 | |||
| 9d640b9fd3 | |||
| 774630ea14 | |||
| b8c6d41dc9 | |||
| 9b3c970bba | |||
| 946b30b486 | |||
| ee063cd439 | |||
| d190971718 | |||
| 539b7edffe | |||
| 9a9bbb310a | |||
| b8a5ade3e0 | |||
| b20418b9ef | |||
| 2a5c9a000b | |||
| 5ba9e8cc85 | |||
| 8e7da7511a | |||
| 7a3749c105 | |||
| 9dbf0c72a1 | |||
| 3d14e10ced | |||
| f10512c46e | |||
| dc130dd9a4 | |||
| e52753446b | |||
| d0feff7d74 | |||
| 72418dd5a4 | |||
| 27474ad2ea | |||
| 00975df11b | |||
| a04af6d6aa | |||
| c3a9e13dd0 | |||
| c6eadf742c | |||
| 3a525f397c | |||
| 79c7a44123 | |||
| fa03bfde3d | |||
| f7126dba43 | |||
| cb90ba1eb3 | |||
| e01500ad0f | |||
| d0e65fc8cb | |||
| 16c34a0bf7 | |||
| a3bd7bf423 | |||
| 4260034fbe | |||
| 26acf70e1b | |||
| 63d2100c57 | |||
| faa16735f5 | |||
| 9e39334486 | |||
| a35e3e4f7a | |||
| a832f3fc97 | |||
| 750f5df4f3 | |||
| d9098182d4 | |||
| 6e240f95cf | |||
| 249cd9aa59 | |||
| 58dbba3b1d | |||
| 411a858089 | |||
| 1a627ab7d9 | |||
| e59f3c05e3 | |||
| 28f71ee479 | |||
| d1ba54c9f9 | |||
| 4ced33c694 | |||
| f2eed9d848 | |||
| d277d9c93e | |||
| b0b0125862 | |||
| b2428f6ae3 | |||
| 10e250a1da | |||
| 8f204a3685 | |||
| 36e804abce | |||
| b0bb249a7a | |||
| 529a9a62ec | |||
| 521217c00d | |||
| dde15b8306 | |||
| 5c25b036a4 | |||
| 34088972f8 | |||
| ddb815d1b4 | |||
| 4656be5a32 | |||
| 75ab34210f | |||
| df57a6f303 | |||
| 7f485cee91 | |||
| 1758011412 | |||
| 307e251388 | 
					 82 changed files with 13364 additions and 15461 deletions
				
			
		
							
								
								
									
										13
									
								
								.env
									
										
									
									
									
								
							
							
						
						
									
										13
									
								
								.env
									
										
									
									
									
								
							|  | @ -23,16 +23,17 @@ APP_SECRET=8a390490e448f181dd8d3e6bd38efe6a | |||
| # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml | ||||
| # | ||||
| # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" | ||||
| # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" | ||||
| DATABASE_URL="postgresql://postgres:develop@localhost:5432/plantex?serverVersion=13&charset=utf8" | ||||
| DATABASE_URL="mysql://pflaenzli:develop@127.0.0.1:3306/pflaenzli?serverVersion=mariadb-10.5.13" | ||||
| # DATABASE_URL="postgresql://postgres:develop@localhost:5432/pflaenzli?serverVersion=13&charset=utf8" | ||||
| ###< doctrine/doctrine-bundle ### | ||||
| 
 | ||||
| ###> symfony/mailer ### | ||||
| MAILER_DSN=smtp://localhost | ||||
| # MAILER_DSN=smtp://localhost | ||||
| ###< symfony/mailer ### | ||||
| 
 | ||||
| DEFAULT_URI='http://localhost:8080/' | ||||
| 
 | ||||
| ###> symfony/loco-translation-provider ### | ||||
| LOCO_DSN=loco://RCrByb141Z9P3QjMhWgHrqRrdxu9x-Rro@default | ||||
| ###< symfony/loco-translation-provider ### | ||||
| ###> Ffiendlycaptcha ### | ||||
| # CAPTCHA_SECRET= | ||||
| # CAPTCHA_SITEKEY= | ||||
| ###< Ffiendlycaptcha ### | ||||
|  |  | |||
							
								
								
									
										38
									
								
								.env.staging
									
										
									
									
									
								
							
							
						
						
									
										38
									
								
								.env.staging
									
										
									
									
									
								
							|  | @ -1,38 +0,0 @@ | |||
| # In all environments, the following files are loaded if they exist, | ||||
| # the latter taking precedence over the former: | ||||
| # | ||||
| #  * .env                contains default values for the environment variables needed by the app | ||||
| #  * .env.local          uncommitted file with local overrides | ||||
| #  * .env.$APP_ENV       committed environment-specific defaults | ||||
| #  * .env.$APP_ENV.local uncommitted environment-specific overrides | ||||
| # | ||||
| # Real environment variables win over .env files. | ||||
| # | ||||
| # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. | ||||
| # | ||||
| # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). | ||||
| # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration | ||||
| 
 | ||||
| ###> symfony/framework-bundle ### | ||||
| APP_ENV=dev | ||||
| APP_SECRET=8a390490e448f181dd8d3e6bd38efe6a | ||||
| ###< symfony/framework-bundle ### | ||||
| 
 | ||||
| ###> doctrine/doctrine-bundle ### | ||||
| # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url | ||||
| # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml | ||||
| # | ||||
| # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" | ||||
| # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" | ||||
| DATABASE_URL="postgresql://postgres:develop@localhost:5432/plantex?serverVersion=13&charset=utf8" | ||||
| ###< doctrine/doctrine-bundle ### | ||||
| 
 | ||||
| ###> symfony/mailer ### | ||||
| MAILER_DSN=smtp://no-reply@xn--pflnz-ira.li:dxS5ooKMEzFEa3YgTvru@mail.infomaniak.com:587 | ||||
| ###< symfony/mailer ### | ||||
| 
 | ||||
| DEFAULT_URI="https://staging.this-server.pflaenz.li/" | ||||
| 
 | ||||
| ###> symfony/loco-translation-provider ### | ||||
| LOCO_DSN=loco://RCrByb141Z9P3QjMhWgHrqRrdxu9x-Rro@default | ||||
| ###< symfony/loco-translation-provider ### | ||||
							
								
								
									
										5
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,4 +1,4 @@ | |||
| /public/uploads/* | ||||
| /public/uploads/photos/* | ||||
| .DS_Store | ||||
| /pgadmin_data | ||||
| 
 | ||||
|  | @ -244,4 +244,5 @@ temp/ | |||
| 
 | ||||
| # End of https://www.toptal.com/developers/gitignore/api/node,phpunit,symfony,composer,yarn | ||||
| 
 | ||||
| translations | ||||
| # Snyk | ||||
| .dccache | ||||
|  | @ -3,9 +3,9 @@ | |||
| ## Prerequisites | ||||
| A good start is to follow this [intro](https://symfony.com/doc/current/the-fast-track/en/1-tools.html). | ||||
| 
 | ||||
| In addition, you will need `yarn` and `npm`, as well as [`docker-compose`](https://docs.docker.com/compose/install/). | ||||
| In addition, you will need `yarn` and `npm`, as well as [`docker compose`](https://docs.docker.com/compose/install/). | ||||
| 
 | ||||
| For that, install [`node.js`](https://nodejs.org/) by downloading directly or using your package manager. | ||||
| For that, install [`node.js`](https://nodejs.org/) by downloading directly or using your package manager (if you're using Linux, you probably knpw how to install). | ||||
| 
 | ||||
| ### MacOS | ||||
| Using [homebrew](https://homebrew.sh): | ||||
|  | @ -22,28 +22,27 @@ choco install nodejs npm yarn | |||
| ## Setup the project | ||||
| 
 | ||||
| ### 1. Clone the repo | ||||
| Using SSH | ||||
| Using SSH ([set up your ssh-keys](https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key) first or use https) | ||||
| ``` | ||||
| git clone  ssh://git@git.thisfro.ch:222/thisfro/plant-exchange.git | ||||
| git clone  ssh://git@git.thisfro.ch:222/thisfro/pflaenz.li.git | ||||
| ``` | ||||
| 
 | ||||
| ### 2. Go to the directory | ||||
| ``` | ||||
| cd plant-exchange | ||||
| cd pflaenz.li | ||||
| ``` | ||||
| 
 | ||||
| ### 3. Setup Database and other stuff | ||||
| ``` | ||||
| docker-compose up -d | ||||
| docker compose up -d | ||||
| ``` | ||||
| To create the database (or use pgadmin): | ||||
| To create the database | ||||
| ``` | ||||
| docker exec -it plant-exchange_db_1 psql -U postgres -c 'CREATE DATABASE plantex;' | ||||
| ``` | ||||
| 
 | ||||
|  bin/console doctrine:schema:create | ||||
|  ``` | ||||
| and migrate | ||||
| ``` | ||||
| symfony console doctrine:migrations:migrate -n  | ||||
| bin/console doctrine:migrations:migrate -n  | ||||
| ``` | ||||
| 
 | ||||
| ### 4. Install dependencies | ||||
|  | @ -62,20 +61,22 @@ symfony serve --port 8080 --no-tls -d | |||
| 
 | ||||
| You sholud be able to access the site under [localhost:8080](http://localhost:8080) | ||||
| 
 | ||||
| You'll also need to build the webpack files (or use watch files): | ||||
| ``` | ||||
| yarn build | ||||
| ``` | ||||
| ### 6. Create a user | ||||
| Go to the database and add a user manually. For the password use this to generate the appropriate hash: | ||||
| ``` | ||||
| symfony console security:encode-password | ||||
| ``` | ||||
| Register your own account and set the `role` in the databse to `["ROLE_ADMIN"]` | ||||
| 
 | ||||
| ### 7. Watch files | ||||
| If you are editing `.scss` or other webpack files, you'll want to run | ||||
| If you are editing `.scss` or `.js`, you'll want to run | ||||
| ``` | ||||
| yarn watch | ||||
| ``` | ||||
| this will automatically rebuild webpack files on save. | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| Thats it for now, you can start developing! | ||||
| Thats it for now, you can start developing! :tada: | ||||
| 
 | ||||
| If you have any questions, ask [thisfro](https://git.thisfro.ch/thisfro) or creat an issue! | ||||
| If you have any questions, ask [thisfro](https://git.thisfro.ch/thisfro) or create an issue! | ||||
							
								
								
									
										35
									
								
								Dockerfile
									
										
									
									
									
								
							
							
						
						
									
										35
									
								
								Dockerfile
									
										
									
									
									
								
							|  | @ -1,35 +0,0 @@ | |||
| FROM php:8.0.6-fpm-buster | ||||
| 
 | ||||
| # Install prerequisites | ||||
| RUN apt-get update && apt-get -y install wget git npm curl zip | ||||
| RUN apt-get install -y libpq-dev \ | ||||
|     && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ | ||||
|     && docker-php-ext-install pdo pdo_pgsql pgsql | ||||
| 
 | ||||
| # Download composer | ||||
| RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \ | ||||
|     php -r "if (hash_file('sha384', 'composer-setup.php') === '756890a4488ce9024fc62c56153228907f1545c228516cbf63f885e036d37e9a59d27d63f46af1d4d07ee0f76181c7d3') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" && \ | ||||
|     php composer-setup.php && \ | ||||
|     php -r "unlink('composer-setup.php');" | ||||
| 
 | ||||
| # Install composer and yarn | ||||
| RUN npm install --global yarn | ||||
| RUN wget https://get.symfony.com/cli/installer -O - | bash | ||||
| 
 | ||||
| COPY . /var/www/html/app/ | ||||
| 
 | ||||
| WORKDIR /var/www/html/app | ||||
| 
 | ||||
| # Install stuff | ||||
| RUN php ../composer.phar update | ||||
| RUN yarn install && \ | ||||
|     yarn build | ||||
| 
 | ||||
| # Pull translation | ||||
| RUN curl "https://localise.biz/api/export/locale/en.yml?key=RCrByb141Z9P3QjMhWgHrqRrdxu9x-Rro&format=symfony" > translations/messages.en.yml | ||||
| RUN curl "https://localise.biz/api/export/locale/de.yml?key=RCrByb141Z9P3QjMhWgHrqRrdxu9x-Rro&format=symfony" > translations/messages.de.yml | ||||
| 
 | ||||
| RUN cp .env.staging .env | ||||
| 
 | ||||
| # Run Migration and dev-webserver | ||||
| CMD /root/.symfony/bin/symfony console doctrine:migrations:migrate && /root/.symfony/bin/symfony serve --port=9999 --no-tls | ||||
							
								
								
									
										39
									
								
								Jenkinsfile
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										39
									
								
								Jenkinsfile
									
										
									
									
										vendored
									
									
								
							|  | @ -2,30 +2,39 @@ node { | |||
|     def app | ||||
| 
 | ||||
|     stage('Clone repository') { | ||||
|         /* Let's make sure we have the repository cloned to our workspace */ | ||||
| 
 | ||||
|         // Let's make sure we have the repository cloned to our workspace | ||||
|         checkout scm | ||||
|     } | ||||
| 
 | ||||
|     stage('Build image') { | ||||
|         /* This builds the actual image; synonymous to | ||||
|          * docker build on the command line */ | ||||
| 
 | ||||
|         app = docker.build("thisfro/plantex") | ||||
|     stage('Install dependencies') { | ||||
|         // Install dependencies for build later | ||||
|         sh 'composer update' | ||||
|         sh 'yarn install' | ||||
|     } | ||||
| 
 | ||||
|     stage('Test image') { | ||||
|         /* Ideally, we would run a test framework against our image. | ||||
|          * For this example, we're using a Volkswagen-type approach ;-) */ | ||||
|     stage('Composer Vulnr test') { | ||||
|         snykSecurity( | ||||
|             snykInstallation: 'snyk-local', | ||||
|             targetFile: 'composer.lock', | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|         app.inside { | ||||
|             // php 'bin/phpunit' | ||||
|             sh 'echo "success"' | ||||
|         } | ||||
|     stage('npm vulnr test') { | ||||
|         snykSecurity( | ||||
|             snykInstallation: 'snyk-local', | ||||
|             targetFile: 'package.json', | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     stage('Deploy staging') { | ||||
|         sh 'docker-compose --project-directory /opt/plant-exchange --file /opt/plant-exchange/docker-compose.yml up -d' | ||||
|         // Deploy to staging host | ||||
|         sh 'vendor/bin/dep deploy lq5xi.ftp.infomaniak.com --no-interaction' | ||||
|     } | ||||
| 
 | ||||
|     /* | ||||
|     stage('Test staging') { | ||||
|         // Run phpunit tests on staging host | ||||
|         bin/phpunit COMMAND | ||||
|     } | ||||
|     */ | ||||
| } | ||||
							
								
								
									
										20
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										20
									
								
								README.md
									
										
									
									
									
								
							|  | @ -1,18 +1,27 @@ | |||
| # plant-exchange | ||||
| # Pflänz.li | ||||
| 
 | ||||
| ## Idea | ||||
| A platform where people can exchange plants. They can post what they have and search for others with [filters](#filters). | ||||
| A platform where people can trade their plants. You can post what you have and search for others with [filters](#filters). The aim is to make it easier to trade plants and collect as few data as possible. Only the email/username and a postal code is required. | ||||
| 
 | ||||
| ## Tech stack | ||||
| - [Symfony](https://symfony.com/) | ||||
| - [PostgreSQL](https://www.postgresql.org/), maybe should be using MySQL for easier deployment? | ||||
| - [MariaDB](https://www.mariadb.org) | ||||
| - [Deployer](https://deployer.org) | ||||
| 
 | ||||
| Deployment: TBD | ||||
| Can easily be depoyed to a LAMP server | ||||
| 
 | ||||
| ## Admin dashboard | ||||
| Find it under `/admin` | ||||
| 
 | ||||
| ## Filters | ||||
| 
 | ||||
| ### Implemented | ||||
| - Distance between postal codes | ||||
| - Search within title | ||||
| 
 | ||||
| ### Ideas | ||||
| It would be nice to have categories somehow, but it would be hard to make it comprehensive. | ||||
| 
 | ||||
| :warning: This list is work in progress! | ||||
| 
 | ||||
| Searching with filters such as: | ||||
|  | @ -22,4 +31,5 @@ Searching with filters such as: | |||
| | Name         | `string`      | textfield  | | ||||
| | Category     | `Category`    | dropdown   | | ||||
| 
 | ||||
| Distance from entered ZIP to the offer ZIP. | ||||
| ## Development | ||||
| To get started with development, see [DEVELOPMENT.md](DEVELOPMENT.md) | ||||
|  | @ -8,7 +8,6 @@ | |||
| // any CSS you import will output into a single css file (app.css in this case)
 | ||||
| import './styles/app.scss'; | ||||
| 
 | ||||
| const $ = require('jquery'); | ||||
| // start the Stimulus application
 | ||||
| require('bootstrap'); | ||||
| 
 | ||||
|  | @ -18,15 +17,32 @@ import '@fortawesome/fontawesome-free/js/solid' | |||
| import '@fortawesome/fontawesome-free/js/regular' | ||||
| import '@fortawesome/fontawesome-free/js/brands' | ||||
| 
 | ||||
| // Friendly captcha
 | ||||
| import "friendly-challenge/widget"; | ||||
| 
 | ||||
| // Dsiplay Filename when uploading
 | ||||
| document.querySelector('.custom-file-input').addEventListener('change',function(e){ | ||||
|     var fileName = document.getElementById("offering_form_photo").files[0].name; | ||||
|     var nextSibling = e.target.nextElementSibling | ||||
|     nextSibling.innerText = fileName | ||||
| }) | ||||
| 
 | ||||
| // Cookie-consent
 | ||||
| import 'cookie-notice/dist/cookie.notice.min.js' | ||||
| import 'cookie-notice/dist/cookie.notice.min'; | ||||
| 
 | ||||
| new cookieNoticeJS({ | ||||
|     // Position for the cookie-notifier (default=bottom)
 | ||||
|     'cookieNoticePosition': 'bottom', | ||||
| 
 | ||||
|     // The message will be shown again in X days
 | ||||
|     'expiresIn': 365, | ||||
| 
 | ||||
|     // Specify a custom font family and size in pixels
 | ||||
|     'fontFamily': 'inherit', | ||||
|     'fontSize': '.9rem', | ||||
| 
 | ||||
|     // Dismiss button background color
 | ||||
|     'buttonBgColor': '#343a40', | ||||
| 
 | ||||
|     // Dismiss button text color
 | ||||
|     'buttonTextColor': '#fff', | ||||
| 
 | ||||
|     // Notice background color
 | ||||
|     'noticeBgColor': '#000', | ||||
| 
 | ||||
|     // Notice text color
 | ||||
|     'noticeTextColor': '#fff', | ||||
| 
 | ||||
|     // Print debug output to the console (default=false)
 | ||||
|     'debug': false | ||||
| }); | ||||
							
								
								
									
										15
									
								
								assets/captcha.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								assets/captcha.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| // Friendly captcha
 | ||||
| import { WidgetInstance } from 'friendly-challenge'; | ||||
| const $ = require('jquery'); | ||||
| 
 | ||||
| function doneCallback(solution) { | ||||
|     $('#registration_form_captcha_solution').val(solution); | ||||
| } | ||||
| 
 | ||||
| const element = document.querySelector('#captcha'); | ||||
| const options = { | ||||
|     doneCallback: doneCallback, | ||||
|     sitekey: 'FCMVL79DP1G5K1K0', | ||||
| } | ||||
| const widget = new WidgetInstance(element, options); | ||||
| widget.start() | ||||
							
								
								
									
										5
									
								
								assets/fileUpload.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								assets/fileUpload.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| const $ = require('jquery'); | ||||
| 
 | ||||
| $( ".custom-file-input" ).change(function() { | ||||
|    $(".custom-file-label").html(($(".custom-file-input").prop("files")[0]["name"])); | ||||
| }); | ||||
|  | @ -8,7 +8,7 @@ $primary: darken(#005035, 20%); | |||
| 
 | ||||
| footer { | ||||
|     background-color: #ddd; | ||||
|     height: 6rem; | ||||
|     height: auto; | ||||
| } | ||||
| 
 | ||||
| nav { | ||||
|  | @ -16,7 +16,7 @@ nav { | |||
| } | ||||
| 
 | ||||
| .container { | ||||
|     min-height: calc(100vh - 10rem); | ||||
|     min-height: calc(100vh - 15rem); | ||||
| } | ||||
| 
 | ||||
| .offer-img { | ||||
|  | @ -68,6 +68,11 @@ nav { | |||
|     margin-bottom: 0 !important; | ||||
| } | ||||
| 
 | ||||
| .link-list { | ||||
|     list-style: none; | ||||
|     padding: 0; | ||||
| } | ||||
| 
 | ||||
| @include media-breakpoint-up(sm) { | ||||
|     .show-img-container { | ||||
|         margin-right: 2rem; | ||||
|  |  | |||
|  | @ -8,39 +8,47 @@ | |||
|         "ext-ctype": "*", | ||||
|         "ext-iconv": "*", | ||||
|         "composer/package-versions-deprecated": "1.11.99.1", | ||||
|         "deployer/deployer": "^7.0", | ||||
|         "doctrine/doctrine-bundle": "^2.3", | ||||
|         "doctrine/doctrine-migrations-bundle": "^3.1", | ||||
|         "doctrine/orm": "^2.8", | ||||
|         "easycorp/easyadmin-bundle": "^3", | ||||
|         "imagine/imagine": "^1.2", | ||||
|         "mjaschen/phpgeo": "^3.2", | ||||
|         "presta/sitemap-bundle": "^3.2", | ||||
|         "samayo/bulletproof": "4.0.1", | ||||
|         "sensio/framework-extra-bundle": "^6.1", | ||||
|         "symfony/asset": "5.3.*", | ||||
|         "symfony/console": "5.3.*", | ||||
|         "symfony/dotenv": "5.3.*", | ||||
|         "symfony/asset": "^5.4.20", | ||||
|         "symfony/console": "^5.4.20", | ||||
|         "symfony/dotenv": "^5.4.20", | ||||
|         "symfony/filesystem": "^5.4.20", | ||||
|         "symfony/flex": "^1.3.1", | ||||
|         "symfony/form": "5.3.*", | ||||
|         "symfony/framework-bundle": "5.3.*", | ||||
|         "symfony/loco-translation-provider": "5.3.*", | ||||
|         "symfony/mailer": "5.3.*", | ||||
|         "symfony/form": "^5.4.20", | ||||
|         "symfony/framework-bundle": "^5.4.20", | ||||
|         "symfony/mailer": "^5.4.20", | ||||
|         "symfony/monolog-bundle": "^3.7", | ||||
|         "symfony/proxy-manager-bridge": "5.3.*", | ||||
|         "symfony/security-bundle": "5.3.*", | ||||
|         "symfony/twig-bundle": "5.3.*", | ||||
|         "symfony/validator": "5.3.*", | ||||
|         "symfony/proxy-manager-bridge": "^5.4.20", | ||||
|         "symfony/security-bundle": "^5.4.20", | ||||
|         "symfony/twig-bundle": "^5.4.20", | ||||
|         "symfony/validator": "^5.4.20", | ||||
|         "symfony/webpack-encore-bundle": "^1.11", | ||||
|         "symfony/yaml": "5.3.*", | ||||
|         "symfony/yaml": "^5.4.20", | ||||
|         "symfonycasts/reset-password-bundle": "^1.7", | ||||
|         "symfonycasts/verify-email-bundle": "^1.4", | ||||
|         "twig/extra-bundle": "^2.12|^3.0", | ||||
|         "twig/intl-extra": "^3.3", | ||||
|         "twig/twig": "^2.12|^3.0" | ||||
|         "twig/twig": "^3.4.3" | ||||
|     }, | ||||
|     "config": { | ||||
|         "optimize-autoloader": true, | ||||
|         "preferred-install": { | ||||
|             "*": "dist" | ||||
|         }, | ||||
|         "sort-packages": true | ||||
|         "sort-packages": true, | ||||
|         "allow-plugins": { | ||||
|             "composer/package-versions-deprecated": false, | ||||
|             "symfony/flex": true | ||||
|         } | ||||
|     }, | ||||
|     "autoload": { | ||||
|         "psr-4": { | ||||
|  | @ -75,17 +83,17 @@ | |||
|     "extra": { | ||||
|         "symfony": { | ||||
|             "allow-contrib": false, | ||||
|             "require": "5.3.*" | ||||
|             "require": "^5.4.20" | ||||
|         } | ||||
|     }, | ||||
|     "require-dev": { | ||||
|         "symfony/browser-kit": "^5.2", | ||||
|         "symfony/css-selector": "^5.2", | ||||
|         "symfony/debug-bundle": "^5.2", | ||||
|         "symfony/browser-kit": "^5.4", | ||||
|         "symfony/css-selector": "^5.4", | ||||
|         "symfony/debug-bundle": "^5.4", | ||||
|         "symfony/maker-bundle": "^1.30", | ||||
|         "symfony/phpunit-bridge": "^5.2", | ||||
|         "symfony/stopwatch": "^5.2", | ||||
|         "symfony/var-dumper": "^5.2", | ||||
|         "symfony/web-profiler-bundle": "^5.2" | ||||
|         "symfony/phpunit-bridge": "^5.4", | ||||
|         "symfony/stopwatch": "^5.4", | ||||
|         "symfony/var-dumper": "^5.4", | ||||
|         "symfony/web-profiler-bundle": "^5.4" | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										3290
									
								
								composer.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										3290
									
								
								composer.lock
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -16,4 +16,5 @@ return [ | |||
|     Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], | ||||
|     Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], | ||||
|     SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true], | ||||
|     Presta\SitemapBundle\PrestaSitemapBundle::class => ['all' => true], | ||||
| ]; | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| doctrine: | ||||
|     dbal: | ||||
|         override_url: true | ||||
|         url: '%env(resolve:DATABASE_URL)%' | ||||
| 
 | ||||
|         # IMPORTANT: You MUST configure your server version, | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ framework: | |||
|         handler_id: null | ||||
|         cookie_secure: auto | ||||
|         cookie_samesite: lax | ||||
|         storage_factory_id: 'session.storage.factory.native' | ||||
| 
 | ||||
|     #esi: true | ||||
|     #fragments: true | ||||
|  |  | |||
|  | @ -15,3 +15,7 @@ monolog: | |||
|             type: console | ||||
|             process_psr_3_messages: false | ||||
|             channels: ["!event", "!doctrine"] | ||||
|         file: | ||||
|             type: stream | ||||
|             path: "%kernel.logs_dir%/%kernel.environment%.log" | ||||
|             level: warning | ||||
|  |  | |||
|  | @ -1,6 +1,11 @@ | |||
| security: | ||||
|     encoders: | ||||
|     enable_authenticator_manager: true | ||||
| 
 | ||||
|     password_hashers: | ||||
|         # use your user class name here | ||||
|         App\Entity\User: | ||||
|             # Use native password hasher, which auto-selects the best | ||||
|             # possible hashing algorithm (starting from Symfony 5.3 this is "bcrypt") | ||||
|             algorithm: auto | ||||
| 
 | ||||
|     # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers | ||||
|  | @ -15,12 +20,10 @@ security: | |||
|             pattern: ^/(_(profiler|wdt)|css|images|js)/ | ||||
|             security: false | ||||
|         main: | ||||
|             anonymous: true | ||||
|             lazy: true | ||||
|             provider: app_user_provider | ||||
|             guard: | ||||
|                 authenticators: | ||||
|                     - App\Security\AppAuthenticator | ||||
|             custom_authenticators: | ||||
|                 - App\Security\AppAuthenticator | ||||
|             logout: | ||||
|                 path: app_logout | ||||
|                 # where to redirect after logout | ||||
|  | @ -47,4 +50,4 @@ security: | |||
|     access_control: | ||||
|         - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } | ||||
|         - { path: ^/admin, roles: ROLE_ADMIN } | ||||
|         - { path: ^(?!/(login|register|reset-password|offers|offer/*|imprint)), roles: ROLE_USER } | ||||
|         - { path: ^(?!/(login|register|reset-password|offers|offer/*|imprint|faq|sitemap.*)), roles: ROLE_USER } | ||||
|  | @ -1,8 +1,6 @@ | |||
| framework: | ||||
|     default_locale: en | ||||
|     translator: | ||||
|         providers: | ||||
|             loco: | ||||
|                 dsn: '%env(LOCO_DSN)%' | ||||
|                 domains: ['messages'] | ||||
|                 locales: ['en', 'de'] | ||||
|         default_path: '%kernel.project_dir%/translations' | ||||
|         fallbacks: | ||||
|             - en | ||||
|  |  | |||
|  | @ -1,3 +1,6 @@ | |||
| twig: | ||||
|     default_path: '%kernel.project_dir%/templates' | ||||
|     form_themes: ['bootstrap_4_horizontal_layout.html.twig'] | ||||
| 
 | ||||
|     globals: | ||||
|         app_env: '%env(APP_ENV)%' | ||||
							
								
								
									
										2
									
								
								config/routes/presta_sitemap.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								config/routes/presta_sitemap.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| presta_sitemap: | ||||
|   resource: "@PrestaSitemapBundle/config/routing.yml" | ||||
|  | @ -4,6 +4,8 @@ | |||
| # Put parameters here that don't need to change on each machine where the app is deployed | ||||
| # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration | ||||
| parameters: | ||||
|     captcha.secret: '%env(CAPTCHA_SECRET)%' | ||||
|     captcha.sitekey: '%env(CAPTCHA_SITEKEY)%' | ||||
| 
 | ||||
| services: | ||||
|     # default configuration for services in *this* file | ||||
|  |  | |||
							
								
								
									
										58
									
								
								deploy.php
									
										
									
									
									
								
							
							
						
						
									
										58
									
								
								deploy.php
									
										
									
									
									
								
							|  | @ -4,37 +4,69 @@ namespace Deployer; | |||
| require 'recipe/symfony.php'; | ||||
| 
 | ||||
| // Project name
 | ||||
| set('application', 'plant-exchange'); | ||||
| set('application', 'pflaenz.li'); | ||||
| 
 | ||||
| // Project repository
 | ||||
| set('repository', 'ssh://git@git.thisfro.ch:222/thisfro/plant-exchange.git'); | ||||
| set('repository', 'ssh://git@git.thisfro.ch:222/thisfro/pflaenz.li.git'); | ||||
| 
 | ||||
| // [Optional] Allocate tty for git clone. Default value is false.
 | ||||
| set('git_tty', true);  | ||||
| 
 | ||||
| set('bin/php', function() { | ||||
|         return '/opt/php8.2/bin/php'; | ||||
| }); | ||||
| 
 | ||||
| set('bin/composer', function() { | ||||
|     return '/opt/php8.2/bin/composer2'; | ||||
| }); | ||||
| 
 | ||||
| // Shared files/dirs between deploys 
 | ||||
| add('shared_files', []); | ||||
| add('shared_dirs', []); | ||||
| add('shared_files', ['public/.htaccess']); | ||||
| add('shared_dirs', ['public/uploads']); | ||||
| 
 | ||||
| // Writable dirs by web server 
 | ||||
| add('writable_dirs', []); | ||||
| 
 | ||||
| // Set composer options
 | ||||
| set('composer_options', '--verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader --no-scripts'); | ||||
| 
 | ||||
| // Hosts
 | ||||
| host('lq5xi.ftp.infomaniak.com') | ||||
|     ->set('remote_user', 'lq5xi_thisfro') | ||||
|     ->set('deploy_path', '~/sites/{{stage}}.{{application}}') | ||||
|     ->set('http_user', 'uid153060') | ||||
|     ->set('stage', 'beta'); | ||||
| 
 | ||||
| host('pflaenz.li') | ||||
|     ->set('deploy_path', '~/{{application}}');     | ||||
|      | ||||
| // Tasks
 | ||||
| 
 | ||||
| task('build', function () { | ||||
|     run('cd {{release_path}} && build'); | ||||
| task('upload:build', function() { | ||||
|     upload('public/build/', '{{release_path}}/public/build/'); | ||||
| }); | ||||
| 
 | ||||
| // Build yarn locally
 | ||||
| task('deploy:build:assets', function (): void { | ||||
|     runLocally('yarn install'); | ||||
|     runLocally('yarn encore production'); | ||||
| })->desc('Install front-end assets'); | ||||
| 
 | ||||
| before('deploy:symlink', 'deploy:build:assets'); | ||||
| 
 | ||||
| // Upload assets
 | ||||
| task('upload:assets', function (): void { | ||||
|     upload(__DIR__.'/public/build/', '{{release_path}}/public/build'); | ||||
| }); | ||||
| 
 | ||||
| task('upload:build', function() { | ||||
|     upload("public/build/", '{{release_path}}/public/build/'); | ||||
| }); | ||||
| 
 | ||||
| task('upload:build', function() { | ||||
|     upload("public/build/", '{{release_path}}/public/build/'); | ||||
| }); | ||||
| 
 | ||||
| after('deploy:build:assets', 'upload:assets'); | ||||
| 
 | ||||
| // [Optional] if deploy fails automatically unlock.
 | ||||
| after('deploy:failed', 'deploy:unlock'); | ||||
| 
 | ||||
| // Migrate database before symlink new release.
 | ||||
| 
 | ||||
| before('deploy:symlink', 'database:migrate'); | ||||
| 
 | ||||
| before('deploy:symlink', 'database:migrate'); | ||||
|  | @ -2,29 +2,20 @@ version: '3' | |||
| 
 | ||||
| services: | ||||
|   db: | ||||
|     image: postgres:latest | ||||
|     image: mariadb:10.5 | ||||
|     environment: | ||||
|       POSTGRES_PASSWORD: develop | ||||
|       MARIADB_USER: pflaenzli | ||||
|       MARIADB_PASSWORD: develop | ||||
|       MARIADB_DATABASE: pflaenzli | ||||
|       MARIADB_ROOT_PASSWORD: r00tpa55w0rd | ||||
|     ports: | ||||
|       - 5432:5432 | ||||
|       - 3306:3306 | ||||
|     volumes: | ||||
|       - postgres-data:/var/lib/postgresql/data | ||||
|    | ||||
|   pgadmin: | ||||
|     image: dpage/pgadmin4 | ||||
|     environment: | ||||
|       PGADMIN_DEFAULT_EMAIL: admin@admin.com | ||||
|       PGADMIN_DEFAULT_PASSWORD: root | ||||
|     ports: | ||||
|       - "8001:80" | ||||
|     volumes: | ||||
|       - ./pgadmin_data/servers.json:/pgadmin4/servers.json | ||||
|       - pgadmin-data:/varl/lib/pgadmin | ||||
|       - mariadb-data:/var/lib/mysql | ||||
| 
 | ||||
|   mailer: | ||||
|     image: schickling/mailcatcher | ||||
|     ports: [1025,1080] | ||||
| 
 | ||||
| volumes: | ||||
|   postgres-data: | ||||
|   pgadmin-data: | ||||
|   mariadb-data: | ||||
|  |  | |||
|  | @ -1,35 +0,0 @@ | |||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace DoctrineMigrations; | ||||
| 
 | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
| 
 | ||||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20210422125046 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription() : string | ||||
|     { | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     public function up(Schema $schema) : void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); | ||||
|         $this->addSql('CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); | ||||
|         $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON "user" (email)'); | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema) : void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SCHEMA public'); | ||||
|         $this->addSql('DROP SEQUENCE "user_id_seq" CASCADE'); | ||||
|         $this->addSql('DROP TABLE "user"'); | ||||
|     } | ||||
| } | ||||
|  | @ -1,34 +0,0 @@ | |||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace DoctrineMigrations; | ||||
| 
 | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
| 
 | ||||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20210422155430 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription() : string | ||||
|     { | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     public function up(Schema $schema) : void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('ALTER TABLE "user" ADD username VARCHAR(255) NOT NULL DEFAULT \'\''); | ||||
|         $this->addSql('ALTER TABLE "user" ALTER is_verified DROP DEFAULT'); | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema) : void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SCHEMA public'); | ||||
|         $this->addSql('ALTER TABLE "user" DROP username'); | ||||
|         $this->addSql('ALTER TABLE "user" ALTER is_verified SET DEFAULT \'true\''); | ||||
|     } | ||||
| } | ||||
|  | @ -1,38 +0,0 @@ | |||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace DoctrineMigrations; | ||||
| 
 | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
| 
 | ||||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20210424153343 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription() : string | ||||
|     { | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     public function up(Schema $schema) : void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SEQUENCE offering_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); | ||||
|         $this->addSql('CREATE TABLE offering (id INT NOT NULL, by_user_id INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, title VARCHAR(255) NOT NULL, photo_filename VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); | ||||
|         $this->addSql('CREATE INDEX IDX_A5682AB1DC9C2434 ON offering (by_user_id)'); | ||||
|         $this->addSql('ALTER TABLE offering ADD CONSTRAINT FK_A5682AB1DC9C2434 FOREIGN KEY (by_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE "user" ALTER username DROP DEFAULT'); | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema) : void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SCHEMA public'); | ||||
|         $this->addSql('DROP SEQUENCE offering_id_seq CASCADE'); | ||||
|         $this->addSql('DROP TABLE offering'); | ||||
|         $this->addSql('ALTER TABLE "user" ALTER username SET DEFAULT \'\''); | ||||
|     } | ||||
| } | ||||
|  | @ -1,34 +0,0 @@ | |||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace DoctrineMigrations; | ||||
| 
 | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
| 
 | ||||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20210426205302 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription() : string | ||||
|     { | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     public function up(Schema $schema) : void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('ALTER TABLE offering ADD zip_code INT NOT NULL DEFAULT 0'); | ||||
|         $this->addSql('ALTER TABLE offering ADD description TEXT NOT NULL DEFAULT \'Lorem ipsum dolor\''); | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema) : void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SCHEMA public'); | ||||
|         $this->addSql('ALTER TABLE offering DROP zip_code'); | ||||
|         $this->addSql('ALTER TABLE offering DROP description'); | ||||
|     } | ||||
| } | ||||
|  | @ -1,42 +0,0 @@ | |||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace DoctrineMigrations; | ||||
| 
 | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
| 
 | ||||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20210502123444 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription() : string | ||||
|     { | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     public function up(Schema $schema) : void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SEQUENCE reset_password_request_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); | ||||
|         $this->addSql('CREATE TABLE reset_password_request (id INT NOT NULL, user_id INT NOT NULL, selector VARCHAR(20) NOT NULL, hashed_token VARCHAR(100) NOT NULL, requested_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); | ||||
|         $this->addSql('CREATE INDEX IDX_7CE748AA76ED395 ON reset_password_request (user_id)'); | ||||
|         $this->addSql('COMMENT ON COLUMN reset_password_request.requested_at IS \'(DC2Type:datetime_immutable)\''); | ||||
|         $this->addSql('COMMENT ON COLUMN reset_password_request.expires_at IS \'(DC2Type:datetime_immutable)\''); | ||||
|         $this->addSql('ALTER TABLE reset_password_request ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE offering ALTER zip_code DROP DEFAULT'); | ||||
|         $this->addSql('ALTER TABLE offering ALTER description DROP DEFAULT'); | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema) : void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SCHEMA public'); | ||||
|         $this->addSql('DROP SEQUENCE reset_password_request_id_seq CASCADE'); | ||||
|         $this->addSql('DROP TABLE reset_password_request'); | ||||
|         $this->addSql('ALTER TABLE offering ALTER zip_code SET DEFAULT 0'); | ||||
|         $this->addSql('ALTER TABLE offering ALTER description SET DEFAULT \'Lorem ipsum dolor\''); | ||||
|     } | ||||
| } | ||||
|  | @ -1,36 +0,0 @@ | |||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace DoctrineMigrations; | ||||
| 
 | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
| 
 | ||||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20210503161858 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription() : string | ||||
|     { | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     public function up(Schema $schema) : void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SEQUENCE wish_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); | ||||
|         $this->addSql('CREATE TABLE wish (id INT NOT NULL, by_user_id INT DEFAULT NULL, title VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); | ||||
|         $this->addSql('CREATE INDEX IDX_D7D174C9DC9C2434 ON wish (by_user_id)'); | ||||
|         $this->addSql('ALTER TABLE wish ADD CONSTRAINT FK_D7D174C9DC9C2434 FOREIGN KEY (by_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema) : void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SCHEMA public'); | ||||
|         $this->addSql('DROP SEQUENCE wish_id_seq CASCADE'); | ||||
|         $this->addSql('DROP TABLE wish'); | ||||
|     } | ||||
| } | ||||
|  | @ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration; | |||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20210519090349 extends AbstractMigration | ||||
| final class Version20220111213438 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|  | @ -20,12 +20,12 @@ final class Version20210519090349 extends AbstractMigration | |||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('ALTER TABLE offering ALTER COLUMN description DROP NOT NULL'); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SCHEMA public'); | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration; | |||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20210614104026 extends AbstractMigration | ||||
| final class Version20220112111528 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|  | @ -20,15 +20,12 @@ final class Version20210614104026 extends AbstractMigration | |||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('ALTER TABLE offering ALTER description SET NOT NULL'); | ||||
|         $this->addSql('ALTER TABLE "user" ADD zip_code INT DEFAULT NULL'); | ||||
|         $this->addSql('ALTER TABLE offering ADD lat DOUBLE PRECISION DEFAULT NULL, ADD lng DOUBLE PRECISION DEFAULT NULL'); | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SCHEMA public'); | ||||
|         $this->addSql('ALTER TABLE "user" DROP zip_code'); | ||||
|         $this->addSql('ALTER TABLE offering ALTER description DROP NOT NULL'); | ||||
|         $this->addSql('ALTER TABLE offering DROP lat, DROP lng'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										32
									
								
								migrations/Version20220117175334.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								migrations/Version20220117175334.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace DoctrineMigrations; | ||||
| 
 | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
| 
 | ||||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20220117175334 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Add an offer ID to be displayed in the URL'; | ||||
|     } | ||||
| 
 | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('ALTER TABLE offering ADD url_id VARCHAR(13) NOT NULL'); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('ALTER TABLE offering DROP url_id'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										33
									
								
								migrations/Version20220117193804.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								migrations/Version20220117193804.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace DoctrineMigrations; | ||||
| 
 | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
| 
 | ||||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20220117193804 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('ALTER TABLE user ADD url_id VARCHAR(13) NOT NULL'); | ||||
|         $this->addSql('ALTER TABLE wish ADD url_id VARCHAR(13) NOT NULL'); | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('ALTER TABLE `user` DROP url_id'); | ||||
|         $this->addSql('ALTER TABLE wish DROP url_id'); | ||||
|     } | ||||
| } | ||||
|  | @ -10,23 +10,22 @@ use Doctrine\Migrations\AbstractMigration; | |||
| /** | ||||
|  * Auto-generated Migration: Please modify to your needs! | ||||
|  */ | ||||
| final class Version20210422132314 extends AbstractMigration | ||||
| final class Version20220119172053 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription() : string | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     public function up(Schema $schema) : void | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         // this up() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('ALTER TABLE "user" ADD is_verified BOOLEAN NOT NULL DEFAULT TRUE'); | ||||
|         $this->addSql('RENAME TABLE offering TO offer'); | ||||
|     } | ||||
| 
 | ||||
|     public function down(Schema $schema) : void | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         // this down() migration is auto-generated, please modify it to your needs
 | ||||
|         $this->addSql('CREATE SCHEMA public'); | ||||
|         $this->addSql('ALTER TABLE "user" DROP is_verified'); | ||||
|         $this->addSql('RENAME TABLE offer TO offering'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										15330
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										15330
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -19,9 +19,11 @@ | |||
|         "dev-server": "encore dev-server", | ||||
|         "dev": "encore dev", | ||||
|         "watch": "encore dev --watch", | ||||
|         "build": "encore production --progress" | ||||
|         "build": "encore production --progress", | ||||
|         "test": "snyk test" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@snyk/protect": "^1.834.0", | ||||
|         "cookie-notice": "^1.3.6", | ||||
|         "friendly-challenge": "^0.8.5" | ||||
|     } | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								public/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 6 KiB | 
|  | @ -18,5 +18,7 @@ if ($_SERVER['APP_DEBUG']) { | |||
| $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); | ||||
| $request = Request::createFromGlobals(); | ||||
| $response = $kernel->handle($request); | ||||
| $response->headers->set('X-Frame-Options', 'DENY'); | ||||
| $response->headers->set('X-Content-Type-Options', 'nosniff'); | ||||
| $response->send(); | ||||
| $kernel->terminate($request, $response); | ||||
| $kernel->terminate($request, $response); | ||||
|  | @ -3,7 +3,7 @@ | |||
| namespace App\Controller\Admin; | ||||
| 
 | ||||
| use App\Entity\User; | ||||
| use App\Entity\Offering; | ||||
| use App\Entity\Offer; | ||||
| use App\Entity\Wish; | ||||
| use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard; | ||||
| use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem; | ||||
|  | @ -36,7 +36,7 @@ class DashboardController extends AbstractDashboardController | |||
|         yield MenuItem::linktoRoute('Back to the website', 'fas fa-arrow-left', 'offers'); | ||||
|         yield MenuItem::linktoDashboard('Dashboard', 'fa fa-home'); | ||||
|         yield MenuItem::linkToCrud('User', 'fas fa-user', User::class); | ||||
|         yield MenuItem::linkToCrud('Offering', 'fas fa-seedling', Offering::class); | ||||
|         yield MenuItem::linkToCrud('Offer', 'fas fa-seedling', Offer::class); | ||||
|         yield MenuItem::linkToCrud('Wish', 'fas fa-star', Wish::class); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| namespace App\Controller\Admin; | ||||
| 
 | ||||
| use App\Entity\Offering; | ||||
| use App\Entity\Offer; | ||||
| use App\Entity\User; | ||||
| 
 | ||||
| use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; | ||||
|  | @ -13,11 +13,11 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; | |||
| use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField; | ||||
| use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; | ||||
| 
 | ||||
| class OfferingCrudController extends AbstractCrudController | ||||
| class OfferCrudController extends AbstractCrudController | ||||
| { | ||||
|     public static function getEntityFqcn(): string | ||||
|     { | ||||
|         return Offering::class; | ||||
|         return Offer::class; | ||||
|     } | ||||
| 
 | ||||
|     public function configureFields(string $pageName): iterable | ||||
|  | @ -10,15 +10,21 @@ use Twig\Environment; | |||
| 
 | ||||
| class AppController extends AbstractController | ||||
| { | ||||
|     #[Route('/', name: 'homepage')]
 | ||||
|     #[Route('/', name: 'homepage', options: ["sitemap" => true])]
 | ||||
|     public function index(): Response | ||||
|     { | ||||
|         return $this->render('app/index.html.twig'); | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/imprint', name: 'imprint')]
 | ||||
|     #[Route('/imprint', name: 'imprint', options: ["sitemap" => true])]
 | ||||
|     public function imprint(): Response | ||||
|     { | ||||
|         return $this->render('app/imprint.html.twig'); | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/faq', name: 'faq', options: ["sitemap" => true])]
 | ||||
|     public function faq(): Response | ||||
|     { | ||||
|         return $this->render('app/faq.html.twig'); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,14 +2,17 @@ | |||
| 
 | ||||
| namespace App\Controller; | ||||
| 
 | ||||
| use App\Entity\Offering; | ||||
| use App\Form\OfferingFormType; | ||||
| use App\Entity\Offer; | ||||
| use App\Form\OfferFormType; | ||||
| use App\Form\OfferFilterFormType; | ||||
| 
 | ||||
| use App\Repository\OfferingRepository; | ||||
| use App\Repository\OfferRepository; | ||||
| use App\Repository\WishRepository; | ||||
| 
 | ||||
| use App\Service\PlzToCoordinate; | ||||
| use App\Service\DistanceCalculator; | ||||
| use App\Service\PhotoResizer; | ||||
| use App\Service\OfferPhotoHelper; | ||||
| 
 | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||||
|  | @ -23,24 +26,62 @@ class OfferController extends AbstractController | |||
| { | ||||
|     private $entityManager; | ||||
| 
 | ||||
|     public function __construct(EntityManagerInterface $entityManager) | ||||
|     public function __construct(EntityManagerInterface $entityManager, PhotoResizer $photoresizer) | ||||
|     { | ||||
|         $this->entityManager = $entityManager; | ||||
|         $this->photoresizer = $photoresizer; | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/offers', name: 'offers')]
 | ||||
|     public function index(Environment $twig, OfferingRepository $offerRepository): Response | ||||
|     #[Route('/offers', name: 'offers', options: ["sitemap" => true])]
 | ||||
|     public function showAll(Request $request, OfferRepository $offerRepository, PlzToCoordinate $plzconverter, DistanceCalculator $distanceCalculator): Response | ||||
|     { | ||||
|         return new Response($twig->render('offer/index.html.twig', [ | ||||
|             'offers' => $offerRepository->findAll(), | ||||
|         ])); | ||||
|         $form = $this->createForm(OfferFilterFormType::class); | ||||
|         $form->handleRequest($request); | ||||
| 
 | ||||
|         $filteredOffers = []; | ||||
| 
 | ||||
|         if ($form->isSubmitted() && $form->isValid() && $form->get('search')->getData() != null) { | ||||
|             $allOffers = $offerRepository->findBySearchLiteral($form->get('search')->getData()); | ||||
|         } | ||||
|         else { | ||||
|             $allOffers = $offerRepository->findAll(); | ||||
|         } | ||||
| 
 | ||||
|         if ($form->isSubmitted() && $form->isValid() && $form->get('distance')->getData() != null && $form->get('zipCode')->getData() != null) { | ||||
|             $filterDistance = $form->get('distance')->getData(); | ||||
|             $filterPlz = $form->get('zipCode')->getData(); | ||||
|             $filterCoordinate = $plzconverter->convertPlzToCoordinate($filterPlz); | ||||
|              | ||||
|             if ($filterCoordinate != null) { | ||||
|                 foreach ($allOffers as $offer) { | ||||
|                     $offerCoordinate = $offer->getCoordinate(); | ||||
|                      | ||||
|                     $distance = $distanceCalculator->calculateDistance($offerCoordinate, $filterCoordinate); | ||||
|              | ||||
|                     if ($distance < $filterDistance) { | ||||
|                         array_push($filteredOffers, $offer); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 $this->addFlash("error", "The PLZ was not found!"); | ||||
|             } | ||||
|         } | ||||
|         else {     | ||||
|             $filteredOffers = $allOffers; | ||||
|         } | ||||
|          | ||||
|         return $this->render('offer/index.html.twig', [ | ||||
|             'offers' => $filteredOffers, | ||||
|             'filter_form' => $form->createView() | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/new', name: 'new_offer')]
 | ||||
|     public function newOffer(Request $request, string $photoDir): Response | ||||
|     public function newOffer(Request $request, PlzToCoordinate $plzconverter, string $photoDir, OfferPhotoHelper $offerPhotoHelper): Response | ||||
|     { | ||||
|         $offer = new Offering(); | ||||
|         $form = $this->createForm(OfferingFormType::class, $offer); | ||||
|         $offer = new Offer(); | ||||
|         $form = $this->createForm(OfferFormType::class, $offer); | ||||
|         $user = $this->getUser();  | ||||
| 
 | ||||
|         $form->handleRequest($request); | ||||
|  | @ -48,22 +89,22 @@ class OfferController extends AbstractController | |||
|         if ($form->isSubmitted() && $form->isValid()) { | ||||
|             $offer->setByUser($user); | ||||
|             $offer->setCreatedAt(new \DateTime()); | ||||
|             $offer->setUrlId(uniqid()); | ||||
| 
 | ||||
|             $coordinate = $plzconverter->convertPlzToCoordinate($form['zipCode']->getData()); | ||||
|             if ($coordinate != null) { | ||||
|                 $offer->setCoordinate($coordinate); | ||||
|             } | ||||
| 
 | ||||
|             if ($photo = $form['photo']->getData()) { | ||||
|                 $filename = uniqid().'.'.$photo->guessExtension(); | ||||
|                 try { | ||||
|                     $photo->move($photoDir, $filename); | ||||
|                 } catch (FileException $e) { | ||||
|                     // unable to upload the photo, give up
 | ||||
|                     $this->addFlash("error", "There was an error uploading the photo: ".$e); | ||||
|                     return $this->redirectToRoute('new_offer'); | ||||
|                 } | ||||
|                 $offer->setPhotoFilename($filename); | ||||
|                 $offerPhotoHelper->uploadOfferPhoto($photoDir, $photo, $offer); | ||||
|             } | ||||
| 
 | ||||
|             $this->entityManager->persist($offer); | ||||
|             $this->entityManager->flush(); | ||||
| 
 | ||||
|             $this->photoresizer->resize($photoDir.'/'.$offer->getPhotoFilename()); | ||||
| 
 | ||||
|             $this->addFlash("success", "Successfully added the new offer!"); | ||||
|             return $this->redirectToRoute('offers'); | ||||
|         } | ||||
|  | @ -74,10 +115,10 @@ class OfferController extends AbstractController | |||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/offer/{id}', name: 'show_offer')]
 | ||||
|     public function show_offer(Offering $offer, WishRepository $wishRepository, PlzToCoordinate $plzconverter, DistanceCalculator $distanceCalculator): Response | ||||
|     #[Route('/offer/{urlId}', name: 'show_offer')]
 | ||||
|     public function showOffer(Offer $offer, WishRepository $wishRepository, PlzToCoordinate $plzconverter, DistanceCalculator $distanceCalculator): Response | ||||
|     { | ||||
|         $distance = 0; | ||||
|         $distance = null; | ||||
|         $user = $this->getUser(); | ||||
|         $offerPlz = $offer->getZipCode(); | ||||
|          | ||||
|  | @ -88,7 +129,16 @@ class OfferController extends AbstractController | |||
| 
 | ||||
|         if (isset($userPlz))  | ||||
|         { | ||||
|             $distance = $distanceCalculator->calculateDistance($plzconverter->getCoordinates($offerPlz), $plzconverter->getCoordinates($userPlz)); | ||||
|             if (isset($offerPlz)) | ||||
|             { | ||||
|                 $offerCoordinate = $plzconverter->convertPlzToCoordinate($offerPlz); | ||||
|                 $userCoordinate = $plzconverter->convertPlzToCoordinate($userPlz); | ||||
|                  | ||||
|                 if ($userCoordinate != null && $offerCoordinate != null) | ||||
|                 { | ||||
|                     $distance = $distanceCalculator->calculateDistance($offerCoordinate, $userCoordinate); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return $this->render('app/offer.html.twig', [ | ||||
|  | @ -99,10 +149,10 @@ class OfferController extends AbstractController | |||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/offer/edit/{id}', name: 'edit_offer')]
 | ||||
|     public function editOffer(Offering $offer, OfferingRepository $offerRepository, Request $request, string $photoDir): Response | ||||
|     #[Route('/offer/edit/{urlId}', name: 'edit_offer')]
 | ||||
|     public function editOffer(Offer $offer, Request $request, string $photoDir, OfferPhotoHelper $offerPhotoHelper): Response | ||||
|     { | ||||
|         $form = $this->createForm(OfferingFormType::class, $offer); | ||||
|         $form = $this->createForm(OfferFormType::class, $offer); | ||||
|         $user = $this->getUser(); | ||||
|         if ($offer->getByUser() === $user) | ||||
|         { | ||||
|  | @ -113,19 +163,17 @@ class OfferController extends AbstractController | |||
|                 $offer->setCreatedAt(new \DateTime()); | ||||
| 
 | ||||
|                 if ($photo = $form['photo']->getData()) { | ||||
|                     $filename = uniqid(random_bytes(6)).'.'.$photo->guessExtension(); | ||||
|                     try { | ||||
|                         $photo->move($photoDir, $filename); | ||||
|                     } catch (FileException $e) { | ||||
|                         // unable to upload the photo, give up
 | ||||
|                         $this->addFlash("error", "There was an error uploading the photo: ".$e); | ||||
|                         return $this->redirectToRoute('new_offer'); | ||||
|                     } | ||||
|                     $offer->setPhotoFilename($filename); | ||||
|                     $oldFilename = $offer->getPhotoFilename(); | ||||
|                     $offerPhotoHelper->uploadOfferPhoto($photoDir, $photo, $offer); | ||||
|                     $offerPhotoHelper->deleteOfferPhoto($photoDir, $oldFilename); | ||||
|                 } | ||||
| 
 | ||||
|                 $this->entityManager->persist($offer); | ||||
|                 $this->entityManager->flush(); | ||||
| 
 | ||||
|                 $this->addFlash("success", "Successfully updated the offer!"); | ||||
| 
 | ||||
|                 return $this->redirectToRoute('show_offer', ['urlId' => $offer->getUrlId()]); | ||||
|             }  | ||||
| 
 | ||||
|             return $this->render('offer/edit.html.twig', [ | ||||
|  | @ -138,16 +186,16 @@ class OfferController extends AbstractController | |||
|         throw new HttpException(403, "No permission"); | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/offer/delete/{id}', name: 'delete_offer')]
 | ||||
|     public function deleteOffer(Offering $offer, string $photoDir): Response | ||||
|     #[Route('/offer/delete/{urlId}', name: 'delete_offer')]
 | ||||
|     public function deleteOffer(Offer $offer, string $photoDir, OfferPhotoHelper $offerPhotoHelper): Response | ||||
|     { | ||||
|         $user = $this->getUser();  | ||||
| 
 | ||||
|         if ($offer->getByUser() === $user) | ||||
|         { | ||||
|             if ($offer->getPhotoFilename()) | ||||
|             if($offer->getPhotoFilename() != null) | ||||
|             { | ||||
|                 unlink($photoDir . '/' . $offer->getPhotoFilename()); | ||||
|                 $offerPhotoHelper->deleteOfferPhoto($photoDir, $offer->getPhotoFilename()); | ||||
|             } | ||||
|             $this->entityManager->remove($offer); | ||||
|             $this->entityManager->flush(); | ||||
|  | @ -162,13 +210,23 @@ class OfferController extends AbstractController | |||
|     } | ||||
| 
 | ||||
|     #[Route('/myoffers', name: 'own_offers')]
 | ||||
|     public function ownOffers(OfferingRepository $offeringRepository): Response | ||||
|     public function ownOffers(OfferRepository $offerRepository): Response | ||||
|     { | ||||
|         $user = $this->getUser(); | ||||
| 
 | ||||
|         return $this->render('user/offers.html.twig', [ | ||||
|             'user' => $user, | ||||
|             'offers' => $offeringRepository->findByUser($user), | ||||
|             'offers' => $offerRepository->findByUser($user), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/offers/search', name: 'search', options: ["sitemap" => false])]
 | ||||
|     public function search(OfferRepository $offerRepository): Response | ||||
|     { | ||||
|         $offers = $offerRepository->findBySearchLiteral(''); | ||||
| 
 | ||||
|         return $this->render('offer/search.html.twig', [ | ||||
|             'offers' => $offers, | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -4,8 +4,9 @@ namespace App\Controller; | |||
| 
 | ||||
| use App\Entity\User; | ||||
| use App\Form\RegistrationFormType; | ||||
| use App\Security\EmailVerifier; | ||||
| use App\Security\AppAuthenticator; | ||||
| use App\Security\EmailVerifier; | ||||
| use App\Service\CaptchaVerifier; | ||||
| use App\Repository\UserRepository; | ||||
| use Symfony\Bridge\Twig\Mime\TemplatedEmail; | ||||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||||
|  | @ -13,8 +14,7 @@ use Symfony\Component\HttpFoundation\Request; | |||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\Mime\Address; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; | ||||
| use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; | ||||
| use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | ||||
| use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface; | ||||
| 
 | ||||
| class RegistrationController extends AbstractController | ||||
|  | @ -26,42 +26,42 @@ class RegistrationController extends AbstractController | |||
|         $this->emailVerifier = $emailVerifier; | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/register', name: 'app_register')]
 | ||||
|     public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, AppAuthenticator $authenticator): Response | ||||
|     #[Route('/register', name: 'app_register', options: ["sitemap" => true])]
 | ||||
|     public function register(Request $request, UserPasswordHasherInterface $passwordEncoder, CaptchaVerifier $captchaVerifier): Response | ||||
|     { | ||||
|         $user = new User(); | ||||
|         $form = $this->createForm(RegistrationFormType::class, $user); | ||||
|         $form->handleRequest($request); | ||||
| 
 | ||||
|         if ($form->isSubmitted() && $form->isValid()) { | ||||
|             // encode the plain password
 | ||||
|             $user->setPassword( | ||||
|                 $passwordEncoder->encodePassword( | ||||
|                     $user, | ||||
|                     $form->get('plainPassword')->getData() | ||||
|                 ) | ||||
|             ); | ||||
|             if ($captchaVerifier->isVerified($form->get('captcha_solution')->getData(), $this->getParameter('captcha.secret'), $this->getParameter('captcha.sitekey')) == true) { | ||||
|                 $user->setUrlId(uniqid()); | ||||
|                 // encode the plain password
 | ||||
|                 $user->setPassword( | ||||
|                     $passwordEncoder->hashPassword( | ||||
|                         $user, | ||||
|                         $form->get('plainPassword')->getData() | ||||
|                     ) | ||||
|                 ); | ||||
| 
 | ||||
|             $entityManager = $this->getDoctrine()->getManager(); | ||||
|             $entityManager->persist($user); | ||||
|             $entityManager->flush(); | ||||
|                 $entityManager = $this->getDoctrine()->getManager(); | ||||
|                 $entityManager->persist($user); | ||||
|                 $entityManager->flush(); | ||||
| 
 | ||||
|             // generate a signed url and email it to the user
 | ||||
|             $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user, | ||||
|                 (new TemplatedEmail()) | ||||
|                     ->from(new Address('no-reply@pflaenz.li', 'Pflänzli no-reply')) | ||||
|                     ->to($user->getEmail()) | ||||
|                     ->subject('Please Confirm your Email') | ||||
|                     ->htmlTemplate('registration/confirmation_email.html.twig') | ||||
|             ); | ||||
|             // do anything else you need here, like send an email
 | ||||
|                 // generate a signed url and email it to the user
 | ||||
|                 $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user, | ||||
|                     (new TemplatedEmail()) | ||||
|                         ->from(new Address('no-reply@pflaenz.li', 'Pflänzli no-reply')) | ||||
|                         ->to($user->getEmail()) | ||||
|                         ->subject('Please Confirm your Email') | ||||
|                         ->htmlTemplate('registration/confirmation_email.html.twig') | ||||
|                 ); | ||||
| 
 | ||||
|             return $guardHandler->authenticateUserAndHandleSuccess( | ||||
|                 $user, | ||||
|                 $request, | ||||
|                 $authenticator, | ||||
|                 'main' // firewall name in security.yaml
 | ||||
|             ); | ||||
|                 return $this->render('registration/created.html.twig'); | ||||
|             } | ||||
|             else { | ||||
|                 $this->addFlash('error', 'CAPTCHA failed'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return $this->render('registration/register.html.twig', [ | ||||
|  | @ -70,32 +70,20 @@ class RegistrationController extends AbstractController | |||
|     } | ||||
| 
 | ||||
|     #[Route('/verify/email', name: 'app_verify_email')]
 | ||||
|     public function verifyUserEmail(Request $request, UserRepository $userRepository): Response | ||||
|     public function verifyUserEmail(Request $request): Response | ||||
|     { | ||||
|         $id = $request->get('id'); | ||||
| 
 | ||||
|         if (null === $id) { | ||||
|             return $this->redirectToRoute('app_register'); | ||||
|         } | ||||
| 
 | ||||
|         $user = $userRepository->find($id); | ||||
| 
 | ||||
|         if (null === $user) { | ||||
|             return $this->redirectToRoute('app_register'); | ||||
|         } | ||||
|         $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); | ||||
| 
 | ||||
|         // validate email confirmation link, sets User::isVerified=true and persists
 | ||||
|         try { | ||||
|             $this->emailVerifier->handleEmailConfirmation($request, $user); | ||||
|             $this->emailVerifier->handleEmailConfirmation($request, $this->getUser()); | ||||
|         } catch (VerifyEmailExceptionInterface $exception) { | ||||
|             $this->addFlash('verify_email_error', $exception->getReason()); | ||||
| 
 | ||||
|             return $this->redirectToRoute('app_register'); | ||||
|         } | ||||
| 
 | ||||
|         // @TODO Change the redirect on success and handle or remove the flash message in your templates
 | ||||
|         $this->addFlash('success', 'Your email address has been verified.'); | ||||
| 
 | ||||
|         return $this->redirectToRoute('user_page'); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response; | |||
| use Symfony\Component\Mailer\MailerInterface; | ||||
| use Symfony\Component\Mime\Address; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; | ||||
| use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | ||||
| use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait; | ||||
| use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface; | ||||
| use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; | ||||
|  | @ -71,7 +71,7 @@ class ResetPasswordController extends AbstractController | |||
|      * Validates and process the reset URL that the user clicked in their email. | ||||
|      */ | ||||
|     #[Route('/reset/{token}', name: 'app_reset_password')]
 | ||||
|     public function reset(Request $request, UserPasswordEncoderInterface $passwordEncoder, string $token = null): Response | ||||
|     public function reset(Request $request, UserPasswordHasherInterface $passwordEncoder, string $token = null): Response | ||||
|     { | ||||
|         if ($token) { | ||||
|             // We store the token in session and remove it from the URL, to avoid the URL being
 | ||||
|  | @ -106,12 +106,13 @@ class ResetPasswordController extends AbstractController | |||
|             $this->resetPasswordHelper->removeResetRequest($token); | ||||
| 
 | ||||
|             // Encode the plain password, and set it.
 | ||||
|             $encodedPassword = $passwordEncoder->encodePassword( | ||||
|                 $user, | ||||
|                 $form->get('plainPassword')->getData() | ||||
|             $user->setPassword( | ||||
|                 $passwordEncoder->hashPassword( | ||||
|                     $user, | ||||
|                     $form->get('plainPassword')->getData() | ||||
|                 ) | ||||
|             ); | ||||
| 
 | ||||
|             $user->setPassword($encodedPassword); | ||||
|             $this->getDoctrine()->getManager()->flush(); | ||||
| 
 | ||||
|             // The session is cleaned up after the password has been changed.
 | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; | |||
| class SecurityController extends AbstractController | ||||
| { | ||||
|     /** | ||||
|      * @Route("/login", name="app_login") | ||||
|      * @Route("/login", name="app_login", options={"sitemap"=true}) | ||||
|      */ | ||||
|     public function login(AuthenticationUtils $authenticationUtils): Response | ||||
|     { | ||||
|  |  | |||
|  | @ -3,9 +3,9 @@ | |||
| namespace App\Controller; | ||||
| 
 | ||||
| use App\Entity\User; | ||||
| use App\Entity\Offering; | ||||
| use App\Entity\Offer; | ||||
| 
 | ||||
| use App\Repository\OfferingRepository; | ||||
| use App\Repository\OfferRepository; | ||||
| use App\Repository\WishRepository; | ||||
| 
 | ||||
| use Symfony\Bridge\Twig\Mime\TemplatedEmail; | ||||
|  | @ -19,8 +19,8 @@ use Symfony\Component\Routing\Annotation\Route; | |||
| 
 | ||||
| class TradeController extends AbstractController | ||||
| { | ||||
|     #[Route('/trade/{id}', name: 'trade')]
 | ||||
|     public function sendEmail(MailerInterface $mailer, Offering $offer, OfferingRepository $offeringRepository, WishRepository $wishRepository): Response | ||||
|     #[Route('/trade/{urlId}', name: 'trade')]
 | ||||
|     public function sendEmail(MailerInterface $mailer, Offer $offer, OfferRepository $offerRepository, WishRepository $wishRepository): Response | ||||
|     { | ||||
|         $user = $this->getUser(); | ||||
| 
 | ||||
|  | @ -35,7 +35,7 @@ class TradeController extends AbstractController | |||
|                 ->htmlTemplate('user/trade/offer_email.html.twig') | ||||
|                 ->context([ | ||||
|                     'user' => $user, | ||||
|                     'id' => $user->getId(), | ||||
|                     'urlId' => $user->getUrlId(), | ||||
|                 ]) | ||||
|             ; | ||||
|             try | ||||
|  | @ -49,6 +49,6 @@ class TradeController extends AbstractController | |||
|             $this->addFlash('error','You can\'t trade with yourself!'); | ||||
|         } | ||||
| 
 | ||||
|         return $this->redirectToRoute('show_offer', ['id' => $offer->getId()]); | ||||
|         return $this->redirectToRoute('show_offer', ['urlId' => $offer->getUrlId()]); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -5,8 +5,9 @@ namespace App\Controller; | |||
| use App\Entity\Wish; | ||||
| use App\Entity\User; | ||||
| use App\Form\WishFormType; | ||||
| use App\Form\ChangePasswordFormType; | ||||
| 
 | ||||
| use App\Repository\OfferingRepository; | ||||
| use App\Repository\OfferRepository; | ||||
| use App\Repository\WishRepository; | ||||
| 
 | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
|  | @ -14,7 +15,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpKernel\Exception\HttpException; | ||||
| use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| 
 | ||||
| use Twig\Environment; | ||||
| 
 | ||||
| class UserController extends AbstractController | ||||
|  | @ -27,7 +30,7 @@ class UserController extends AbstractController | |||
|     } | ||||
| 
 | ||||
|     #[Route('/user', name: 'user_page')]
 | ||||
|     public function user(OfferingRepository $offeringRepository): Response | ||||
|     public function user(OfferRepository $offerRepository, Request $request, UserPasswordHasherInterface $passwordEncoder): Response | ||||
|     { | ||||
|         $user = $this->getUser(); | ||||
| 
 | ||||
|  | @ -36,30 +39,48 @@ class UserController extends AbstractController | |||
|             $this->addFlash('error','Your email is not verified, please check your inbox'); | ||||
|         } | ||||
| 
 | ||||
|         $form = $this->createForm(ChangePasswordFormType::class); | ||||
|         $form->handleRequest($request); | ||||
| 
 | ||||
|         if ($form->isSubmitted() && $form->isValid()) { | ||||
|             $user->setPassword( | ||||
|                 $passwordEncoder->hashPassword( | ||||
|                     $user, | ||||
|                     $form->get('plainPassword')->getData() | ||||
|                 ) | ||||
|             ); | ||||
| 
 | ||||
|             $entityManager = $this->getDoctrine()->getManager(); | ||||
|             $entityManager->persist($user); | ||||
|             $entityManager->flush(); | ||||
| 
 | ||||
|             $this->addFlash("success", "Successfully changed the password!"); | ||||
|         } | ||||
| 
 | ||||
|         return $this->render('user/index.html.twig', [ | ||||
|             'user' => $user, | ||||
|             'offers' => $offeringRepository->findByUser($user), | ||||
|             'changePassword_form' => $form->createView(), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/user/offers', name: 'user_offers')]
 | ||||
|     public function userOffers(OfferingRepository $offeringRepository): Response | ||||
|     public function userOffers(OfferRepository $offerRepository): Response | ||||
|     { | ||||
|         $user = $this->getUser(); | ||||
| 
 | ||||
|         return $this->render('user/public.html.twig', [ | ||||
|             'user' => $user, | ||||
|             'offers' => $offeringRepository->findByUser($user), | ||||
|             'offers' => $offerRepository->findByUser($user), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/user/{id}', name: 'user_public')]
 | ||||
|     public function show_user(User $user, OfferingRepository $offeringRepository, WishRepository $wishRepository): Response | ||||
|     #[Route('/user/{urlId}', name: 'user_public')]
 | ||||
|     public function show_user(User $user, OfferRepository $offerRepository, WishRepository $wishRepository): Response | ||||
|     { | ||||
|         return $this->render('user/public.html.twig', [ | ||||
|             'username' => $user->getUsername(), | ||||
|             'wishes' => $wishRepository->findByUser($user), | ||||
|             'offers' => $offeringRepository->findByUser($user), | ||||
|             'offers' => $offerRepository->findByUser($user), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|  | @ -74,6 +95,7 @@ class UserController extends AbstractController | |||
| 
 | ||||
|         if ($form->isSubmitted() && $form->isValid()) { | ||||
|             $wish->setByUser($user); | ||||
|             $wish->setUrlId(uniqid()); | ||||
| 
 | ||||
|             $this->entityManager->persist($wish); | ||||
|             $this->entityManager->flush(); | ||||
|  | @ -89,7 +111,7 @@ class UserController extends AbstractController | |||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     #[Route('/wish/delete/{id}', name: 'delete_wish')]
 | ||||
|     #[Route('/wish/delete/{urlId}', name: 'delete_wish')]
 | ||||
|     public function deleteWish(Wish $wish): Response | ||||
|     { | ||||
|         $user = $this->getUser(); | ||||
|  |  | |||
|  | @ -2,14 +2,15 @@ | |||
| 
 | ||||
| namespace App\Entity; | ||||
| 
 | ||||
| use App\Repository\OfferingRepository; | ||||
| use App\Repository\OfferRepository; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
| use Location\Coordinate; | ||||
| use Symfony\Component\Validator\Constraints as Assert; | ||||
| 
 | ||||
| /** | ||||
|  * @ORM\Entity(repositoryClass=OfferingRepository::class) | ||||
|  * @ORM\Entity(repositoryClass=OfferRepository::class) | ||||
|  */ | ||||
| class Offering | ||||
| class Offer | ||||
| { | ||||
|     /** | ||||
|      * @ORM\Id | ||||
|  | @ -19,7 +20,7 @@ class Offering | |||
|     private $id; | ||||
| 
 | ||||
|     /** | ||||
|      * @ORM\ManyToOne(targetEntity=User::class, inversedBy="offerings") | ||||
|      * @ORM\ManyToOne(targetEntity=User::class, inversedBy="offers") | ||||
|      * @ORM\JoinColumn(nullable=false) | ||||
|      */ | ||||
|     private $byUser; | ||||
|  | @ -51,6 +52,21 @@ class Offering | |||
|      */ | ||||
|     private $description; | ||||
| 
 | ||||
|     /** | ||||
|      * @ORM\Column(type="float", nullable=true) | ||||
|      */ | ||||
|     private $lat; | ||||
| 
 | ||||
|     /** | ||||
|      * @ORM\Column(type="float", nullable=true) | ||||
|      */ | ||||
|     private $lng; | ||||
| 
 | ||||
|     /** | ||||
|      * @ORM\Column(type="string", length=13) | ||||
|      */ | ||||
|     private $urlId; | ||||
| 
 | ||||
|     public function getId(): ?int | ||||
|     { | ||||
|         return $this->id; | ||||
|  | @ -132,4 +148,31 @@ class Offering | |||
|     { | ||||
|         return (string) $this-getTitle(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     public function getCoordinate(): ?Coordinate | ||||
|     { | ||||
|         $coordinate = new Coordinate($this->lat, $this->lng); | ||||
| 
 | ||||
|         return $coordinate; | ||||
|     } | ||||
| 
 | ||||
|     public function setCoordinate(Coordinate $coordinate): self | ||||
|     { | ||||
|         $this->lat = $coordinate->getLat(); | ||||
|         $this->lng = $coordinate->getLng(); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| 
 | ||||
|     public function getUrlId(): ?string | ||||
|     { | ||||
|         return $this->urlId; | ||||
|     } | ||||
| 
 | ||||
|     public function setUrlId(string $urlId): self | ||||
|     { | ||||
|         $this->urlId = $urlId; | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| } | ||||
|  | @ -7,6 +7,7 @@ use Doctrine\Common\Collections\ArrayCollection; | |||
| use Doctrine\Common\Collections\Collection; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
| use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; | ||||
| use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; | ||||
| use Symfony\Component\Security\Core\User\UserInterface; | ||||
| 
 | ||||
| /** | ||||
|  | @ -14,7 +15,7 @@ use Symfony\Component\Security\Core\User\UserInterface; | |||
|  * @ORM\Table(name="`user`") | ||||
|  * @UniqueEntity(fields={"email"}, message="There is already an account with this email") | ||||
|  */ | ||||
| class User implements UserInterface | ||||
| class User implements UserInterface, PasswordAuthenticatedUserInterface | ||||
| { | ||||
|     /** | ||||
|      * @ORM\Id | ||||
|  | @ -50,9 +51,9 @@ class User implements UserInterface | |||
|     private $username; | ||||
| 
 | ||||
|     /** | ||||
|      * @ORM\OneToMany(targetEntity=Offering::class, mappedBy="byUser", orphanRemoval=true) | ||||
|      * @ORM\OneToMany(targetEntity=Offer::class, mappedBy="byUser", orphanRemoval=true) | ||||
|      */ | ||||
|     private $offerings; | ||||
|     private $offers; | ||||
| 
 | ||||
|     /** | ||||
|      * @ORM\OneToMany(targetEntity=Wish::class, mappedBy="byUser") | ||||
|  | @ -64,9 +65,14 @@ class User implements UserInterface | |||
|      */ | ||||
|     private $zipCode; | ||||
| 
 | ||||
|     /** | ||||
|      * @ORM\Column(type="string", length=13) | ||||
|      */ | ||||
|     private $urlId; | ||||
| 
 | ||||
|     public function __construct() | ||||
|     { | ||||
|         $this->offerings = new ArrayCollection(); | ||||
|         $this->offers = new ArrayCollection(); | ||||
|         $this->wishes = new ArrayCollection(); | ||||
|     } | ||||
| 
 | ||||
|  | @ -92,6 +98,14 @@ class User implements UserInterface | |||
|      * | ||||
|      * @see UserInterface | ||||
|      */ | ||||
|     public function getUserIdentifier(): string | ||||
|     { | ||||
|         return (string) $this->email; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated since Symfony 5.3, use getUserIdentifier instead | ||||
|      */ | ||||
|     public function getUsername(): string | ||||
|     { | ||||
|         return (string) $this->username; | ||||
|  | @ -117,11 +131,11 @@ class User implements UserInterface | |||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @see UserInterface | ||||
|      * @see PasswordAuthenticatedUserInterface | ||||
|      */ | ||||
|     public function getPassword(): string | ||||
|     { | ||||
|         return (string) $this->password; | ||||
|         return $this->password; | ||||
|     } | ||||
| 
 | ||||
|     public function setPassword(string $password): self | ||||
|  | @ -171,29 +185,29 @@ class User implements UserInterface | |||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @return Collection|Offering[] | ||||
|      * @return Collection|Offer[] | ||||
|      */ | ||||
|     public function getOfferings(): Collection | ||||
|     public function getOffers(): Collection | ||||
|     { | ||||
|         return $this->offerings; | ||||
|         return $this->offers; | ||||
|     } | ||||
| 
 | ||||
|     public function addOffering(Offering $offering): self | ||||
|     public function addOffer(Offer $offer): self | ||||
|     { | ||||
|         if (!$this->offerings->contains($offering)) { | ||||
|             $this->offerings[] = $offering; | ||||
|             $offering->setByUser($this); | ||||
|         if (!$this->offers->contains($offer)) { | ||||
|             $this->offers[] = $offer; | ||||
|             $offer->setByUser($this); | ||||
|         } | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| 
 | ||||
|     public function removeOffering(Offering $offering): self | ||||
|     public function removeOffer(Offer $offer): self | ||||
|     { | ||||
|         if ($this->offerings->removeElement($offering)) { | ||||
|         if ($this->offers->removeElement($offer)) { | ||||
|             // set the owning side to null (unless already changed)
 | ||||
|             if ($offering->getByUser() === $this) { | ||||
|                 $offering->setByUser(null); | ||||
|             if ($offer->getByUser() === $this) { | ||||
|                 $offer->setByUser(null); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -246,4 +260,16 @@ class User implements UserInterface | |||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| 
 | ||||
|     public function getUrlId(): ?string | ||||
|     { | ||||
|         return $this->urlId; | ||||
|     } | ||||
| 
 | ||||
|     public function setUrlId(string $urlId): self | ||||
|     { | ||||
|         $this->urlId = $urlId; | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -27,6 +27,11 @@ class Wish | |||
|      */ | ||||
|     private $byUser; | ||||
| 
 | ||||
|     /** | ||||
|      * @ORM\Column(type="string", length=13) | ||||
|      */ | ||||
|     private $urlId; | ||||
| 
 | ||||
|     public function getId(): ?int | ||||
|     { | ||||
|         return $this->id; | ||||
|  | @ -60,4 +65,16 @@ class Wish | |||
|     { | ||||
|         return (string) $this->getTitle(); | ||||
|     } | ||||
| 
 | ||||
|     public function getUrlId(): ?string | ||||
|     { | ||||
|         return $this->urlId; | ||||
|     } | ||||
| 
 | ||||
|     public function setUrlId(string $urlId): self | ||||
|     { | ||||
|         $this->urlId = $urlId; | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ namespace App\Form; | |||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\PasswordType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\RepeatedType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SubmitType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
| use Symfony\Component\Validator\Constraints\Length; | ||||
|  | @ -39,6 +40,7 @@ class ChangePasswordFormType extends AbstractType | |||
|                 // this is read and encoded in the controller
 | ||||
|                 'mapped' => false, | ||||
|             ]) | ||||
|             ->add('submit', SubmitType::class) | ||||
|         ; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										39
									
								
								src/Form/OfferFilterFormType.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/Form/OfferFilterFormType.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| <?php | ||||
| 
 | ||||
| namespace App\Form; | ||||
| 
 | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\NumberType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SubmitType; | ||||
| use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
| 
 | ||||
| class OfferFilterFormType extends AbstractType | ||||
| { | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options): void | ||||
|     { | ||||
|         $builder | ||||
|             ->add('search', TextType::class, [ | ||||
|                 'label' => '<i class="fas fa-search mr-1"></i>Search', | ||||
|                 'label_html' => true, | ||||
|             ]) | ||||
|             ->add('zipCode', NumberType::class, [ | ||||
|                 'label' => '<i class="fas fa-map-marker-alt mr-2"></i>ZIP', | ||||
|                 'label_html' => true, | ||||
|             ]) | ||||
|             ->add('distance', NumberType::class, [ | ||||
|                 'label' => '<i class="fas fa-map-signs mr-1"></i>Distance', | ||||
|                 'label_html' => true, | ||||
|             ]) | ||||
|             ->add('Apply', SubmitType::class) | ||||
|         ; | ||||
|     } | ||||
| 
 | ||||
|     public function configureOptions(OptionsResolver $resolver): void | ||||
|     { | ||||
|         $resolver->setDefaults([ | ||||
|             // Configure your form options here
 | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| namespace App\Form; | ||||
| 
 | ||||
| use App\Entity\Offering; | ||||
| use App\Entity\Offer; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\FileType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
|  | @ -12,7 +12,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; | |||
| use Symfony\Component\Validator\Constraints\Image; | ||||
| use Symfony\Component\Validator\Constraints\NotBlank; | ||||
| 
 | ||||
| class OfferingFormType extends AbstractType | ||||
| class OfferFormType extends AbstractType | ||||
| { | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
|     { | ||||
|  | @ -33,7 +33,7 @@ class OfferingFormType extends AbstractType | |||
|                 'required' => false, | ||||
|                 'mapped' => false, | ||||
|                 'constraints' => [ | ||||
|                     new Image(['maxSize' => '5096k']) | ||||
|                     new Image(['maxSize' => '10m']) | ||||
|                 ], | ||||
|             ]) | ||||
|             ->add('submit', SubmitType::class) | ||||
|  | @ -43,7 +43,7 @@ class OfferingFormType extends AbstractType | |||
|     public function configureOptions(OptionsResolver $resolver) | ||||
|     { | ||||
|         $resolver->setDefaults([ | ||||
|             'data_class' => Offering::class, | ||||
|             'data_class' => Offer::class, | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
|  | @ -6,12 +6,16 @@ use App\Entity\User; | |||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\CheckboxType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\EmailType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\HiddenType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\NumberType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\PasswordType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SubmitType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
| use Symfony\Component\Validator\Constraints\IsTrue; | ||||
| use Symfony\Component\Validator\Constraints\Length; | ||||
| use Symfony\Component\Validator\Constraints\NotBlank; | ||||
| use Symfony\Component\Validator\Constraints\NotNull; | ||||
| 
 | ||||
| class RegistrationFormType extends AbstractType | ||||
| { | ||||
|  | @ -20,19 +24,14 @@ class RegistrationFormType extends AbstractType | |||
|         $builder | ||||
|             ->add('email', EmailType::class) | ||||
|             ->add('username') | ||||
|             ->add('zipcode') | ||||
|             ->add('agreeTerms', CheckboxType::class, [ | ||||
|                 'mapped' => false, | ||||
|                 'constraints' => [ | ||||
|                     new IsTrue([ | ||||
|                         'message' => 'You should agree to our terms.', | ||||
|                     ]), | ||||
|                 ], | ||||
|             ->add('zipcode', NumberType::class, [ | ||||
|                 'label' => 'ZIP' | ||||
|             ]) | ||||
|             ->add('plainPassword', PasswordType::class, [ | ||||
|                 // instead of being set onto the object directly,
 | ||||
|                 // this is read and encoded in the controller
 | ||||
|                 'mapped' => false, | ||||
|                 'label' => 'Password', | ||||
|                 'constraints' => [ | ||||
|                     new NotBlank([ | ||||
|                         'message' => 'Please enter a password', | ||||
|  | @ -45,6 +44,30 @@ class RegistrationFormType extends AbstractType | |||
|                     ]), | ||||
|                 ], | ||||
|             ]) | ||||
|             ->add('agreeTerms', CheckboxType::class, [ | ||||
|                 'mapped' => false, | ||||
|                 'label' => 'Agree to <a href="/imprint" target="_blank">Terms</a>', | ||||
|                 'label_html' => true, | ||||
|                 'constraints' => [ | ||||
|                     new IsTrue([ | ||||
|                         'message' => 'You need to agree to our terms.', | ||||
|                     ]), | ||||
|                 ], | ||||
|             ]) | ||||
|             ->add('submit', SubmitType::class, [ | ||||
|                 'label' => 'Register', | ||||
|                 'attr' => [ | ||||
|                     'class' => 'btn-lg btn-primary', | ||||
|                 ], | ||||
|             ]) | ||||
|             ->add('captcha_solution', HiddenType::class, [ | ||||
|                 'mapped' => false, | ||||
|                 'constraints' => [ | ||||
|                     new NotNull([ | ||||
|                         'message' => 'Please wait for the CAPTCHA to complete', | ||||
|                     ]), | ||||
|                 ], | ||||
|             ]) | ||||
|         ; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,25 +2,25 @@ | |||
| 
 | ||||
| namespace App\Repository; | ||||
| 
 | ||||
| use App\Entity\Offering; | ||||
| use App\Entity\Offer; | ||||
| use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | ||||
| use Doctrine\Persistence\ManagerRegistry; | ||||
| 
 | ||||
| /** | ||||
|  * @method Offering|null find($id, $lockMode = null, $lockVersion = null) | ||||
|  * @method Offering|null findOneBy(array $criteria, array $orderBy = null) | ||||
|  * @method Offering[]    findAll() | ||||
|  * @method Offering[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) | ||||
|  * @method Offer|null find($id, $lockMode = null, $lockVersion = null) | ||||
|  * @method Offer|null findOneBy(array $criteria, array $orderBy = null) | ||||
|  * @method Offer[]    findAll() | ||||
|  * @method Offer[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) | ||||
|  */ | ||||
| class OfferingRepository extends ServiceEntityRepository | ||||
| class OfferRepository extends ServiceEntityRepository | ||||
| { | ||||
|     public function __construct(ManagerRegistry $registry) | ||||
|     { | ||||
|         parent::__construct($registry, Offering::class); | ||||
|         parent::__construct($registry, Offer::class); | ||||
|     } | ||||
| 
 | ||||
|     // /**
 | ||||
|     //  * @return Offering[] Returns an array of Offering objects
 | ||||
|     //  * @return Offer[] Returns an array of Offer objects
 | ||||
|     //  */
 | ||||
|     public function findByUser($user) | ||||
|     { | ||||
|  | @ -44,8 +44,21 @@ class OfferingRepository extends ServiceEntityRepository | |||
|         ; | ||||
|     } | ||||
| 
 | ||||
|     public function findBySearchLiteral(string $literal) | ||||
|     { | ||||
|         $qb = $this->createQueryBuilder('o'); | ||||
|         $qb->andWhere($qb->expr()->like('o.title', ':lit')) | ||||
|             ->setParameter('lit', '%' . $literal . '%') | ||||
|             ->orderBy('o.id', 'ASC') | ||||
|         ; | ||||
| 
 | ||||
|         $qb = $qb->getQuery()->getResult(); | ||||
| 
 | ||||
|         return $qb; | ||||
|     } | ||||
| 
 | ||||
|     /* | ||||
|     public function findOneBySomeField($value): ?Offering | ||||
|     public function findOneBySomeField($value): ?Offer | ||||
|     { | ||||
|         return $this->createQueryBuilder('o') | ||||
|             ->andWhere('o.exampleField = :val') | ||||
|  | @ -2,105 +2,59 @@ | |||
| 
 | ||||
| namespace App\Security; | ||||
| 
 | ||||
| use App\Entity\User; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Component\HttpFoundation\RedirectResponse; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||||
| use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||||
| use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; | ||||
| use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; | ||||
| use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Security\Core\User\UserInterface; | ||||
| use Symfony\Component\Security\Core\User\UserProviderInterface; | ||||
| use Symfony\Component\Security\Csrf\CsrfToken; | ||||
| use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; | ||||
| use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; | ||||
| use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; | ||||
| use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; | ||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; | ||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | ||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; | ||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | ||||
| use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; | ||||
| use Symfony\Component\Security\Http\Util\TargetPathTrait; | ||||
| 
 | ||||
| class AppAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface | ||||
| class AppAuthenticator extends AbstractLoginFormAuthenticator | ||||
| { | ||||
|     use TargetPathTrait; | ||||
| 
 | ||||
|     public const LOGIN_ROUTE = 'app_login'; | ||||
| 
 | ||||
|     private $entityManager; | ||||
|     private $urlGenerator; | ||||
|     private $csrfTokenManager; | ||||
|     private $passwordEncoder; | ||||
|     private UrlGeneratorInterface $urlGenerator; | ||||
| 
 | ||||
|     public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder) | ||||
|     public function __construct(UrlGeneratorInterface $urlGenerator) | ||||
|     { | ||||
|         $this->entityManager = $entityManager; | ||||
|         $this->urlGenerator = $urlGenerator; | ||||
|         $this->csrfTokenManager = $csrfTokenManager; | ||||
|         $this->passwordEncoder = $passwordEncoder; | ||||
|     } | ||||
| 
 | ||||
|     public function supports(Request $request) | ||||
|     public function authenticate(Request $request): PassportInterface | ||||
|     { | ||||
|         return self::LOGIN_ROUTE === $request->attributes->get('_route') | ||||
|             && $request->isMethod('POST'); | ||||
|     } | ||||
|         $email = $request->request->get('email', ''); | ||||
| 
 | ||||
|     public function getCredentials(Request $request) | ||||
|     { | ||||
|         $credentials = [ | ||||
|             'email' => $request->request->get('email'), | ||||
|             'password' => $request->request->get('password'), | ||||
|             'csrf_token' => $request->request->get('_csrf_token'), | ||||
|         ]; | ||||
|         $request->getSession()->set( | ||||
|             Security::LAST_USERNAME, | ||||
|             $credentials['email'] | ||||
|         $request->getSession()->set(Security::LAST_USERNAME, $email); | ||||
| 
 | ||||
|         return new Passport( | ||||
|             new UserBadge($email), | ||||
|             new PasswordCredentials($request->request->get('password', '')), | ||||
|             [ | ||||
|                 new CsrfTokenBadge('authenticate', $request->get('_csrf_token')), | ||||
|             ] | ||||
|         ); | ||||
| 
 | ||||
|         return $credentials; | ||||
|     } | ||||
| 
 | ||||
|     public function getUser($credentials, UserProviderInterface $userProvider) | ||||
|     public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | ||||
|     { | ||||
|         $token = new CsrfToken('authenticate', $credentials['csrf_token']); | ||||
|         if (!$this->csrfTokenManager->isTokenValid($token)) { | ||||
|             throw new InvalidCsrfTokenException(); | ||||
|         } | ||||
| 
 | ||||
|         $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]); | ||||
| 
 | ||||
|         if (!$user) { | ||||
|             // fail authentication with a custom error
 | ||||
|             throw new CustomUserMessageAuthenticationException('Email could not be found.'); | ||||
|         } | ||||
| 
 | ||||
|         return $user; | ||||
|     } | ||||
| 
 | ||||
|     public function checkCredentials($credentials, UserInterface $user) | ||||
|     { | ||||
|         return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Used to upgrade (rehash) the user's password automatically over time. | ||||
|      */ | ||||
|     public function getPassword($credentials): ?string | ||||
|     { | ||||
|         return $credentials['password']; | ||||
|     } | ||||
| 
 | ||||
|     public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey) | ||||
|     { | ||||
|         if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { | ||||
|         if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { | ||||
|             return new RedirectResponse($targetPath); | ||||
|         } | ||||
| 
 | ||||
|         return new RedirectResponse($this->urlGenerator->generate('user_page')); | ||||
|     } | ||||
| 
 | ||||
|     protected function getLoginUrl() | ||||
|     protected function getLoginUrl(Request $request): string | ||||
|     { | ||||
|         return $this->urlGenerator->generate(self::LOGIN_ROUTE); | ||||
|     } | ||||
| } | ||||
| } | ||||
							
								
								
									
										33
									
								
								src/Service/CaptchaVerifier.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/Service/CaptchaVerifier.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| <?php | ||||
| 
 | ||||
| namespace App\Service; | ||||
| 
 | ||||
| class CaptchaVerifier | ||||
| { | ||||
|     public function isVerified(string $solution, string $secret, string $sitekey) | ||||
|     { | ||||
|         $url = "https://api.friendlycaptcha.com/api/v1/siteverify"; | ||||
|         $data = array( | ||||
|             'solution' => $solution, | ||||
|             'secret'=> $secret, | ||||
|             'sitekey'=> $sitekey, | ||||
|         ); | ||||
| 
 | ||||
|         $options = array( | ||||
|             'http' => array( | ||||
|                 'method'  => 'POST', | ||||
|                 'content' => json_encode( $data ), | ||||
|                 'header'=>  "Content-Type: application/json\r\n" . | ||||
|                             "Accept: application/json\r\n" | ||||
|             ) | ||||
|         ); | ||||
|            | ||||
|         $context  = stream_context_create( $options ); | ||||
|         $result = file_get_contents( $url, false, $context ); | ||||
|         $response = json_decode( $result ); | ||||
| 
 | ||||
|         $isVerified = $response->success; | ||||
| 
 | ||||
|         return $isVerified; | ||||
|     } | ||||
| } | ||||
|  | @ -9,12 +9,19 @@ class DistanceCalculator | |||
| { | ||||
|     public function calculateDistance(Coordinate $coordinate1, Coordinate $coordinate2) | ||||
|     { | ||||
|         $calculator = new Vincenty(); | ||||
|         if ($coordinate1 == null || $coordinate2 == null) | ||||
|         { | ||||
|             $distance = "N/A"; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             $calculator = new Vincenty(); | ||||
| 
 | ||||
|         $distance = $calculator->getDistance($coordinate1, $coordinate2); | ||||
| 
 | ||||
|         $distance = round($distance / 1000); | ||||
|             $distance = $calculator->getDistance($coordinate1, $coordinate2); | ||||
| 
 | ||||
|             $distance = round($distance / 1000); | ||||
|         } | ||||
|          | ||||
|         return $distance; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/Service/OfferPhotoHelper.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/Service/OfferPhotoHelper.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | |||
| <?php | ||||
| 
 | ||||
| namespace App\Service; | ||||
| 
 | ||||
| use App\Entity\Offer; | ||||
| 
 | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\Filesystem\Exception\IOExceptionInterface; | ||||
| use Symfony\Component\Filesystem\Filesystem; | ||||
| use Symfony\Component\HttpFoundation\File\UploadedFile; | ||||
| 
 | ||||
| class OfferPhotoHelper | ||||
| { | ||||
|     private $logger; | ||||
| 
 | ||||
|     public function __construct(LoggerInterface $logger) | ||||
|     { | ||||
|         $this->filesystem = new Filesystem(); | ||||
|     } | ||||
| 
 | ||||
|     public function uploadOfferPhoto(string $photoDir, UploadedFile $photo, Offer $offer) | ||||
|     { | ||||
|         $filename = uniqid().'.'.$photo->guessExtension(); | ||||
|         try { | ||||
|             $photo->move($photoDir, $filename); | ||||
|         } catch (FileException $e) { | ||||
|             // unable to upload the photo, give up
 | ||||
|             $this->addFlash("error", "There was an error uploading the photo: ".$e); | ||||
|             return $this->redirectToRoute('new_offer'); | ||||
|         } | ||||
|         $offer->setPhotoFilename($filename); | ||||
|     } | ||||
| 
 | ||||
|     public function deleteOfferPhoto(string $photoDir, string $filename) | ||||
|     { | ||||
|         $file = $photoDir . '/' . $filename; | ||||
|         if($this->filesystem->exists($file)) { | ||||
|             $this->filesystem->remove($file); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										37
									
								
								src/Service/PhotoResizer.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/Service/PhotoResizer.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| <?php | ||||
| 
 | ||||
| // Source: https://symfony.com/doc/current/the-fast-track/en/23-imagine.html
 | ||||
| 
 | ||||
| namespace App\Service; | ||||
| 
 | ||||
| use Imagine\Gd\Imagine; | ||||
| use Imagine\Image\Box; | ||||
| 
 | ||||
| class PhotoResizer | ||||
| { | ||||
|     private const MAX_WIDTH = 1000; | ||||
|     private const MAX_HEIGHT = 1000; | ||||
| 
 | ||||
|     private $imagine; | ||||
| 
 | ||||
|     public function __construct() | ||||
|     { | ||||
|         $this->imagine = new Imagine(); | ||||
|     } | ||||
| 
 | ||||
|     public function resize(string $filename): void | ||||
|     { | ||||
|         list($iwidth, $iheight) = getimagesize($filename); | ||||
|         $ratio = $iwidth / $iheight; | ||||
|         $width = self::MAX_WIDTH; | ||||
|         $height = self::MAX_HEIGHT; | ||||
|         if ($width / $height > $ratio) { | ||||
|             $width = $height * $ratio; | ||||
|         } else { | ||||
|             $height = $width / $ratio; | ||||
|         } | ||||
| 
 | ||||
|         $photo = $this->imagine->open($filename); | ||||
|         $photo->resize(new Box($width, $height))->save($filename); | ||||
|     } | ||||
| } | ||||
|  | @ -6,13 +6,25 @@ use Location\Coordinate; | |||
| 
 | ||||
| class PlzToCoordinate | ||||
| { | ||||
|     public function getCoordinates(int $plz) | ||||
|     public function convertPlzToCoordinate(int $plz) | ||||
|     { | ||||
|         $content = file_get_contents("https://swisspost.opendatasoft.com/api/records/1.0/search/?dataset=plz_verzeichnis_v2&q=postleitzahl%3D" . $plz); | ||||
|         $result  = json_decode($content); | ||||
| 
 | ||||
|         if (isset($result->records[0]->fields->geo_point_2d[0]) && isset($result->records[0]->fields->geo_point_2d[1])) { | ||||
|             $coordinate = new Coordinate($result->records[0]->fields->geo_point_2d[0], $result->records[0]->fields->geo_point_2d[1]); | ||||
|         for($i = 0; $i < count($result->records); $i++) { | ||||
|             try { | ||||
|                 $lat = $result->records[$i]->fields->geo_point_2d[0]; | ||||
|                 $long = $result->records[$i]->fields->geo_point_2d[1]; | ||||
|             } catch (\Throwable $th) { | ||||
|                 // throw $th;
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (isset($lat) && isset($long)) { | ||||
|             $coordinate = new Coordinate($lat, $long); | ||||
|         } | ||||
|         else { | ||||
|             $coordinate = null; | ||||
|         } | ||||
| 
 | ||||
|         return $coordinate; | ||||
|  |  | |||
							
								
								
									
										41
									
								
								symfony.lock
									
										
									
									
									
								
							
							
						
						
									
										41
									
								
								symfony.lock
									
										
									
									
									
								
							|  | @ -97,9 +97,18 @@ | |||
|     "friendsofphp/proxy-manager-lts": { | ||||
|         "version": "v1.0.3" | ||||
|     }, | ||||
|     "imagine/imagine": { | ||||
|         "version": "1.2.4" | ||||
|     }, | ||||
|     "laminas/laminas-code": { | ||||
|         "version": "4.2.0" | ||||
|     }, | ||||
|     "laminas/laminas-eventmanager": { | ||||
|         "version": "3.3.1" | ||||
|     }, | ||||
|     "laminas/laminas-zendframework-bridge": { | ||||
|         "version": "1.2.0" | ||||
|     }, | ||||
|     "mjaschen/phpgeo": { | ||||
|         "version": "3.2.1" | ||||
|     }, | ||||
|  | @ -109,6 +118,9 @@ | |||
|     "nikic/php-parser": { | ||||
|         "version": "v4.10.4" | ||||
|     }, | ||||
|     "presta/sitemap-bundle": { | ||||
|         "version": "v3.2.1" | ||||
|     }, | ||||
|     "psr/cache": { | ||||
|         "version": "2.0.0" | ||||
|     }, | ||||
|  | @ -121,6 +133,9 @@ | |||
|     "psr/log": { | ||||
|         "version": "1.1.3" | ||||
|     }, | ||||
|     "samayo/bulletproof": { | ||||
|         "version": "v4.0.1" | ||||
|     }, | ||||
|     "sensio/framework-extra-bundle": { | ||||
|         "version": "5.2", | ||||
|         "recipe": { | ||||
|  | @ -243,12 +258,6 @@ | |||
|             "src/Kernel.php" | ||||
|         ] | ||||
|     }, | ||||
|     "symfony/http-client": { | ||||
|         "version": "v5.3.0" | ||||
|     }, | ||||
|     "symfony/http-client-contracts": { | ||||
|         "version": "v2.4.0" | ||||
|     }, | ||||
|     "symfony/http-foundation": { | ||||
|         "version": "v5.2.4" | ||||
|     }, | ||||
|  | @ -258,15 +267,6 @@ | |||
|     "symfony/intl": { | ||||
|         "version": "v5.2.4" | ||||
|     }, | ||||
|     "symfony/loco-translation-provider": { | ||||
|         "version": "5.3", | ||||
|         "recipe": { | ||||
|             "repo": "github.com/symfony/recipes", | ||||
|             "branch": "master", | ||||
|             "version": "5.3", | ||||
|             "ref": "c706da68af4ae956f45cffccf99a130e9484cb33" | ||||
|         } | ||||
|     }, | ||||
|     "symfony/mailer": { | ||||
|         "version": "4.3", | ||||
|         "recipe": { | ||||
|  | @ -316,7 +316,7 @@ | |||
|         "version": "v2.1.0" | ||||
|     }, | ||||
|     "symfony/password-hasher": { | ||||
|         "version": "v5.3.0" | ||||
|         "version": "v5.3.7" | ||||
|     }, | ||||
|     "symfony/phpunit-bridge": { | ||||
|         "version": "5.1", | ||||
|  | @ -348,15 +348,9 @@ | |||
|     "symfony/polyfill-mbstring": { | ||||
|         "version": "v1.22.1" | ||||
|     }, | ||||
|     "symfony/polyfill-php73": { | ||||
|         "version": "v1.22.1" | ||||
|     }, | ||||
|     "symfony/polyfill-php80": { | ||||
|         "version": "v1.22.1" | ||||
|     }, | ||||
|     "symfony/polyfill-php81": { | ||||
|         "version": "v1.23.0" | ||||
|     }, | ||||
|     "symfony/polyfill-uuid": { | ||||
|         "version": "v1.22.1" | ||||
|     }, | ||||
|  | @ -404,9 +398,6 @@ | |||
|     "symfony/security-csrf": { | ||||
|         "version": "v5.2.4" | ||||
|     }, | ||||
|     "symfony/security-guard": { | ||||
|         "version": "v5.2.4" | ||||
|     }, | ||||
|     "symfony/security-http": { | ||||
|         "version": "v5.2.6" | ||||
|     }, | ||||
|  |  | |||
							
								
								
									
										11
									
								
								templates/app/faq.html.twig
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								templates/app/faq.html.twig
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| {% extends 'base.html.twig' %} | ||||
| 
 | ||||
| {% block title %}Privacy Policy{% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
| <h1 class="mb-3">Frequently Asked Questions</h1> | ||||
| <h2 class="mb-3">Is it free?</h2> | ||||
| <p>Yes, pflänz.li is free to use and its source code is <a href="https://git.thisfro.ch/thisfro/pflaenz.li">publically accessible</a>.</p> | ||||
| <h2 class="mb-3">Can I help?</h2> | ||||
| Yes, feel free to contact <a href="mailto:jannis@thisfro.ch">@thisfro</a>!</p> | ||||
| {% endblock %} | ||||
|  | @ -57,12 +57,12 @@ | |||
| 
 | ||||
| </p> | ||||
| 
 | ||||
| <p>Pflänzl.i</p> | ||||
| <p>Langgrütstrasse 89</p> | ||||
| <p>8047 Zürich</p> | ||||
| <p>Pflänz.li</p> | ||||
| <p>Marbachweg 22</p> | ||||
| <p>8041 Zürich</p> | ||||
| <p>Switzerland</p> | ||||
| <p>Email: contact@pflaenz.li</p> | ||||
| <p>Website: pflänzl.i</p> | ||||
| <p>Website: pflänz.li</p> | ||||
| 
 | ||||
| <h4>3. Cookies</h4> | ||||
| <p>The Internet pages of the Pflänzl.i use cookies. Cookies are text files that are stored in a computer system via an Internet browser.</p> | ||||
|  |  | |||
|  | @ -2,11 +2,15 @@ | |||
| 
 | ||||
| {% block title %}Home{% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
| {% block meta %} | ||||
| <meta name="description" content="A platform to trade plants." > | ||||
| {% endblock %} | ||||
| 
 | ||||
| 
 | ||||
| {% block body %} | ||||
| <div class="jumbotron"> | ||||
|     <h1 class="display-4">{% trans %} homepage.welcome.header {% endtrans %}</h1> | ||||
|     <p class="lead">{% trans %} homepage.welcome.info {% endtrans %}</p> | ||||
|     <h1 class="display-4">Welcome to Pflänz.li</h1> | ||||
|     <p class="lead">This is a platform to trade plants. You can offer plants and setup a wishlist what you want to trade it for.</p> | ||||
|     <hr class="my-4"> | ||||
|     <p>To offer your plants, please register first.</p> | ||||
|     <a class="btn btn-primary btn-lg" href="{{ path('app_register') }}" role="button">Register now</a> | ||||
|  |  | |||
|  | @ -1,5 +1,11 @@ | |||
| {% extends 'base.html.twig' %} | ||||
| 
 | ||||
| {% block title %}New Offer{% endblock %} | ||||
| 
 | ||||
| {% block javascripts %} | ||||
| 	{{ encore_entry_script_tags('fileUpload') }} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
|     {% for message in app.flashes('error') %} | ||||
|     <div class="alert alert-error" role="alert"> | ||||
|  |  | |||
|  | @ -1,5 +1,12 @@ | |||
| {% extends 'base.html.twig' %} | ||||
| 
 | ||||
| {% block title %}Offer: {{ offer.title }}{% endblock %} | ||||
| 
 | ||||
| {% block meta %} | ||||
| <meta name="description" content="{{offer.byuser }} offers {{ offer.title}}!" > | ||||
| <meta name="author" content="{{ offer.byUser }}"> | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
|     {% for message in app.flashes('error') %} | ||||
|         <div class="alert alert-danger" role="alert"> | ||||
|  | @ -34,10 +41,10 @@ | |||
|                 </p> | ||||
|                 <p class="pr-3"> | ||||
|                     <i class="fas fa-map-marker-alt"></i> {{ offer.zipCode }}  | ||||
|                     {% if distance > 0 %} | ||||
|                         (ca. {{ distance }} km) | ||||
|                     {% endif %} | ||||
|                 </p> | ||||
|                 {% if distance > 0 %} | ||||
|                 <p class="pr-3"><i class="fas fa-map-signs mr-1"></i>ca. {{ distance }} km</p> | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|           <h3>Description</h3> | ||||
|           <p>{{ offer.description }}</p> | ||||
|  | @ -45,7 +52,7 @@ | |||
|     </div> | ||||
| 
 | ||||
|     {% if offer.byUser == user %} | ||||
|         <a href="{{ path('edit_offer', {'id': offer.id}) }}" class="btn btn-info mb-3"><i class="fas fa-pen"></i></a> | ||||
|         <a href="{{ path('edit_offer', {'urlId': offer.urlId}) }}" class="btn btn-info mb-3"><i class="fas fa-pen"></i></a> | ||||
|         <button type="button" class="btn btn-danger mb-3" data-toggle="modal" data-target="#exampleModal"><i class="fas fa-trash-alt"></i></button> | ||||
|         <!-- Modal --> | ||||
|         <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> | ||||
|  | @ -62,7 +69,7 @@ | |||
|                     </div> | ||||
|                     <div class="modal-footer"> | ||||
|                         <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> | ||||
|                         <a type="button" class="btn btn-danger" href="{{ path('delete_offer', {'id': offer.id}) }}">Delete</a> | ||||
|                         <a type="button" class="btn btn-danger" href="{{ path('delete_offer', {'urlId': offer.urlId}) }}">Delete</a> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | @ -83,6 +90,6 @@ | |||
|                 </ul> | ||||
|             {% endif %} | ||||
|             </div> | ||||
|             <a class="btn btn-primary mb-3" href="{{ path('trade', {'id': offer.id}) }}">Offer trade</a> | ||||
|             <a class="btn btn-primary mb-3" href="{{ path('trade', {'urlId': offer.urlId}) }}">Offer trade</a> | ||||
|         {% endif %} | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -1,85 +1,131 @@ | |||
| <!DOCTYPE html> | ||||
| <html> | ||||
|     <head> | ||||
|         <meta charset="UTF-8"> | ||||
|         <title>{% block title %}Plant Exchange Home{% endblock %}</title> | ||||
| <html lang="en"> | ||||
| 	<head> | ||||
| 		<meta charset="UTF-8"> | ||||
| 		<title>Pflänz.li - | ||||
| 			{% block title %}{% endblock %} | ||||
| 		</title> | ||||
| 		{% block meta %}{% endblock %} | ||||
| 		{% if app_env == 'prod' %} | ||||
| 		<meta name="robots" content="index,follow" /> | ||||
| 		{% endif %} | ||||
| 		<meta name="publisher" content="pflänz.li" /> | ||||
| 		<meta name="keywords" content="trade, share, plants, sustainability, pflanzentausch, pflanzen" /> | ||||
| 
 | ||||
|         {% block stylesheets %} | ||||
|             {{ encore_entry_link_tags('app') }} | ||||
|         {% endblock %} | ||||
| 		{% block stylesheets %} | ||||
| 			{{ encore_entry_link_tags('app') }} | ||||
| 		{% endblock %} | ||||
| 
 | ||||
|         {% block javascripts %} | ||||
|             {{ encore_entry_script_tags('app') }} | ||||
| 		{% block javascripts %}{% endblock %} | ||||
| 
 | ||||
|             <!-- Matomo --> | ||||
|             <script type="text/javascript"> | ||||
|               var _paq = window._paq = window._paq || []; | ||||
|               /* tracker methods like "setCustomDimension" should be called before "trackPageView" */ | ||||
|               _paq.push(['trackPageView']); | ||||
|               _paq.push(['enableLinkTracking']); | ||||
|               (function() { | ||||
|                 var u="//analytics.thisfro.ch/"; | ||||
|                 _paq.push(['setTrackerUrl', u+'matomo.php']); | ||||
|                 _paq.push(['setSiteId', '14']); | ||||
|                 var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; | ||||
|                 g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); | ||||
|               })(); | ||||
|             </script> | ||||
|             <!-- End Matomo Code --> | ||||
|         {% endblock %} | ||||
| 		{{ encore_entry_script_tags('app') }} | ||||
| 
 | ||||
|         <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     </head> | ||||
|     <body> | ||||
|       <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> | ||||
|           <a class="navbar-brand" href="{{ path('homepage') }}"><i class="fas fa-seedling"></i>Pflänz.li</a> | ||||
|           <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> | ||||
|             <span class="navbar-toggler-icon"></span> | ||||
|           </button> | ||||
|          | ||||
|           <div class="collapse navbar-collapse" id="navbarSupportedContent"> | ||||
|             <ul class="navbar-nav mr-auto"> | ||||
|               <li class="nav-item"> | ||||
|                 <a class="nav-link" href=" {{ path('offers') }} "><i class="fas fa-seedling"></i> Offers</a> | ||||
|               </li> | ||||
|               <li class="nav-item dropdown"> | ||||
|                 <a class="nav-link dropdown-toggle" data-toggle="dropdown"role="button" aria-haspopup="true" aria-expanded="false"><i class="fas fa-user"></i> User</a> | ||||
|                 <div class="dropdown-menu"> | ||||
|                   <a class="dropdown-item" href="{{ path('own_offers') }}"><i class="fas fa-seedling"></i> My Offers</a> | ||||
|                   <a class="dropdown-item" href="{{ path('wishlist') }}"><i class="fas fa-star"></i> Wishlist</a> | ||||
|                   <div class="dropdown-divider"></div> | ||||
|                   <a class="dropdown-item" href="{{ path('user_page') }}"><i class="fas fa-user"></i> User settings</a> | ||||
|                 </div> | ||||
|               </li> | ||||
|               <li> | ||||
|                 <a class="nav-link" href="{{ path('new_offer') }}"><i class="fas fa-plus-square"></i> New Offer</a> | ||||
|               </li> | ||||
|             </ul> | ||||
|             <span> | ||||
|                 {% if is_granted('ROLE_USER') %} | ||||
|                   <a type="button" class="btn btn-light" href="{{ path('app_logout') }}">Log out</a> | ||||
|                 {% else %} | ||||
|                   <a type="button" class="btn btn-light" href="{{ path('app_login') }}">Log in</a> | ||||
|                 {% endif %} | ||||
|               </span> | ||||
|           </div> | ||||
|         </nav> | ||||
| 		<!-- Matomo --> | ||||
| 		<script> | ||||
| 			var _paq = window._paq = window._paq || []; | ||||
| 			/* tracker methods like "setCustomDimension" should be called before "trackPageView" */ | ||||
| 			_paq.push(['trackPageView']); | ||||
| 			_paq.push(['enableLinkTracking']); | ||||
| 			(function () { | ||||
| 			var u = "https://analytics.thisfro.ch/"; | ||||
| 			_paq.push([ | ||||
| 			'setTrackerUrl', | ||||
| 			u + 'matomo.php' | ||||
| 			]); | ||||
| 			_paq.push(['setSiteId', '2']); | ||||
| 			var d = document, | ||||
| 			g = d.createElement('script'), | ||||
| 			s = d.getElementsByTagName('script')[0]; | ||||
| 			g.async = true; | ||||
| 			g.src = u + 'matomo.js'; | ||||
| 			s.parentNode.insertBefore(g, s); | ||||
| 			})(); | ||||
| 		</script> | ||||
| 		<!-- End Matomo Code --> | ||||
| 
 | ||||
|         <div class="container pt-5"> | ||||
|             {% block body %}{% endblock %} | ||||
|         </div> | ||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> | ||||
| 			<a class="navbar-brand" href="{{ path('homepage') }}"> | ||||
| 				<i class="fas fa-seedling"></i>Pflänz.li</a> | ||||
| 			<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> | ||||
| 				<span class="navbar-toggler-icon"></span> | ||||
| 			</button> | ||||
| 
 | ||||
|         <footer class="text-center text-white"> | ||||
|           <div class="pt-1"> | ||||
|             <section class="mb-1"> | ||||
|               <a class="btn btn-link btn-floating btn-lg text-dark m-1" href="#!" role="button" data-mdb-ripple-color="dark"><i class="fab fa-mastodon"></i></a> | ||||
|               <a class="btn btn-link btn-floating btn-lg text-dark m-1" href="#!" role="button" data-mdb-ripple-color="dark"><i class="fab fa-github"></i></a> | ||||
|             </section> | ||||
|           </div> | ||||
|           <div class="text-center text-dark"> | ||||
|             <a href="https://creativecommons.org"><i class="fab fa-creative-commons"></i><i class="fab fa-creative-commons-by"></i></a> | ||||
|             <a class="text-dark" href="{{ path('homepage') }}">pflänz.li</a> | ||||
|           </div> | ||||
|         </footer> | ||||
|     </body> | ||||
| </html> | ||||
| 			<div class="collapse navbar-collapse" id="navbarSupportedContent"> | ||||
| 				<ul class="navbar-nav mr-auto"> | ||||
| 					<li class="nav-item"> | ||||
| 						<a class="nav-link" href=" {{ path('offers') }} "> | ||||
| 							<i class="fas fa-seedling"></i> | ||||
| 							Offers</a> | ||||
| 					</li> | ||||
| 					<li class="nav-item dropdown"> | ||||
| 						<a class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> | ||||
| 							<i class="fas fa-user"></i> | ||||
| 							User</a> | ||||
| 						<div class="dropdown-menu"> | ||||
| 							<a class="dropdown-item" href="{{ path('own_offers') }}"> | ||||
| 								<i class="fas fa-seedling"></i> | ||||
| 								My Offers</a> | ||||
| 							<a class="dropdown-item" href="{{ path('wishlist') }}"> | ||||
| 								<i class="fas fa-star"></i> | ||||
| 								Wishlist</a> | ||||
| 							<div class="dropdown-divider"></div> | ||||
| 							<a class="dropdown-item" href="{{ path('user_page') }}"> | ||||
| 								<i class="fas fa-user"></i> | ||||
| 								User settings</a> | ||||
| 						</div> | ||||
| 					</li> | ||||
| 					<li> | ||||
| 						<a class="nav-link" href="{{ path('new_offer') }}"> | ||||
| 							<i class="fas fa-plus-square"></i> | ||||
| 							New Offer</a> | ||||
| 					</li> | ||||
| 				</ul> | ||||
| 				<span> | ||||
| 					{% if is_granted('ROLE_USER') %} | ||||
| 						<a class="btn btn-light" href="{{ path('app_logout') }}">Log out</a> | ||||
| 					{% else %} | ||||
| 						<a class="btn btn-light" href="{{ path('app_login') }}">Log in</a> | ||||
| 					{% endif %} | ||||
| 				</span> | ||||
| 			</div> | ||||
| 		</nav> | ||||
| 
 | ||||
| 		<div class="container pt-5"> {% block body %}{% endblock %} | ||||
| 			</div> | ||||
| 
 | ||||
| 			<footer class="text-dark p-3"> | ||||
| 				<div class="row"> | ||||
| 					<div class="col-lg"></div> | ||||
| 					<div class="col-lg text-center pt-3"> | ||||
| 						<section class="mb-1"> | ||||
| 							<h2 class="d-none">Social Links</h2> | ||||
| 							<a class="btn btn-link btn-floating btn-lg text-dark m-1" rel="me" href="https://mastodon.social/@pflaenzli" role="button" data-mdb-ripple-color="dark"> | ||||
| 								<i class="fab fa-mastodon"></i> | ||||
| 							</a> | ||||
| 							<a class="btn btn-link btn-floating btn-lg text-dark m-1" href="https://git.thisfro.ch/thisfro/pflaenz.li" role="button" data-mdb-ripple-color="dark"> | ||||
| 								<i class="fab fa-git-alt"></i> | ||||
| 							</a> | ||||
| 						</section> | ||||
| 					</div> | ||||
| 					<div class="col-lg pt-4 pl-5"> | ||||
| 						<section> | ||||
| 							<h2 class="h5">Links</h2> | ||||
| 							<ul class="link-list"> | ||||
| 								<li><a href="https://blog.pflaenz.li">Blog</a></li> | ||||
| 								<li><a href="{{ path('imprint') }}">Imprint</a></li> | ||||
|                 <li><a href="{{ path('faq') }}">FAQ</a></li> | ||||
| 							</ul> | ||||
| 						</section> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div class="row pt-3"> | ||||
| 					<div class="text-center text-dark w-100"> | ||||
| 						<a class="text-dark" href="{{ path('homepage') }}"><i class="far fa-copyright mr-1"></i>{{ 'now' | date('Y') }} pflänz.li</a> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</footer> | ||||
| 		</body> | ||||
| 	</html> | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| {% extends 'base.html.twig' %} | ||||
| 
 | ||||
| {% block title%}Edit offer{% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
|     {% for message in app.flashes('error') %} | ||||
|     <div class="alert alert-error" role="alert"> | ||||
|  | @ -7,7 +9,7 @@ | |||
|     </div> | ||||
|     {% endfor %} | ||||
| 
 | ||||
|     <h1 class="mb-3">Add new offer</h1> | ||||
|     <h1 class="mb-3">Edit offer</h1> | ||||
|     {{ form_start(offer_form) }} | ||||
|         {{ form_row(offer_form.title) }} | ||||
|         {{ form_row(offer_form.zipCode) }} | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| {% extends 'base.html.twig' %} | ||||
| 
 | ||||
| {% block title %}Offers{% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
| 
 | ||||
|     {% for message in app.flashes('success') %} | ||||
|  | @ -7,6 +9,20 @@ | |||
|         {{ message }} | ||||
|     </div> | ||||
|     {% endfor %} | ||||
|     {% for message in app.flashes('error') %} | ||||
|     <div class="alert alert-danger" role="alert"> | ||||
|         {{ message }} | ||||
|     </div> | ||||
|     {% endfor %} | ||||
| 
 | ||||
|     <div class="mb-5"> | ||||
|         <a class="btn btn-primary" data-toggle="collapse" href="#collapseExample" role="button" aria-expanded="false" aria-controls="collapseExample"> | ||||
|             <div class="btn btn-primary"><i class="fas fa-filter mr-3"></i>Filter<i class="fas fa-chevron-down ml-3 dropdown-collapse"></i></div> | ||||
|         </a> | ||||
|         <div class="collapse" id="collapseExample"> | ||||
|             {{ form(filter_form, {attr: {novalidate: 'novalidate'}}) }} | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <h1>Offers</h1> | ||||
|     {% if offers|length > 0 %} | ||||
|  | @ -14,7 +30,7 @@ | |||
|             {% for offer in offers %} | ||||
|                 <div class="mb-5"> | ||||
|                     <div class="card offer h-100"> | ||||
|                         <a href="{{ path('show_offer', {'id': offer.id }) }}"> | ||||
|                         <a href="{{ path('show_offer', {'urlId': offer.urlId }) }}"> | ||||
|                             {% if offer.photoFilename %} | ||||
|                                 <img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" /> | ||||
|                             {% else %} | ||||
|  | @ -25,7 +41,7 @@ | |||
|                             </div> | ||||
|                         </a> | ||||
|                         <div class="card-footer offer-footer"> | ||||
|                             <a class="user-link" href="{{ path('user_public', { 'id': offer.byuser.id }) }}"> | ||||
|                             <a class="user-link" href="{{ path('user_public', { 'urlId': offer.byuser.urlId }) }}"> | ||||
|                                 <p class="username"><i class="fas fa-user mt-3"></i> {{ offer.byUser }}</p> | ||||
|                             </a> | ||||
|                             <p class="zip"><i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipCode }}</p> | ||||
|  | @ -35,6 +51,6 @@ | |||
|             {% endfor %} | ||||
|         </div> | ||||
|     {% else %} | ||||
|         <div class="alert alert-warning" role="alert">There are currently no active offers.</div> | ||||
|         <div class="alert alert-warning" role="alert">There are no active offers with the current filter.</div> | ||||
|     {% endif %} | ||||
| {% endblock %} | ||||
							
								
								
									
										9
									
								
								templates/registration/created.html.twig
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								templates/registration/created.html.twig
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| {% extends 'base.html.twig' %} | ||||
| 
 | ||||
| {% block title %}Account created{% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
|     <h1>Your account has been created!</h1> | ||||
| 
 | ||||
|     <p>Check your inbox for the verification email!</p> | ||||
| {% endblock %} | ||||
|  | @ -1,32 +1,35 @@ | |||
| {% extends 'base.html.twig' %} | ||||
| 
 | ||||
| {% block title %}Register{% endblock %} | ||||
| {% block title %}Register | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block meta %} | ||||
| <meta name="description" content="Register for pflänz.li" /> | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block javascripts %} | ||||
| 	{{ encore_entry_script_tags('captcha') }} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
|     {% for flashError in app.flashes('verify_email_error') %} | ||||
|         <div class="alert alert-danger" role="alert">{{ flashError }}</div> | ||||
|     {% endfor %} | ||||
| 	{% for flashError in app.flashes('verify_email_error') %} | ||||
| 		<div class="alert alert-danger" role="alert">{{ flashError }}</div> | ||||
| 	{% endfor %} | ||||
| 	{% for message in app.flashes('error') %} | ||||
| 		<div class="alert alert-danger" role="alert"> | ||||
| 			{{ message }} | ||||
| 		</div> | ||||
| 	{% endfor %} | ||||
| 
 | ||||
|     <h1>Register</h1> | ||||
| 	<h1>Register</h1> | ||||
| 
 | ||||
|     {{ form_start(registrationForm) }} | ||||
|         {{ form_row(registrationForm.email) }} | ||||
|         {{ form_row(registrationForm.username) }} | ||||
|         {{ form_row(registrationForm.zipcode, { | ||||
|             label: 'PLZ' | ||||
|         }) }} | ||||
|         {{ form_row(registrationForm.plainPassword, { | ||||
|             label: 'Password' | ||||
|         }) }} | ||||
|         {{ form_row(registrationForm.agreeTerms) }} | ||||
| 
 | ||||
| 
 | ||||
|         <div class="form-group row"> | ||||
|             <label class="col-form-label col-sm-2">CAPTCHA</label> | ||||
|             <div class="col-sm-10"> | ||||
|                 <div class="frc-captcha" data-sitekey="FCMLGE739LB528NG"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <button type="submit" class="btn btn-lg btn-primary">Register</button> | ||||
|     {{ form_end(registrationForm) }} | ||||
| 	{{ form_start(registrationForm) }} | ||||
| 	{{ form_widget(registrationForm) }} | ||||
| 	<div class="form-group row"> | ||||
| 		<label class="col-form-label col-sm-2">CAPTCHA</label> | ||||
| 		<div class="col-sm-10"> | ||||
| 			<div class="frc-captcha" data-sitekey="FCMLGE739LB528NG" id="captcha"></div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	{{ form_end(registrationForm) }} | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ | |||
|     <h1>Reset your password</h1> | ||||
| 
 | ||||
|     {{ form_start(resetForm) }} | ||||
|         {{ form_row(resetForm.plainPassword) }} | ||||
|         <button class="btn btn-primary">Reset password</button> | ||||
|         {{form_widget(resetForm)}} | ||||
|     {{ form_end(resetForm) }} | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -2,6 +2,10 @@ | |||
| 
 | ||||
| {% block title %}Log in{% endblock %} | ||||
| 
 | ||||
| {% block meta %} | ||||
| <meta name="description" content="Register for pflänz.li" | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
| <form method="post"> | ||||
|     {% if error %} | ||||
|  |  | |||
|  | @ -1,55 +1,33 @@ | |||
| {% extends 'base.html.twig' %} | ||||
| 
 | ||||
| {% block title %}User{% endblock %} | ||||
| {% block title %}User | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
|     {% for message in app.flashes('error') %} | ||||
|         <div class="alert alert-danger" role="alert"> | ||||
|             {{ message }} | ||||
|         </div> | ||||
|     {% endfor %} | ||||
|     {% for message in app.flashes('success') %} | ||||
|         <div class="alert alert-success" role="alert"> | ||||
|             {{ message }} | ||||
|         </div> | ||||
|     {% endfor %} | ||||
| 	{% for message in app.flashes('error') %} | ||||
| 		<div class="alert alert-danger" role="alert"> | ||||
| 			{{ message }} | ||||
| 		</div> | ||||
| 	{% endfor %} | ||||
| 	{% for message in app.flashes('success') %} | ||||
| 		<div class="alert alert-success" role="alert"> | ||||
| 			{{ message }} | ||||
| 		</div> | ||||
| 	{% endfor %} | ||||
| 
 | ||||
|     <div class="alert alert-info" role="alert"> | ||||
|         Please note: This is not yet functional! | ||||
|     </div> | ||||
| 	<div class="mb-5"> | ||||
| 		<h1>Hello | ||||
| 			{{ user.username }}!</p> | ||||
| 	</div> | ||||
| 	<div class="mb-5"> | ||||
| 		<h2>Change Password</h2> | ||||
| 		{{ form_start(changePassword_form) }} | ||||
| 		{{ form_widget(changePassword_form) }} | ||||
| 		{{ form_end(changePassword_form) }} | ||||
| 	</div> | ||||
| 
 | ||||
|     <div class="mb-3"> | ||||
|         <h1>Hello {{ user.username }}!</p> | ||||
|     </div> | ||||
|     <div class="mb-3"> | ||||
|         <form method="post"> | ||||
|             <h3 class="mb-3 font-weight-normal">Change your user data</h3> | ||||
|             <div class="mb-3"> | ||||
|                 <label for="inputEmail" class="form-label">Email address</label> | ||||
|                 <input name="email" type="email" class="form-control" id="inputEmail" aria-describedby="emailHelp" placeholder="{{ app.user.email }}" readonly> | ||||
|             </div> | ||||
|             <div class="mb-3"> | ||||
|                 <label for="inputPassword">Password</label> | ||||
|                 <input type="password" name="password" id="inputPassword" class="form-control"> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="mb-3"> | ||||
|                 <label for="inputPlz">PLZ</label> | ||||
|                 <input name="plz" id="inputPlz" class="form-control" value="{{ user.zipcode }}"> | ||||
|             </div> | ||||
| 
 | ||||
|             <input type="hidden" name="_csrf_token" | ||||
|                 value="{{ csrf_token('authenticate') }}" | ||||
|             > | ||||
| 
 | ||||
|             <button class="btn btn-lg btn-primary" type="submit"> | ||||
|                 Save | ||||
|             </button> | ||||
|         </form> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="mb-3"> | ||||
|         <h3 class="mb-3">Delete Account</h3> | ||||
|         <button class="btn btn-danger">Delete Account</button> | ||||
|     </div> | ||||
| 	<div class="mb-3"> | ||||
| 		<h2>Delete Account</h2> | ||||
| 		<button class="btn btn-danger">Delete Account</button> | ||||
| 	</div> | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ | |||
|             {% for offer in offers %} | ||||
|                 <div class="mb-5"> | ||||
|                     <div class="card offer h-100"> | ||||
|                         <a href="{{ path('show_offer', {'id': offer.id }) }}"> | ||||
|                         <a href="{{ path('show_offer', {'urlId': offer.urlId }) }}"> | ||||
|                             {% if offer.photoFilename %} | ||||
|                                 <img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" /> | ||||
|                             {% else %} | ||||
|  | @ -26,12 +26,12 @@ | |||
|                             </div> | ||||
|                         </a> | ||||
|                         <div class="card-footer offer-footer"> | ||||
|                             <a href="{{ path('edit_offer', {'id': offer.id}) }}" class="btn btn-info"><i class="fas fa-pen"></i></a> | ||||
|                             <button type="button" class="btn btn-danger" data-toggle="modal" data-target="#offer-modal-{{ offer.id }}"><i class="fas fa-trash-alt"></i></button> | ||||
|                             <a href="{{ path('edit_offer', {'urlId': offer.urlId}) }}" class="btn btn-info"><i class="fas fa-pen"></i></a> | ||||
|                             <button type="button" class="btn btn-danger" data-toggle="modal" data-target="#offer-modal-{{ offer.urlId }}"><i class="fas fa-trash-alt"></i></button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <!-- Modal --> | ||||
|                     <div class="modal fade" id="offer-modal-{{ offer.id }}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> | ||||
|                     <div class="modal fade" id="offer-modal-{{ offer.urlId }}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> | ||||
|                         <div class="modal-dialog" role="document"> | ||||
|                             <div class="modal-content"> | ||||
|                                 <div class="modal-header"> | ||||
|  | @ -45,7 +45,7 @@ | |||
|                                 </div> | ||||
|                                 <div class="modal-footer"> | ||||
|                                     <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> | ||||
|                                     <a type="button" class="btn btn-danger" href="{{ path('delete_offer', {'id': offer.id}) }}">Delete</a> | ||||
|                                     <a type="button" class="btn btn-danger" href="{{ path('delete_offer', {'urlId': offer.urlId}) }}">Delete</a> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| {% extends 'base.html.twig' %} | ||||
| 
 | ||||
| {% block title %}Whishlist{% endblock %} | ||||
| {% block title %}User {{ username }}{% endblock %} | ||||
| 
 | ||||
| {% block body %} | ||||
|     {% for message in app.flashes('success') %} | ||||
|  | @ -36,7 +36,7 @@ | |||
|             {% for offer in offers %} | ||||
|                 <div class="mb-5"> | ||||
|                     <div class="card offer h-100"> | ||||
|                         <a href="{{ path('show_offer', {'id': offer.id }) }}"> | ||||
|                         <a href="{{ path('show_offer', {'urlId': offer.urlId }) }}"> | ||||
|                             {% if offer.photoFilename %} | ||||
|                                 <img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" /> | ||||
|                             {% else %} | ||||
|  | @ -48,7 +48,7 @@ | |||
|                             </div> | ||||
|                         </a> | ||||
|                         <div class="card-footer offer-footer"> | ||||
|                             <a class="user-link" href="{{ path('user_public', { 'id': offer.byuser.id }) }}"> | ||||
|                             <a class="user-link" href="{{ path('user_public', { 'urlId': offer.byuser.id }) }}"> | ||||
|                                 <p class="username"><i class="fas fa-user mt-3"></i> {{ offer.byUser }}</p> | ||||
|                             </a> | ||||
|                             <p class="zip"><i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipCode }}</p> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <h1>{{ user.username }} wants to trade!</h1> | ||||
| 
 | ||||
| <p>Checkout {{ user.username}}'s offers:</p> | ||||
| <a href="{{ url('user_public', {'id': id}) }}">Link</a> | ||||
| <a href="{{ url('user_public', {'urlId': urlId}) }}">Link</a> | ||||
| <p>Reply to this email to start trading.</p> | ||||
|  | @ -28,7 +28,7 @@ | |||
|         {% for wish in wishes %} | ||||
|             <li class="list-group-item d-flex justify-content-between align-items-center"> {{ wish.title }} | ||||
|                 <span> | ||||
|                     <a href="{{ path('delete_wish', {'id': wish.id}) }}" class="btn btn-danger" aria-label="Delete"><i class="fas fa-trash-alt" aria-hidden="true" title="Delete"></i></a> | ||||
|                     <a href="{{ path('delete_wish', {'urlId': wish.urlid}) }}" class="btn btn-danger" aria-label="Delete"><i class="fas fa-trash-alt" aria-hidden="true" title="Delete"></i></a> | ||||
|                 </span> | ||||
|             </li> | ||||
|         {% endfor %} | ||||
|  |  | |||
|  | @ -21,6 +21,8 @@ Encore | |||
|      * and one CSS file (e.g. app.css) if your JavaScript imports CSS. | ||||
|      */ | ||||
|     .addEntry('app', './assets/app.js') | ||||
|     .addEntry('captcha', './assets/captcha.js') | ||||
|     .addEntry('fileUpload', './assets/fileUpload.js') | ||||
| 
 | ||||
|     // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
 | ||||
|     .enableStimulusBridge('./assets/controllers.json') | ||||
|  |  | |||
		Reference in a new issue