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 |
81 changed files with 13455 additions and 15375 deletions
11
.env
11
.env
|
@ -23,12 +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
|
||||
###< symfony/mailer ###
|
||||
|
||||
DEFAULT_URI='http://localhost:8080/'
|
||||
DEFAULT_URI='http://localhost:8080/'
|
||||
|
||||
###> Ffiendlycaptcha ###
|
||||
# CAPTCHA_SECRET=
|
||||
# CAPTCHA_SITEKEY=
|
||||
###< Ffiendlycaptcha ###
|
||||
|
|
34
.env.staging
34
.env.staging
|
@ -1,34 +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/"
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
|||
/public/uploads/*
|
||||
/public/uploads/photos/*
|
||||
.DS_Store
|
||||
/pgadmin_data
|
||||
|
||||
|
@ -243,3 +243,6 @@ temp/
|
|||
# .pnp.*
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,phpunit,symfony,composer,yarn
|
||||
|
||||
# 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!
|
31
Dockerfile
31
Dockerfile
|
@ -1,31 +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
|
||||
|
||||
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,38 +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.2.*",
|
||||
"symfony/console": "5.2.*",
|
||||
"symfony/dotenv": "5.2.*",
|
||||
"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.2.*",
|
||||
"symfony/framework-bundle": "5.2.*",
|
||||
"symfony/mailer": "5.2.*",
|
||||
"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.2.*",
|
||||
"symfony/security-bundle": "5.2.*",
|
||||
"symfony/twig-bundle": "5.2.*",
|
||||
"symfony/validator": "5.2.*",
|
||||
"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.2.*",
|
||||
"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": {
|
||||
|
@ -74,17 +83,17 @@
|
|||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "5.2.*"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
3336
composer.lock
generated
3336
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,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;
|
||||
|
|
24
symfony.lock
24
symfony.lock
|
@ -97,6 +97,9 @@
|
|||
"friendsofphp/proxy-manager-lts": {
|
||||
"version": "v1.0.3"
|
||||
},
|
||||
"imagine/imagine": {
|
||||
"version": "1.2.4"
|
||||
},
|
||||
"laminas/laminas-code": {
|
||||
"version": "4.2.0"
|
||||
},
|
||||
|
@ -115,6 +118,9 @@
|
|||
"nikic/php-parser": {
|
||||
"version": "v4.10.4"
|
||||
},
|
||||
"presta/sitemap-bundle": {
|
||||
"version": "v3.2.1"
|
||||
},
|
||||
"psr/cache": {
|
||||
"version": "2.0.0"
|
||||
},
|
||||
|
@ -127,6 +133,9 @@
|
|||
"psr/log": {
|
||||
"version": "1.1.3"
|
||||
},
|
||||
"samayo/bulletproof": {
|
||||
"version": "v4.0.1"
|
||||
},
|
||||
"sensio/framework-extra-bundle": {
|
||||
"version": "5.2",
|
||||
"recipe": {
|
||||
|
@ -249,9 +258,6 @@
|
|||
"src/Kernel.php"
|
||||
]
|
||||
},
|
||||
"symfony/http-client-contracts": {
|
||||
"version": "v2.4.0"
|
||||
},
|
||||
"symfony/http-foundation": {
|
||||
"version": "v5.2.4"
|
||||
},
|
||||
|
@ -309,6 +315,9 @@
|
|||
"symfony/orm-pack": {
|
||||
"version": "v2.1.0"
|
||||
},
|
||||
"symfony/password-hasher": {
|
||||
"version": "v5.3.7"
|
||||
},
|
||||
"symfony/phpunit-bridge": {
|
||||
"version": "5.1",
|
||||
"recipe": {
|
||||
|
@ -339,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"
|
||||
},
|
||||
|
@ -395,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,6 +2,11 @@
|
|||
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
<meta name="description" content="A platform to trade plants." >
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block body %}
|
||||
<div class="jumbotron">
|
||||
<h1 class="display-4">Welcome to Pflänz.li</h1>
|
||||
|
|
|
@ -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