diff --git a/Server/.DS_Store b/Server/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..b5910d166eb60b1202226d02e92e0548a189dd15 Binary files /dev/null and b/Server/.DS_Store differ diff --git a/Server/.env.dev b/Server/.env.dev new file mode 100644 index 0000000000000000000000000000000000000000..f041153c504c4b5d732326af456e7080f72019e3 --- /dev/null +++ b/Server/.env.dev @@ -0,0 +1,4 @@ + +###> symfony/framework-bundle ### +APP_SECRET=f915e49a66085e034470522edccc5376 +###< symfony/framework-bundle ### diff --git a/Server/.gitignore b/Server/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..23731a63410526a8f42aeb8d022292e55b50e868 --- /dev/null +++ b/Server/.gitignore @@ -0,0 +1,12 @@ + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +/.env +/compose.yaml +###< symfony/framework-bundle ### diff --git a/Server/bin/console b/Server/bin/console new file mode 100644 index 0000000000000000000000000000000000000000..d8d530e2c36a68f7916acdfa16c9afafcad06bf0 --- /dev/null +++ b/Server/bin/console @@ -0,0 +1,21 @@ +#!/usr/bin/env php +<?php + +use App\Kernel; +use Symfony\Bundle\FrameworkBundle\Console\Application; + +if (!is_dir(dirname(__DIR__).'/vendor')) { + throw new LogicException('Dependencies are missing. Try running "composer install".'); +} + +if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) { + throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".'); +} + +require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + +return function (array $context) { + $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); + + return new Application($kernel); +}; diff --git a/Server/compose.override.yaml b/Server/compose.override.yaml new file mode 100644 index 0000000000000000000000000000000000000000..171a4820e290c0707db133195bcf319293af664e --- /dev/null +++ b/Server/compose.override.yaml @@ -0,0 +1,7 @@ + +services: +###> doctrine/doctrine-bundle ### + db: + ports: + - "5432" +###< doctrine/doctrine-bundle ### diff --git a/Server/composer.json b/Server/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..4bd6edb80cb18fb58154807537b856c0d8249198 --- /dev/null +++ b/Server/composer.json @@ -0,0 +1,86 @@ +{ + "type": "project", + "license": "proprietary", + "minimum-stability": "stable", + "prefer-stable": true, + "require": { + "php": ">=8.2", + "ext-ctype": "*", + "ext-iconv": "*", + "doctrine/doctrine-bundle": "^2.13", + "doctrine/doctrine-migrations-bundle": "^3.4", + "doctrine/orm": "^3.3", + "symfony/asset": "7.2.*", + "symfony/console": "7.2.*", + "symfony/dotenv": "7.2.*", + "symfony/expression-language": "7.2.*", + "symfony/flex": "^2", + "symfony/form": "7.2.*", + "symfony/framework-bundle": "7.2.*", + "symfony/mime": "7.2.*", + "symfony/monolog-bundle": "^3.10", + "symfony/routing": "7.2.*", + "symfony/runtime": "7.2.*", + "symfony/security-bundle": "7.2.*", + "symfony/twig-bundle": "7.2.*", + "symfony/uid": "7.2.*", + "symfony/validator": "7.2.*", + "symfony/yaml": "7.2.*" + }, + "require-dev": { + "doctrine/doctrine-fixtures-bundle": "^4.0", + "symfony/maker-bundle": "^1.62", + "symfony/stopwatch": "7.2.*", + "symfony/web-profiler-bundle": "7.2.*" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "bump-after-update": true, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*", + "symfony/polyfill-php82": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "7.2.*" + } + } +} diff --git a/Server/composer.lock b/Server/composer.lock new file mode 100644 index 0000000000000000000000000000000000000000..1889dff85f619d564674d78179d2ac0ae13f0921 --- /dev/null +++ b/Server/composer.lock @@ -0,0 +1,6471 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "a1a4de11b587f410a26453b67deb8aac", + "packages": [ + { + "name": "doctrine/cache", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2022-05-20T20:07:39+00:00" + }, + { + "name": "doctrine/collections", + "version": "2.2.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "d8af7f248c74f195f7347424600fd9e17b57af59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/d8af7f248c74f195f7347424600fd9e17b57af59", + "reference": "d8af7f248c74f195f7347424600fd9e17b57af59", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "ext-json": "*", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.2.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2024-04-18T06:56:21+00:00" + }, + { + "name": "doctrine/dbal", + "version": "4.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/33d2d7fe1269b2301640c44cf2896ea607b30e3e", + "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^0.5.3|^1", + "php": "^8.1", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "10.5.39", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^6.3.8|^7.0", + "symfony/console": "^5.4|^6.3|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-03-07T18:29:05+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.4" + }, + "time": "2024-12-07T21:18:45+00:00" + }, + { + "name": "doctrine/doctrine-bundle", + "version": "2.13.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "2363c43d9815a11657e452625cd64172d5587486" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/2363c43d9815a11657e452625cd64172d5587486", + "reference": "2363c43d9815a11657e452625cd64172d5587486", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/dbal": "^3.7.0 || ^4.0", + "doctrine/persistence": "^2.2 || ^3", + "doctrine/sql-formatter": "^1.0.1", + "php": "^7.4 || ^8.0", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/doctrine-bridge": "^5.4.46 || ~6.3.12 || ^6.4.3 || ^7.0.3", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1.1 || ^2.0 || ^3" + }, + "conflict": { + "doctrine/annotations": ">=3.0", + "doctrine/orm": "<2.17 || >=4.0", + "twig/twig": "<1.34 || >=2.0 <2.4" + }, + "require-dev": { + "doctrine/annotations": "^1 || ^2", + "doctrine/coding-standard": "^12", + "doctrine/deprecations": "^1.0", + "doctrine/orm": "^2.17 || ^3.0", + "friendsofphp/proxy-manager-lts": "^1.0", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9.5.26", + "psr/log": "^1.1.4 || ^2.0 || ^3.0", + "symfony/phpunit-bridge": "^6.1 || ^7.0", + "symfony/property-info": "^5.4 || ^6.0 || ^7.0", + "symfony/proxy-manager-bridge": "^5.4 || ^6.0", + "symfony/security-bundle": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", + "symfony/string": "^5.4 || ^6.0 || ^7.0", + "symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0", + "symfony/validator": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^5.4 || ^6.2 || ^7.0", + "symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0", + "twig/twig": "^1.34 || ^2.12 || ^3.0" + }, + "suggest": { + "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", + "ext-pdo": "*", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org/" + } + ], + "description": "Symfony DoctrineBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineBundle/issues", + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.13.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle", + "type": "tidelift" + } + ], + "time": "2025-01-15T11:12:38+00:00" + }, + { + "name": "doctrine/doctrine-migrations-bundle", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "e858ce0f5c12b266dce7dce24834448355155da7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/e858ce0f5c12b266dce7dce24834448355155da7", + "reference": "e858ce0f5c12b266dce7dce24834448355155da7", + "shasum": "" + }, + "require": { + "doctrine/doctrine-bundle": "^2.4", + "doctrine/migrations": "^3.2", + "php": "^7.2 || ^8.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/coding-standard": "^12", + "doctrine/orm": "^2.6 || ^3", + "doctrine/persistence": "^2.0 || ^3", + "phpstan/phpstan": "^1.4 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpstan/phpstan-symfony": "^1.3 || ^2", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/phpunit-bridge": "^6.3 || ^7", + "symfony/var-exporter": "^5.4 || ^6 || ^7" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\MigrationsBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineMigrationsBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "dbal", + "migrations", + "schema" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle", + "type": "tidelift" + } + ], + "time": "2025-01-27T22:48:22+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "doctrine/migrations", + "version": "3.8.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "5007eb1168691225ac305fe16856755c20860842" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/5007eb1168691225ac305fe16856755c20860842", + "reference": "5007eb1168691225ac305fe16856755c20860842", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/dbal": "^3.6 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2.0", + "php": "^8.1", + "psr/log": "^1.1.3 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.2 || ^7.0" + }, + "conflict": { + "doctrine/orm": "<2.12 || >=4" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "doctrine/orm": "^2.13 || ^3", + "doctrine/persistence": "^2 || ^3", + "doctrine/sql-formatter": "^1.0", + "ext-pdo_sqlite": "*", + "fig/log-test": "^1", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.4", + "phpstan/phpstan-symfony": "^1.3", + "phpunit/phpunit": "^10.3", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", + "symfony/yaml": "Allows the use of yaml for migration configuration files." + }, + "bin": [ + "bin/doctrine-migrations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Migrations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" + } + ], + "description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.", + "homepage": "https://www.doctrine-project.org/projects/migrations.html", + "keywords": [ + "database", + "dbal", + "migrations" + ], + "support": { + "issues": "https://github.com/doctrine/migrations/issues", + "source": "https://github.com/doctrine/migrations/tree/3.8.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fmigrations", + "type": "tidelift" + } + ], + "time": "2024-10-10T21:35:27+00:00" + }, + { + "name": "doctrine/orm", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "c9557c588b3a70ed93caff069d0aa75737f25609" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/c9557c588b3a70ed93caff069d0aa75737f25609", + "reference": "c9557c588b3a70ed93caff069d0aa75737f25609", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/collections": "^2.2", + "doctrine/dbal": "^3.8.2 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^3", + "doctrine/persistence": "^3.3.1 || ^4", + "ext-ctype": "*", + "php": "^8.1", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.3.9 || ^7.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0", + "phpbench/phpbench": "^1.0", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "2.0.3", + "phpstan/phpstan-deprecation-rules": "^2", + "phpunit/phpunit": "^10.4.0", + "psr/log": "^1 || ^2 || ^3", + "squizlabs/php_codesniffer": "3.7.2", + "symfony/cache": "^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/3.3.2" + }, + "time": "2025-02-04T19:43:15+00:00" + }, + { + "name": "doctrine/persistence", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/0ea965320cec355dba75031c1b23d4c78362e3ff", + "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1 || ^2", + "php": "^7.2 || ^8.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "doctrine/common": "<2.10" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "doctrine/common": "^3.0", + "phpstan/phpstan": "1.12.7", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5.38 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src/Persistence" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/3.4.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2024-10-30T19:48:12+00:00" + }, + { + "name": "doctrine/sql-formatter", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/sql-formatter.git", + "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "ergebnis/phpunit-slow-test-detector": "^2.14", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5" + }, + "bin": [ + "bin/sql-formatter" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\SqlFormatter\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Dorn", + "email": "jeremy@jeremydorn.com", + "homepage": "https://jeremydorn.com/" + } + ], + "description": "a PHP SQL highlighting library", + "homepage": "https://github.com/doctrine/sql-formatter/", + "keywords": [ + "highlight", + "sql" + ], + "support": { + "issues": "https://github.com/doctrine/sql-formatter/issues", + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.2" + }, + "time": "2025-01-24T11:45:48+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/asset", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "cb926cd59fefa1f9b4900b3695f0f846797ba5c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/cb926cd59fefa1f9b4900b3695f0f846797ba5c0", + "reference": "cb926cd59fefa1f9b4900b3695f0f846797ba5c0", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/http-foundation": "<6.4" + }, + "require-dev": { + "symfony/http-client": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:15:23+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "d33cd9e14326e14a4145c21e600602eaf17cc9e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/d33cd9e14326e14a4145c21e600602eaf17cc9e7", + "reference": "d33cd9e14326e14a4145c21e600602eaf17cc9e7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^2.5|^3", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/annotations": "^1 || ^2", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-26T09:57:54+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/config", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "7716594aaae91d9141be080240172a92ecca4d44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/7716594aaae91d9141be080240172a92ecca4d44", + "reference": "7716594aaae91d9141be080240172a92ecca4d44", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-22T12:07:01+00:00" + }, + { + "name": "symfony/console", + "version": "v7.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-11T03:49:26+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "f0a1614cccb4b8431a97076f9debc08ddca321ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f0a1614cccb4b8431a97076f9debc08ddca321ca", + "reference": "f0a1614cccb4b8431a97076f9debc08ddca321ca", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.5", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-21T09:47:16+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/doctrine-bridge", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "95bc5dde5202828bbc462bc06ba67cd244fa8a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/95bc5dde5202828bbc462bc06ba67cd244fa8a15", + "reference": "95bc5dde5202828bbc462bc06ba67cd244fa8a15", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^2", + "doctrine/persistence": "^3.1|^4", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/collections": "<1.8", + "doctrine/dbal": "<3.6", + "doctrine/lexer": "<1.1", + "doctrine/orm": "<2.15", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/form": "<6.4.6|>=7,<7.0.6", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/lock": "<6.4", + "symfony/messenger": "<6.4", + "symfony/property-info": "<6.4", + "symfony/security-bundle": "<6.4", + "symfony/security-core": "<6.4", + "symfony/validator": "<6.4" + }, + "require-dev": { + "doctrine/collections": "^1.8|^2.0", + "doctrine/data-fixtures": "^1.1|^2", + "doctrine/dbal": "^3.6|^4", + "doctrine/orm": "^2.15|^3", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/doctrine-messenger": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/form": "^6.4.6|^7.0.6", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/type-info": "^7.1", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Doctrine with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-bridge/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-18T16:43:05+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "28347a897771d0c28e99b75166dd2689099f3045" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/28347a897771d0c28e99b75166dd2689099f3045", + "reference": "28347a897771d0c28e99b75166dd2689099f3045", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/process": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-27T11:18:42+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "aabf79938aa795350c07ce6464dd1985607d95d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/aabf79938aa795350c07ce6464dd1985607d95d5", + "reference": "aabf79938aa795350c07ce6464dd1985607d95d5", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^6.4|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-02T20:27:07+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/expression-language", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/expression-language.git", + "reference": "26f4884a455e755e630a5fc372df124a3578da2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/26f4884a455e755e630a5fc372df124a3578da2e", + "reference": "26f4884a455e755e630a5fc372df124a3578da2e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/cache": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ExpressionLanguage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an engine that can compile and evaluate expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/expression-language/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-15T11:52:45+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:15:23+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-30T19:00:17+00:00" + }, + { + "name": "symfony/flex", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/8ce1acd9842abe0e9b4c4a0bd3f259859516c018", + "reference": "8ce1acd9842abe0e9b4c4a0bd3f259859516c018", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1", + "php": ">=8.0" + }, + "conflict": { + "composer/semver": "<1.7.2" + }, + "require-dev": { + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Flex\\Flex" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "support": { + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-03T07:50:46+00:00" + }, + { + "name": "symfony/form", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/form.git", + "reference": "7209804c018b88cc2b0beabe38eef91b83f1d69a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/form/zipball/7209804c018b88cc2b0beabe38eef91b83f1d69a", + "reference": "7209804c018b88cc2b0beabe38eef91b83f1d69a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/options-resolver": "^6.4|^7.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/error-handler": "<6.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "doctrine/collections": "^1.0|^2.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/html-sanitizer": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Form\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows to easily create, process and reuse HTML forms", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/form/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-07T22:04:27+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "6d6614378cd8128eed0a037ce6ac51a26c5aaed5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6d6614378cd8128eed0a037ce6ac51a26c5aaed5", + "reference": "6d6614378cd8128eed0a037ce6ac51a26c5aaed5", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^7.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/filesystem": "^7.1", + "symfony/finder": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^7.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/routing": "^6.4|^7.0" + }, + "conflict": { + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/asset": "<6.4", + "symfony/asset-mapper": "<6.4", + "symfony/clock": "<6.4", + "symfony/console": "<6.4", + "symfony/dom-crawler": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/lock": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", + "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", + "symfony/security-core": "<6.4", + "symfony/security-csrf": "<7.2", + "symfony/serializer": "<7.1", + "symfony/stopwatch": "<6.4", + "symfony/translation": "<6.4", + "symfony/twig-bridge": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4", + "symfony/web-profiler-bundle": "<6.4", + "symfony/webhook": "<7.2", + "symfony/workflow": "<6.4" + }, + "require-dev": { + "doctrine/persistence": "^1.3|^2|^3", + "dragonmantank/cron-expression": "^3.1", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "seld/jsonlint": "^1.10", + "symfony/asset": "^6.4|^7.0", + "symfony/asset-mapper": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/dotenv": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/html-sanitizer": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/mailer": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/notifier": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/scheduler": "^6.4.4|^7.0.4", + "symfony/security-bundle": "^6.4|^7.0", + "symfony/semaphore": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "symfony/type-info": "^7.1", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/web-link": "^6.4|^7.0", + "symfony/webhook": "^7.2", + "symfony/workflow": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-26T08:19:39+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee1b504b8926198be89d05e5b6fc4c3810c090f0", + "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-17T10:56:55+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "9f1103734c5789798fefb90e91de4586039003ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f1103734c5789798fefb90e91de4586039003ed", + "reference": "9f1103734c5789798fefb90e91de4586039003ed", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-26T11:01:22+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b", + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-19T08:51:20+00:00" + }, + { + "name": "symfony/monolog-bridge", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "bbae784f0456c5a87c89d7c1a3fcc9cbee976c1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/bbae784f0456c5a87c89d7c1a3fcc9cbee976c1d", + "reference": "bbae784f0456c5a87c89d7c1a3fcc9cbee976c1d", + "shasum": "" + }, + "require": { + "monolog/monolog": "^3", + "php": ">=8.2", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/mailer": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-14T18:16:08+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "php": ">=7.2.5", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T17:08:13+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-20T11:17:29+00:00" + }, + { + "name": "symfony/password-hasher", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "d8bd3d66d074c0acba1214a0d42f5941a8e1e94d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/d8bd3d66d074c0acba1214a0d42f5941a8e1e94d", + "reference": "d8bd3d66d074c0acba1214a0d42f5941a8e1e94d", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "d80a05e9904d2c2b9b95929f3e4b5d3a8f418d78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/d80a05e9904d2c2b9b95929f3e4b5d3a8f418d78", + "reference": "d80a05e9904d2c2b9b95929f3e4b5d3a8f418d78", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/property-access", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "b28732e315d81fbec787f838034de7d6c9b2b902" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/b28732e315d81fbec787f838034de7d6c9b2b902", + "reference": "b28732e315d81fbec787f838034de7d6c9b2b902", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/property-info": "^6.4|^7.0" + }, + "require-dev": { + "symfony/cache": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-17T10:56:55+00:00" + }, + { + "name": "symfony/property-info", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "dedb118fd588a92f226b390250b384d25f4192fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/dedb118fd588a92f226b390250b384d25f4192fe", + "reference": "dedb118fd588a92f226b390250b384d25f4192fe", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/string": "^6.4|^7.0", + "symfony/type-info": "~7.1.9|^7.2.2" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-27T11:08:17+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/ee9a67edc6baa33e5fae662f94f91fd262930996", + "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-17T10:56:55+00:00" + }, + { + "name": "symfony/runtime", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/runtime.git", + "reference": "8e8d09bd69b7f6c0260dd3d58f37bd4fbdeab5ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/runtime/zipball/8e8d09bd69b7f6c0260dd3d58f37bd4fbdeab5ad", + "reference": "8e8d09bd69b7f6c0260dd3d58f37bd4fbdeab5ad", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": ">=8.2" + }, + "conflict": { + "symfony/dotenv": "<6.4" + }, + "require-dev": { + "composer/composer": "^2.6", + "symfony/console": "^6.4|^7.0", + "symfony/dotenv": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Runtime\\": "", + "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Enables decoupling PHP applications from global state", + "homepage": "https://symfony.com", + "keywords": [ + "runtime" + ], + "support": { + "source": "https://github.com/symfony/runtime/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-29T21:39:47+00:00" + }, + { + "name": "symfony/security-bundle", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "721de227035c6e4c322fb7dd4839586d58bc0cf5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/721de227035c6e4c322fb7dd4839586d58bc0cf5", + "reference": "721de227035c6e4c322fb7dd4839586d58bc0cf5", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/password-hasher": "^6.4|^7.0", + "symfony/security-core": "^7.2", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/security-http": "^7.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/console": "<6.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-client": "<6.4", + "symfony/ldap": "<6.4", + "symfony/serializer": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/ldap": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.12", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SecurityBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-bundle/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-07T09:39:55+00:00" + }, + { + "name": "symfony/security-core", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "466784ffcd0b5a16e05394335897f790b17d07e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/466784ffcd0b5a16e05394335897f790b17d07e4", + "reference": "466784ffcd0b5a16e05394335897f790b17d07e4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/ldap": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<6.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/ldap": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-27T11:08:17+00:00" + }, + { + "name": "symfony/security-csrf", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-csrf.git", + "reference": "2b4b0c46c901729e4e90719eacd980381f53e0a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/2b4b0c46c901729e4e90719eacd980381f53e0a3", + "reference": "2b4b0c46c901729e4e90719eacd980381f53e0a3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/security-core": "^6.4|^7.0" + }, + "conflict": { + "symfony/http-foundation": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - CSRF Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-csrf/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T18:42:10+00:00" + }, + { + "name": "symfony/security-http", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "8478e95e273f8daa23bf4860dbad2a09d3fb3722" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/8478e95e273f8daa23bf4860dbad2a09d3fb3722", + "reference": "8478e95e273f8daa23bf4860dbad2a09d3fb3722", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/security-core": "^7.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/clock": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/http-client-contracts": "<3.0", + "symfony/security-bundle": "<6.4", + "symfony/security-csrf": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-11T16:46:20+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-24T10:49:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:26+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/twig-bridge", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "45c00afd4c9accf00a91215067c2858e5a9a3c4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/45c00afd4c9accf00a91215067c2858e5a9a3c4e", + "reference": "45c00afd4c9accf00a91215067c2858e5a9a3c4e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/translation-contracts": "^2.5|^3", + "twig/twig": "^3.12" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/console": "<6.4", + "symfony/form": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/mime": "<6.4", + "symfony/serializer": "<6.4", + "symfony/translation": "<6.4", + "symfony/workflow": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^6.4|^7.0", + "symfony/asset-mapper": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/emoji": "^7.1", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/html-sanitizer": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/security-http": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/web-link": "^6.4|^7.0", + "symfony/workflow": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Twig\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Twig with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bridge/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-14T14:27:24+00:00" + }, + { + "name": "symfony/twig-bundle", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "cd2be4563afaef5285bb6e0a06c5445e644a5c01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/cd2be4563afaef5285bb6e0a06c5445e644a5c01", + "reference": "cd2be4563afaef5285bb6e0a06c5445e644a5c01", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.2", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "conflict": { + "symfony/framework-bundle": "<6.4", + "symfony/translation": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/web-link": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\TwigBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of Twig into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bundle/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-23T08:11:15+00:00" + }, + { + "name": "symfony/type-info", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "269344575181c326781382ed53f7262feae3c6a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/269344575181c326781382ed53f7262feae3c6a4", + "reference": "269344575181c326781382ed53f7262feae3c6a4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.0|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-25T15:19:41+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "2d294d0c48df244c71c105a169d0190bfb080426" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426", + "reference": "2d294d0c48df244c71c105a169d0190bfb080426", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/validator", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "00936b34ef29d0e0e9a5340bbce6e7562092da56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/00936b34ef29d0e0e9a5340bbce6e7562092da56", + "reference": "00936b34ef29d0e0e9a5340bbce6e7562092da56", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<7.0", + "symfony/expression-language": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/intl": "<6.4", + "symfony/property-info": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/type-info": "^7.1", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-21T09:47:16+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "82b478c69745d8878eb60f9a049a4d584996f73a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a", + "reference": "82b478c69745d8878eb60f9a049a4d584996f73a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-17T11:39:41+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "4ede73aa7a73d81506002d2caadbbdad1ef5b69a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/4ede73aa7a73d81506002d2caadbbdad1ef5b69a", + "reference": "4ede73aa7a73d81506002d2caadbbdad1ef5b69a", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-13T10:27:23+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.2.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec", + "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-07T12:55:42+00:00" + }, + { + "name": "twig/twig", + "version": "v3.20.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "3468920399451a384bef53cf7996965f7cd40183" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183", + "reference": "3468920399451a384bef53cf7996965f7cd40183", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.20.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-02-13T08:34:43+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/data-fixtures", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "f7f1e12d6bceb58c204b3e77210a103c1c57601e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/f7f1e12d6bceb58c204b3e77210a103c1c57601e", + "reference": "f7f1e12d6bceb58c204b3e77210a103c1c57601e", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^3.1 || ^4.0", + "php": "^8.1", + "psr/log": "^1.1 || ^2 || ^3" + }, + "conflict": { + "doctrine/dbal": "<3.5 || >=5", + "doctrine/orm": "<2.14 || >=4", + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "doctrine/dbal": "^3.5 || ^4", + "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", + "doctrine/orm": "^2.14 || ^3", + "ext-sqlite3": "*", + "fig/log-test": "^1", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5.3", + "symfony/cache": "^6.4 || ^7", + "symfony/var-exporter": "^6.4 || ^7" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database" + ], + "support": { + "issues": "https://github.com/doctrine/data-fixtures/issues", + "source": "https://github.com/doctrine/data-fixtures/tree/2.0.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures", + "type": "tidelift" + } + ], + "time": "2025-01-21T13:21:31+00:00" + }, + { + "name": "doctrine/doctrine-fixtures-bundle", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", + "reference": "90185317e6bb3d845667c5ebd444d9c83ae19a01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/90185317e6bb3d845667c5ebd444d9c83ae19a01", + "reference": "90185317e6bb3d845667c5ebd444d9c83ae19a01", + "shasum": "" + }, + "require": { + "doctrine/data-fixtures": "^2.0", + "doctrine/doctrine-bundle": "^2.2", + "doctrine/orm": "^2.14.0 || ^3.0", + "doctrine/persistence": "^2.4 || ^3.0", + "php": "^8.1", + "psr/log": "^2 || ^3", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/doctrine-bridge": "^5.4.48 || ^6.4.16 || ^7.1.9", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0" + }, + "conflict": { + "doctrine/dbal": "< 3" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10.5.38 || ^11" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\FixturesBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineFixturesBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "Fixture", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/4.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-fixtures-bundle", + "type": "tidelift" + } + ], + "time": "2024-12-05T18:35:55+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "v1.62.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "468ff2708200c95ebc0d85d3174b6c6711b8a590" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/468ff2708200c95ebc0d85d3174b6c6711b8a590", + "reference": "468ff2708200c95ebc0d85d3174b6c6711b8a590", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^4.18|^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.62.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-15T00:21:40+00:00" + }, + { + "name": "symfony/process", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", + "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-05T08:33:46+00:00" + }, + { + "name": "symfony/web-profiler-bundle", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "4ffde1c860a100533b02697d9aaf5f45759ec26a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4ffde1c860a100533b02697d9aaf5f45759ec26a", + "reference": "4ffde1c860a100533b02697d9aaf5f45759ec26a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/config": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "conflict": { + "symfony/form": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/serializer": "<7.2" + }, + "require-dev": { + "symfony/browser-kit": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\WebProfilerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a development tool that gives detailed information about the execution of any request", + "homepage": "https://symfony.com", + "keywords": [ + "dev" + ], + "support": { + "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-14T14:27:24+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.2", + "ext-ctype": "*", + "ext-iconv": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/Server/config/.DS_Store b/Server/config/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..229755acaefcded874da7ad90bde504e59d6cb06 Binary files /dev/null and b/Server/config/.DS_Store differ diff --git a/Server/config/bundles.php b/Server/config/bundles.php new file mode 100644 index 0000000000000000000000000000000000000000..c6a5ed46f23a8b5804c3b9c35f5401dbf2934ecb --- /dev/null +++ b/Server/config/bundles.php @@ -0,0 +1,13 @@ +<?php + +return [ + Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], +]; diff --git a/Server/config/packages/cache.yaml b/Server/config/packages/cache.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6899b72003fca67f5a56b945cd3e07f5c8a33774 --- /dev/null +++ b/Server/config/packages/cache.yaml @@ -0,0 +1,19 @@ +framework: + cache: + # Unique name of your app: used to compute stable namespaces for cache keys. + #prefix_seed: your_vendor_name/app_name + + # The "app" cache stores to the filesystem by default. + # The data in this cache should persist between deploys. + # Other options include: + + # Redis + #app: cache.adapter.redis + #default_redis_provider: redis://localhost + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + #pools: + #my.dedicated.cache: null diff --git a/Server/config/packages/csrf.yaml b/Server/config/packages/csrf.yaml new file mode 100644 index 0000000000000000000000000000000000000000..40d40405e1e9651723b0169bac8f1a36489021ea --- /dev/null +++ b/Server/config/packages/csrf.yaml @@ -0,0 +1,11 @@ +# Enable stateless CSRF protection for forms and logins/logouts +framework: + form: + csrf_protection: + token_id: submit + + csrf_protection: + stateless_token_ids: + - submit + - authenticate + - logout diff --git a/Server/config/packages/doctrine.yaml b/Server/config/packages/doctrine.yaml new file mode 100644 index 0000000000000000000000000000000000000000..25138b979b685a90e7abcc8dbc563d5a5896683b --- /dev/null +++ b/Server/config/packages/doctrine.yaml @@ -0,0 +1,54 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + #server_version: '16' + + profiling_collect_backtrace: '%kernel.debug%' + use_savepoints: true + orm: + auto_generate_proxy_classes: true + enable_lazy_ghost_objects: true + report_fields_where_declared: true + validate_xml_mapping: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + identity_generation_preferences: + Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity + auto_mapping: true + mappings: + App: + type: attribute + is_bundle: false + dir: '%kernel.project_dir%/src/Entity' + prefix: 'App\Entity' + alias: App + controller_resolver: + auto_mapping: false + +when@test: + doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/Server/config/packages/doctrine_migrations.yaml b/Server/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000000000000000000000000000000000000..29231d94bd1afd6f35b3c1e64687640d3f1e2f50 --- /dev/null +++ b/Server/config/packages/doctrine_migrations.yaml @@ -0,0 +1,6 @@ +doctrine_migrations: + migrations_paths: + # namespace is arbitrary but should be different from App\Migrations + # as migrations classes should NOT be autoloaded + 'DoctrineMigrations': '%kernel.project_dir%/migrations' + enable_profiler: false diff --git a/Server/config/packages/framework.yaml b/Server/config/packages/framework.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ee8c275ac1419b7e8c20a4f82704c6e92ef19d8d --- /dev/null +++ b/Server/config/packages/framework.yaml @@ -0,0 +1,18 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + secret: "%env(APP_SECRET)%" + + # Note that the session will be started ONLY if you read or write from it. + session: true + + #esi: true + #fragments: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file + handler_id: null + cookie_secure: auto + cookie_samesite: lax diff --git a/Server/config/packages/monolog.yaml b/Server/config/packages/monolog.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0c3ad0d0eee26654d800f78197493fb0ee6c836e --- /dev/null +++ b/Server/config/packages/monolog.yaml @@ -0,0 +1,6 @@ +monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug diff --git a/Server/config/packages/routing.yaml b/Server/config/packages/routing.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8166181c68ab07bfac9aee6e482c7cc24031b8aa --- /dev/null +++ b/Server/config/packages/routing.yaml @@ -0,0 +1,10 @@ +framework: + router: + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands + #default_uri: http://localhost + +when@prod: + framework: + router: + strict_requirements: null diff --git a/Server/config/packages/security.yaml b/Server/config/packages/security.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2bf3f50144a7b7a08eeed455d08660e8dbb95690 --- /dev/null +++ b/Server/config/packages/security.yaml @@ -0,0 +1,46 @@ +security: + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: "auto" + # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: + # Remplacer users_in_memory par app_user_provider + app_user_provider: + entity: + class: App\Entity\User + property: email + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true + provider: app_user_provider # Utiliser votre nouveau provider + custom_authenticator: App\Controller\LoginFormAuthenticator + logout: + path: app_logout + target: app_home + enable_csrf: true + csrf_parameter: "_csrf_token" + csrf_token_id: "logout" + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + - { path: ^/admin, roles: ROLE_ADMIN } + - { path: ^/profile, roles: ROLE_USER } + - { path: ^/myWishlist/token, roles: PUBLIC_ACCESS } # Pour Symfony 5.2+ + - { path: ^/myWishlist, roles: ROLE_USER } + +when@test: + security: + password_hashers: + # By default, password hashers are resource intensive and take time. This is + # important to generate secure password hashes. In tests however, secure hashes + # are not important, waste resources and increase test times. The following + # reduces the work factor to the lowest possible values. + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon diff --git a/Server/config/packages/twig.yaml b/Server/config/packages/twig.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e5ecc5ea6407d0eaa0fb87e74624f74bed562702 --- /dev/null +++ b/Server/config/packages/twig.yaml @@ -0,0 +1,7 @@ +twig: + file_name_pattern: "*.twig" + +when@test: + twig: + strict_variables: true + exception_controller: null diff --git a/Server/config/packages/validator.yaml b/Server/config/packages/validator.yaml new file mode 100644 index 0000000000000000000000000000000000000000..dd47a6ad82a8ecc3adc7dd22262ebe27b4b10534 --- /dev/null +++ b/Server/config/packages/validator.yaml @@ -0,0 +1,11 @@ +framework: + validation: + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] + +when@test: + framework: + validation: + not_compromised_password: false diff --git a/Server/config/packages/web_profiler.yaml b/Server/config/packages/web_profiler.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b94611102de60c95b63ab6e990b241796da4ace2 --- /dev/null +++ b/Server/config/packages/web_profiler.yaml @@ -0,0 +1,17 @@ +when@dev: + web_profiler: + toolbar: true + intercept_redirects: false + + framework: + profiler: + only_exceptions: false + collect_serializer_data: true + +when@test: + web_profiler: + toolbar: false + intercept_redirects: false + + framework: + profiler: { collect: false } diff --git a/Server/config/preload.php b/Server/config/preload.php new file mode 100644 index 0000000000000000000000000000000000000000..5ebcdb2153c870e7d915f3b3fcdd55821424284c --- /dev/null +++ b/Server/config/preload.php @@ -0,0 +1,5 @@ +<?php + +if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) { + require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php'; +} diff --git a/Server/config/routes.yaml b/Server/config/routes.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2ca147f6d4e3e3d05a3d2dcd88e810cfcbb16e80 --- /dev/null +++ b/Server/config/routes.yaml @@ -0,0 +1,5 @@ +controllers: + resource: + path: ../src/Controller/ + namespace: App\Controller + type: attribute \ No newline at end of file diff --git a/Server/config/routes/annotations.yaml b/Server/config/routes/annotations.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/Server/config/routes/framework.yaml b/Server/config/routes/framework.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0fc74bbac4b757dd49782f0be496f6fd7e07e47c --- /dev/null +++ b/Server/config/routes/framework.yaml @@ -0,0 +1,4 @@ +when@dev: + _errors: + resource: '@FrameworkBundle/Resources/config/routing/errors.xml' + prefix: /_error diff --git a/Server/config/routes/security.yaml b/Server/config/routes/security.yaml new file mode 100644 index 0000000000000000000000000000000000000000..54a4908cdb38ab71d8111b104006ea04e20a323a --- /dev/null +++ b/Server/config/routes/security.yaml @@ -0,0 +1,4 @@ +_security_logout: + resource: security.route_loader.logout + type: service + diff --git a/Server/config/routes/web_profiler.yaml b/Server/config/routes/web_profiler.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8d85319fd8654424c2c9602a3f813415d58b6dae --- /dev/null +++ b/Server/config/routes/web_profiler.yaml @@ -0,0 +1,8 @@ +when@dev: + web_profiler_wdt: + resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' + prefix: /_wdt + + web_profiler_profiler: + resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' + prefix: /_profiler diff --git a/Server/config/services.yaml b/Server/config/services.yaml new file mode 100644 index 0000000000000000000000000000000000000000..df9078696f1fc1c59cfb02558ff4d56bffd2f648 --- /dev/null +++ b/Server/config/services.yaml @@ -0,0 +1,29 @@ +# This file is the entry point to configure your own services. +# Files in the packages/ subdirectory configure your dependencies. + +# Put parameters here that don't need to change on each machine where the app is deployed +# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration +parameters: + profile_photos_directory: "%kernel.project_dir%/public/images/profile" + proofs_directory: '%kernel.project_dir%/public/images/proofs' + +services: + App\Exceptions\AccessDeniedListener: + tags: + - { name: kernel.event_listener, event: kernel.exception } + # default configuration for services in *this* file + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: "../src/" + exclude: + - "../src/DependencyInjection/" + - "../src/Entity/" + - "../src/Kernel.php" + + # add more service definitions when explicit configuration is needed + # please note that last definitions always *replace* previous ones diff --git a/Server/docker-compose b/Server/docker-compose new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/Server/migrations/.gitignore b/Server/migrations/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/Server/migrations/Version20250315151445.php b/Server/migrations/Version20250315151445.php new file mode 100644 index 0000000000000000000000000000000000000000..9400a7b14a5ea0e1474105191cc9a045c74554df --- /dev/null +++ b/Server/migrations/Version20250315151445.php @@ -0,0 +1,50 @@ +<?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 Version20250315151445 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 TABLE item (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, price DOUBLE PRECISION NOT NULL, has_purchased TINYINT(1) NOT NULL, url VARCHAR(255) DEFAULT NULL, wishlist_id INT NOT NULL, INDEX IDX_1F1B251EFB8E54CD (wishlist_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8'); + $this->addSql('CREATE TABLE proof (id INT AUTO_INCREMENT NOT NULL, congrats_message VARCHAR(255) NOT NULL, proof VARCHAR(255) DEFAULT NULL, buyer_id INT DEFAULT NULL, INDEX IDX_FBF940DD6C755722 (buyer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8'); + $this->addSql('CREATE TABLE `user` (id INT AUTO_INCREMENT NOT NULL, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, username VARCHAR(50) NOT NULL, email VARCHAR(180) NOT NULL, password VARCHAR(255) NOT NULL, photo VARCHAR(255) DEFAULT NULL, lock_status TINYINT(1) NOT NULL, type VARCHAR(20) NOT NULL, UNIQUE INDEX UNIQ_8D93D649F85E0677 (username), UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8'); + $this->addSql('CREATE TABLE wishlist (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, deadline DATE DEFAULT NULL, owner_id INT NOT NULL, INDEX IDX_9CE12A317E3C61F9 (owner_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8'); + $this->addSql('CREATE TABLE wishlist_user (wishlist_id INT NOT NULL, user_id INT NOT NULL, INDEX IDX_F6AC07BFFB8E54CD (wishlist_id), INDEX IDX_F6AC07BFA76ED395 (user_id), PRIMARY KEY(wishlist_id, user_id)) DEFAULT CHARACTER SET utf8'); + $this->addSql('ALTER TABLE item ADD CONSTRAINT FK_1F1B251EFB8E54CD FOREIGN KEY (wishlist_id) REFERENCES wishlist (id)'); + $this->addSql('ALTER TABLE proof ADD CONSTRAINT FK_FBF940DD6C755722 FOREIGN KEY (buyer_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE wishlist ADD CONSTRAINT FK_9CE12A317E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE wishlist_user ADD CONSTRAINT FK_F6AC07BFFB8E54CD FOREIGN KEY (wishlist_id) REFERENCES wishlist (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE wishlist_user ADD CONSTRAINT FK_F6AC07BFA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE item DROP FOREIGN KEY FK_1F1B251EFB8E54CD'); + $this->addSql('ALTER TABLE proof DROP FOREIGN KEY FK_FBF940DD6C755722'); + $this->addSql('ALTER TABLE wishlist DROP FOREIGN KEY FK_9CE12A317E3C61F9'); + $this->addSql('ALTER TABLE wishlist_user DROP FOREIGN KEY FK_F6AC07BFFB8E54CD'); + $this->addSql('ALTER TABLE wishlist_user DROP FOREIGN KEY FK_F6AC07BFA76ED395'); + $this->addSql('DROP TABLE item'); + $this->addSql('DROP TABLE proof'); + $this->addSql('DROP TABLE `user`'); + $this->addSql('DROP TABLE wishlist'); + $this->addSql('DROP TABLE wishlist_user'); + } +} diff --git a/Server/migrations/Version20250319110817.php b/Server/migrations/Version20250319110817.php new file mode 100644 index 0000000000000000000000000000000000000000..7c00f405564d9a8ab5f7247eb306d15ca1e22489 --- /dev/null +++ b/Server/migrations/Version20250319110817.php @@ -0,0 +1,49 @@ +<?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 Version20250319110817 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 TABLE item (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, price DOUBLE PRECISION NOT NULL, has_purchased TINYINT(1) NOT NULL, url VARCHAR(255) DEFAULT NULL, wishlist_id INT NOT NULL, INDEX IDX_1F1B251EFB8E54CD (wishlist_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE proof (id INT AUTO_INCREMENT NOT NULL, congrats_message VARCHAR(255) NOT NULL, proof VARCHAR(255) DEFAULT NULL, buyer_id INT DEFAULT NULL, INDEX IDX_FBF940DD6C755722 (buyer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE `user` (id INT AUTO_INCREMENT NOT NULL, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, username VARCHAR(50) NOT NULL, email VARCHAR(180) NOT NULL, password VARCHAR(255) NOT NULL, photo VARCHAR(255) DEFAULT NULL, lock_status TINYINT(1) NOT NULL, type VARCHAR(20) NOT NULL, UNIQUE INDEX UNIQ_8D93D649F85E0677 (username), UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE wishlist (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, deadline DATE DEFAULT NULL, owner_id INT NOT NULL, INDEX IDX_9CE12A317E3C61F9 (owner_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE wishlist_user (wishlist_id INT NOT NULL, user_id INT NOT NULL, INDEX IDX_F6AC07BFFB8E54CD (wishlist_id), INDEX IDX_F6AC07BFA76ED395 (user_id), PRIMARY KEY(wishlist_id, user_id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE item ADD CONSTRAINT FK_1F1B251EFB8E54CD FOREIGN KEY (wishlist_id) REFERENCES wishlist (id)'); + $this->addSql('ALTER TABLE proof ADD CONSTRAINT FK_FBF940DD6C755722 FOREIGN KEY (buyer_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE wishlist ADD CONSTRAINT FK_9CE12A317E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE wishlist_user ADD CONSTRAINT FK_F6AC07BFFB8E54CD FOREIGN KEY (wishlist_id) REFERENCES wishlist (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE wishlist_user ADD CONSTRAINT FK_F6AC07BFA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE item DROP FOREIGN KEY FK_1F1B251EFB8E54CD'); + $this->addSql('ALTER TABLE proof DROP FOREIGN KEY FK_FBF940DD6C755722'); + $this->addSql('ALTER TABLE wishlist DROP FOREIGN KEY FK_9CE12A317E3C61F9'); + $this->addSql('ALTER TABLE wishlist_user DROP FOREIGN KEY FK_F6AC07BFFB8E54CD'); + $this->addSql('ALTER TABLE wishlist_user DROP FOREIGN KEY FK_F6AC07BFA76ED395'); + $this->addSql('DROP TABLE item'); + $this->addSql('DROP TABLE proof'); + $this->addSql('DROP TABLE `user`'); + $this->addSql('DROP TABLE wishlist'); + $this->addSql('DROP TABLE wishlist_user'); + } +} diff --git a/Server/migrations/Version20250319114015.php b/Server/migrations/Version20250319114015.php new file mode 100644 index 0000000000000000000000000000000000000000..155dc64cd8c1aaf5767f52820bb28724a9a290a4 --- /dev/null +++ b/Server/migrations/Version20250319114015.php @@ -0,0 +1,49 @@ +<?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 Version20250319114015 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 TABLE item (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, price DOUBLE PRECISION NOT NULL, has_purchased TINYINT(1) NOT NULL, url VARCHAR(255) DEFAULT NULL, wishlist_id INT NOT NULL, INDEX IDX_1F1B251EFB8E54CD (wishlist_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE proof (id INT AUTO_INCREMENT NOT NULL, congrats_message VARCHAR(255) NOT NULL, proof VARCHAR(255) DEFAULT NULL, buyer_id INT DEFAULT NULL, INDEX IDX_FBF940DD6C755722 (buyer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE `user` (id INT AUTO_INCREMENT NOT NULL, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, username VARCHAR(50) NOT NULL, email VARCHAR(180) NOT NULL, password VARCHAR(255) NOT NULL, photo VARCHAR(255) DEFAULT NULL, lock_status TINYINT(1) NOT NULL, type VARCHAR(20) NOT NULL, UNIQUE INDEX UNIQ_8D93D649F85E0677 (username), UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE wishlist (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, deadline DATE DEFAULT NULL, owner_id INT NOT NULL, INDEX IDX_9CE12A317E3C61F9 (owner_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE wishlist_user (wishlist_id INT NOT NULL, user_id INT NOT NULL, INDEX IDX_F6AC07BFFB8E54CD (wishlist_id), INDEX IDX_F6AC07BFA76ED395 (user_id), PRIMARY KEY(wishlist_id, user_id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE item ADD CONSTRAINT FK_1F1B251EFB8E54CD FOREIGN KEY (wishlist_id) REFERENCES wishlist (id)'); + $this->addSql('ALTER TABLE proof ADD CONSTRAINT FK_FBF940DD6C755722 FOREIGN KEY (buyer_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE wishlist ADD CONSTRAINT FK_9CE12A317E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE wishlist_user ADD CONSTRAINT FK_F6AC07BFFB8E54CD FOREIGN KEY (wishlist_id) REFERENCES wishlist (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE wishlist_user ADD CONSTRAINT FK_F6AC07BFA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE item DROP FOREIGN KEY FK_1F1B251EFB8E54CD'); + $this->addSql('ALTER TABLE proof DROP FOREIGN KEY FK_FBF940DD6C755722'); + $this->addSql('ALTER TABLE wishlist DROP FOREIGN KEY FK_9CE12A317E3C61F9'); + $this->addSql('ALTER TABLE wishlist_user DROP FOREIGN KEY FK_F6AC07BFFB8E54CD'); + $this->addSql('ALTER TABLE wishlist_user DROP FOREIGN KEY FK_F6AC07BFA76ED395'); + $this->addSql('DROP TABLE item'); + $this->addSql('DROP TABLE proof'); + $this->addSql('DROP TABLE `user`'); + $this->addSql('DROP TABLE wishlist'); + $this->addSql('DROP TABLE wishlist_user'); + } +} diff --git a/Server/migrations/Version20250325164020.php b/Server/migrations/Version20250325164020.php new file mode 100644 index 0000000000000000000000000000000000000000..b2e86eec762a9de8d842e2724b53f5fe62a96709 --- /dev/null +++ b/Server/migrations/Version20250325164020.php @@ -0,0 +1,53 @@ +<?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 Version20250325164020 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 TABLE `invitation` (id INT AUTO_INCREMENT NOT NULL, receivers_id INT DEFAULT NULL, INDEX IDX_F11D61A2C5BEF871 (receivers_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE notification (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL, sender_id INT DEFAULT NULL, recipient_id INT DEFAULT NULL, invitation_id INT DEFAULT NULL, item_id INT DEFAULT NULL, INDEX IDX_BF5476CAF624B39D (sender_id), INDEX IDX_BF5476CAE92F8F78 (recipient_id), INDEX IDX_BF5476CAA35D7AF0 (invitation_id), INDEX IDX_BF5476CA126F525E (item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE test (id INT AUTO_INCREMENT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2C5BEF871 FOREIGN KEY (receivers_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAF624B39D FOREIGN KEY (sender_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAE92F8F78 FOREIGN KEY (recipient_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAA35D7AF0 FOREIGN KEY (invitation_id) REFERENCES `invitation` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); + $this->addSql('ALTER TABLE user DROP collaboration_token, DROP public_token'); + $this->addSql('ALTER TABLE wishlist ADD collaboration_token VARCHAR(36) DEFAULT NULL, ADD public_token VARCHAR(36) DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A3124CB2F13 ON wishlist (collaboration_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A31AE981E3B ON wishlist (public_token)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2C5BEF871'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAF624B39D'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAE92F8F78'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAA35D7AF0'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CA126F525E'); + $this->addSql('DROP TABLE `invitation`'); + $this->addSql('DROP TABLE notification'); + $this->addSql('DROP TABLE test'); + $this->addSql('DROP INDEX UNIQ_9CE12A3124CB2F13 ON wishlist'); + $this->addSql('DROP INDEX UNIQ_9CE12A31AE981E3B ON wishlist'); + $this->addSql('ALTER TABLE wishlist DROP collaboration_token, DROP public_token'); + $this->addSql('ALTER TABLE `user` ADD collaboration_token VARCHAR(255) DEFAULT NULL, ADD public_token VARCHAR(255) DEFAULT NULL'); + } +} diff --git a/Server/migrations/Version20250325170618.php b/Server/migrations/Version20250325170618.php new file mode 100644 index 0000000000000000000000000000000000000000..d57d787bba2ae375637f950590feaae3dacbca1d --- /dev/null +++ b/Server/migrations/Version20250325170618.php @@ -0,0 +1,53 @@ +<?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 Version20250325170618 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 TABLE `invitation` (id INT AUTO_INCREMENT NOT NULL, receivers_id INT DEFAULT NULL, INDEX IDX_F11D61A2C5BEF871 (receivers_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE notification (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL, sender_id INT DEFAULT NULL, recipient_id INT DEFAULT NULL, invitation_id INT DEFAULT NULL, item_id INT DEFAULT NULL, INDEX IDX_BF5476CAF624B39D (sender_id), INDEX IDX_BF5476CAE92F8F78 (recipient_id), INDEX IDX_BF5476CAA35D7AF0 (invitation_id), INDEX IDX_BF5476CA126F525E (item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE test (id INT AUTO_INCREMENT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2C5BEF871 FOREIGN KEY (receivers_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAF624B39D FOREIGN KEY (sender_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAE92F8F78 FOREIGN KEY (recipient_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAA35D7AF0 FOREIGN KEY (invitation_id) REFERENCES `invitation` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); + $this->addSql('ALTER TABLE user DROP collaboration_token, DROP public_token'); + $this->addSql('ALTER TABLE wishlist CHANGE collaboration_token collaboration_token VARCHAR(36) DEFAULT NULL, CHANGE public_token public_token VARCHAR(36) DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A3124CB2F13 ON wishlist (collaboration_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A31AE981E3B ON wishlist (public_token)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2C5BEF871'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAF624B39D'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAE92F8F78'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAA35D7AF0'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CA126F525E'); + $this->addSql('DROP TABLE `invitation`'); + $this->addSql('DROP TABLE notification'); + $this->addSql('DROP TABLE test'); + $this->addSql('DROP INDEX UNIQ_9CE12A3124CB2F13 ON wishlist'); + $this->addSql('DROP INDEX UNIQ_9CE12A31AE981E3B ON wishlist'); + $this->addSql('ALTER TABLE wishlist CHANGE collaboration_token collaboration_token VARCHAR(255) DEFAULT NULL, CHANGE public_token public_token VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE `user` ADD collaboration_token VARCHAR(255) DEFAULT NULL, ADD public_token VARCHAR(255) DEFAULT NULL'); + } +} diff --git a/Server/migrations/Version20250326005907.php b/Server/migrations/Version20250326005907.php new file mode 100644 index 0000000000000000000000000000000000000000..c7a99b750badb303aabd465d6011d5e485096d65 --- /dev/null +++ b/Server/migrations/Version20250326005907.php @@ -0,0 +1,59 @@ +<?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 Version20250326005907 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 TABLE `invitation` (id INT AUTO_INCREMENT NOT NULL, receiver_id INT DEFAULT NULL, INDEX IDX_F11D61A2CD53EDB6 (receiver_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE notification (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL, sender_id INT DEFAULT NULL, recipient_id INT DEFAULT NULL, invitation_id INT DEFAULT NULL, item_id INT DEFAULT NULL, INDEX IDX_BF5476CAF624B39D (sender_id), INDEX IDX_BF5476CAE92F8F78 (recipient_id), INDEX IDX_BF5476CAA35D7AF0 (invitation_id), INDEX IDX_BF5476CA126F525E (item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE test (id INT AUTO_INCREMENT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2CD53EDB6 FOREIGN KEY (receiver_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAF624B39D FOREIGN KEY (sender_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAE92F8F78 FOREIGN KEY (recipient_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAA35D7AF0 FOREIGN KEY (invitation_id) REFERENCES `invitation` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); + $this->addSql('ALTER TABLE proof ADD created_at DATETIME NOT NULL, ADD item_id INT NOT NULL, CHANGE proof proof_image_path VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE proof ADD CONSTRAINT FK_FBF940DD126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); + $this->addSql('CREATE INDEX IDX_FBF940DD126F525E ON proof (item_id)'); + $this->addSql('ALTER TABLE user DROP collaboration_token, DROP public_token'); + $this->addSql('ALTER TABLE wishlist CHANGE collaboration_token collaboration_token VARCHAR(36) DEFAULT NULL, CHANGE public_token public_token VARCHAR(36) DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A3124CB2F13 ON wishlist (collaboration_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A31AE981E3B ON wishlist (public_token)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2CD53EDB6'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAF624B39D'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAE92F8F78'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAA35D7AF0'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CA126F525E'); + $this->addSql('DROP TABLE `invitation`'); + $this->addSql('DROP TABLE notification'); + $this->addSql('DROP TABLE test'); + $this->addSql('DROP INDEX UNIQ_9CE12A3124CB2F13 ON wishlist'); + $this->addSql('DROP INDEX UNIQ_9CE12A31AE981E3B ON wishlist'); + $this->addSql('ALTER TABLE wishlist CHANGE collaboration_token collaboration_token VARCHAR(255) DEFAULT NULL, CHANGE public_token public_token VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE `user` ADD collaboration_token VARCHAR(255) DEFAULT NULL, ADD public_token VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE proof DROP FOREIGN KEY FK_FBF940DD126F525E'); + $this->addSql('DROP INDEX IDX_FBF940DD126F525E ON proof'); + $this->addSql('ALTER TABLE proof DROP created_at, DROP item_id, CHANGE proof_image_path proof VARCHAR(255) DEFAULT NULL'); + } +} diff --git a/Server/migrations/Version20250326011042.php b/Server/migrations/Version20250326011042.php new file mode 100644 index 0000000000000000000000000000000000000000..5b377a15137cd8d098e0040a5ab72e6df9c04023 --- /dev/null +++ b/Server/migrations/Version20250326011042.php @@ -0,0 +1,59 @@ +<?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 Version20250326011042 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 TABLE `invitation` (id INT AUTO_INCREMENT NOT NULL, receiver_id INT DEFAULT NULL, INDEX IDX_F11D61A2CD53EDB6 (receiver_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE notification (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL, sender_id INT DEFAULT NULL, recipient_id INT DEFAULT NULL, invitation_id INT DEFAULT NULL, item_id INT DEFAULT NULL, INDEX IDX_BF5476CAF624B39D (sender_id), INDEX IDX_BF5476CAE92F8F78 (recipient_id), INDEX IDX_BF5476CAA35D7AF0 (invitation_id), INDEX IDX_BF5476CA126F525E (item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE test (id INT AUTO_INCREMENT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2CD53EDB6 FOREIGN KEY (receiver_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAF624B39D FOREIGN KEY (sender_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAE92F8F78 FOREIGN KEY (recipient_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAA35D7AF0 FOREIGN KEY (invitation_id) REFERENCES `invitation` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); + $this->addSql('ALTER TABLE proof ADD created_at DATETIME NOT NULL, ADD item_id INT NOT NULL, CHANGE proof proof_image_path VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE proof ADD CONSTRAINT FK_FBF940DD126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); + $this->addSql('CREATE INDEX IDX_FBF940DD126F525E ON proof (item_id)'); + $this->addSql('ALTER TABLE user DROP collaboration_token, DROP public_token'); + $this->addSql('ALTER TABLE wishlist CHANGE collaboration_token collaboration_token VARCHAR(36) DEFAULT NULL, CHANGE public_token public_token VARCHAR(36) DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A3124CB2F13 ON wishlist (collaboration_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A31AE981E3B ON wishlist (public_token)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2CD53EDB6'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAF624B39D'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAE92F8F78'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAA35D7AF0'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CA126F525E'); + $this->addSql('DROP TABLE `invitation`'); + $this->addSql('DROP TABLE notification'); + $this->addSql('DROP TABLE test'); + $this->addSql('DROP INDEX UNIQ_9CE12A3124CB2F13 ON wishlist'); + $this->addSql('DROP INDEX UNIQ_9CE12A31AE981E3B ON wishlist'); + $this->addSql('ALTER TABLE wishlist CHANGE collaboration_token collaboration_token VARCHAR(255) DEFAULT NULL, CHANGE public_token public_token VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE `user` ADD collaboration_token VARCHAR(255) DEFAULT NULL, ADD public_token VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE proof DROP FOREIGN KEY FK_FBF940DD126F525E'); + $this->addSql('DROP INDEX IDX_FBF940DD126F525E ON proof'); + $this->addSql('ALTER TABLE proof DROP created_at, DROP item_id, CHANGE proof_image_path proof VARCHAR(255) DEFAULT NULL'); + } +} diff --git a/Server/migrations/Version20250326011218.php b/Server/migrations/Version20250326011218.php new file mode 100644 index 0000000000000000000000000000000000000000..ae3b7d30ba27f76ae095068ae988529b49fd7a2b --- /dev/null +++ b/Server/migrations/Version20250326011218.php @@ -0,0 +1,67 @@ +<?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 Version20250326011218 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 TABLE `invitation` (id INT AUTO_INCREMENT NOT NULL, receiver_id INT DEFAULT NULL, INDEX IDX_F11D61A2CD53EDB6 (receiver_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE item (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, price DOUBLE PRECISION NOT NULL, has_purchased TINYINT(1) NOT NULL, url VARCHAR(255) DEFAULT NULL, wishlist_id INT NOT NULL, INDEX IDX_1F1B251EFB8E54CD (wishlist_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE notification (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL, sender_id INT DEFAULT NULL, recipient_id INT DEFAULT NULL, invitation_id INT DEFAULT NULL, item_id INT DEFAULT NULL, INDEX IDX_BF5476CAF624B39D (sender_id), INDEX IDX_BF5476CAE92F8F78 (recipient_id), INDEX IDX_BF5476CAA35D7AF0 (invitation_id), INDEX IDX_BF5476CA126F525E (item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE proof (id INT AUTO_INCREMENT NOT NULL, congrats_message VARCHAR(255) NOT NULL, proof_image_path VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL, buyer_id INT DEFAULT NULL, item_id INT NOT NULL, INDEX IDX_FBF940DD6C755722 (buyer_id), INDEX IDX_FBF940DD126F525E (item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE test (id INT AUTO_INCREMENT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE `user` (id INT AUTO_INCREMENT NOT NULL, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, username VARCHAR(50) NOT NULL, email VARCHAR(180) NOT NULL, password VARCHAR(255) NOT NULL, photo VARCHAR(255) DEFAULT NULL, lock_status TINYINT(1) NOT NULL, type VARCHAR(20) NOT NULL, UNIQUE INDEX UNIQ_8D93D649F85E0677 (username), UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE wishlist (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, deadline DATE DEFAULT NULL, collaboration_token VARCHAR(36) DEFAULT NULL, public_token VARCHAR(36) DEFAULT NULL, owner_id INT NOT NULL, UNIQUE INDEX UNIQ_9CE12A3124CB2F13 (collaboration_token), UNIQUE INDEX UNIQ_9CE12A31AE981E3B (public_token), INDEX IDX_9CE12A317E3C61F9 (owner_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE wishlist_user (wishlist_id INT NOT NULL, user_id INT NOT NULL, INDEX IDX_F6AC07BFFB8E54CD (wishlist_id), INDEX IDX_F6AC07BFA76ED395 (user_id), PRIMARY KEY(wishlist_id, user_id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2CD53EDB6 FOREIGN KEY (receiver_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE item ADD CONSTRAINT FK_1F1B251EFB8E54CD FOREIGN KEY (wishlist_id) REFERENCES wishlist (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAF624B39D FOREIGN KEY (sender_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAE92F8F78 FOREIGN KEY (recipient_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAA35D7AF0 FOREIGN KEY (invitation_id) REFERENCES `invitation` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); + $this->addSql('ALTER TABLE proof ADD CONSTRAINT FK_FBF940DD6C755722 FOREIGN KEY (buyer_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE proof ADD CONSTRAINT FK_FBF940DD126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); + $this->addSql('ALTER TABLE wishlist ADD CONSTRAINT FK_9CE12A317E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE wishlist_user ADD CONSTRAINT FK_F6AC07BFFB8E54CD FOREIGN KEY (wishlist_id) REFERENCES wishlist (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE wishlist_user ADD CONSTRAINT FK_F6AC07BFA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2CD53EDB6'); + $this->addSql('ALTER TABLE item DROP FOREIGN KEY FK_1F1B251EFB8E54CD'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAF624B39D'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAE92F8F78'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAA35D7AF0'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CA126F525E'); + $this->addSql('ALTER TABLE proof DROP FOREIGN KEY FK_FBF940DD6C755722'); + $this->addSql('ALTER TABLE proof DROP FOREIGN KEY FK_FBF940DD126F525E'); + $this->addSql('ALTER TABLE wishlist DROP FOREIGN KEY FK_9CE12A317E3C61F9'); + $this->addSql('ALTER TABLE wishlist_user DROP FOREIGN KEY FK_F6AC07BFFB8E54CD'); + $this->addSql('ALTER TABLE wishlist_user DROP FOREIGN KEY FK_F6AC07BFA76ED395'); + $this->addSql('DROP TABLE `invitation`'); + $this->addSql('DROP TABLE item'); + $this->addSql('DROP TABLE notification'); + $this->addSql('DROP TABLE proof'); + $this->addSql('DROP TABLE test'); + $this->addSql('DROP TABLE `user`'); + $this->addSql('DROP TABLE wishlist'); + $this->addSql('DROP TABLE wishlist_user'); + } +} diff --git a/Server/migrations/Version20250331063151.php b/Server/migrations/Version20250331063151.php new file mode 100644 index 0000000000000000000000000000000000000000..4100b90d11fbf52545ce78a3b06036de72668711 --- /dev/null +++ b/Server/migrations/Version20250331063151.php @@ -0,0 +1,61 @@ +<?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 Version20250331063151 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 TABLE `invitation` (id INT AUTO_INCREMENT NOT NULL, accepted TINYINT(1) DEFAULT NULL, wishlist_id INT NOT NULL, sender_id INT NOT NULL, receiver_id INT DEFAULT NULL, INDEX IDX_F11D61A2FB8E54CD (wishlist_id), INDEX IDX_F11D61A2F624B39D (sender_id), INDEX IDX_F11D61A2CD53EDB6 (receiver_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE notification (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL, sender_id INT DEFAULT NULL, recipient_id INT DEFAULT NULL, invitation_id INT DEFAULT NULL, item_id INT DEFAULT NULL, INDEX IDX_BF5476CAF624B39D (sender_id), INDEX IDX_BF5476CAE92F8F78 (recipient_id), INDEX IDX_BF5476CAA35D7AF0 (invitation_id), INDEX IDX_BF5476CA126F525E (item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE test (id INT AUTO_INCREMENT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2FB8E54CD FOREIGN KEY (wishlist_id) REFERENCES wishlist (id)'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2F624B39D FOREIGN KEY (sender_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2CD53EDB6 FOREIGN KEY (receiver_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAF624B39D FOREIGN KEY (sender_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAE92F8F78 FOREIGN KEY (recipient_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAA35D7AF0 FOREIGN KEY (invitation_id) REFERENCES `invitation` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); + $this->addSql('ALTER TABLE proof ADD created_at DATETIME NOT NULL, ADD item_id INT NOT NULL, CHANGE proof proof_image_path VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE proof ADD CONSTRAINT FK_FBF940DD126F525E FOREIGN KEY (item_id) REFERENCES item (id) ON DELETE CASCADE'); + $this->addSql('CREATE INDEX IDX_FBF940DD126F525E ON proof (item_id)'); + $this->addSql('ALTER TABLE wishlist CHANGE collaboration_token collaboration_token VARCHAR(36) DEFAULT NULL, CHANGE public_token public_token VARCHAR(36) DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A3124CB2F13 ON wishlist (collaboration_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A31AE981E3B ON wishlist (public_token)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2FB8E54CD'); + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2F624B39D'); + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2CD53EDB6'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAF624B39D'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAE92F8F78'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAA35D7AF0'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CA126F525E'); + $this->addSql('DROP TABLE `invitation`'); + $this->addSql('DROP TABLE notification'); + $this->addSql('DROP TABLE test'); + $this->addSql('DROP INDEX UNIQ_9CE12A3124CB2F13 ON wishlist'); + $this->addSql('DROP INDEX UNIQ_9CE12A31AE981E3B ON wishlist'); + $this->addSql('ALTER TABLE wishlist CHANGE collaboration_token collaboration_token VARCHAR(1024) DEFAULT NULL, CHANGE public_token public_token VARCHAR(1024) DEFAULT NULL'); + $this->addSql('ALTER TABLE proof DROP FOREIGN KEY FK_FBF940DD126F525E'); + $this->addSql('DROP INDEX IDX_FBF940DD126F525E ON proof'); + $this->addSql('ALTER TABLE proof DROP created_at, DROP item_id, CHANGE proof_image_path proof VARCHAR(255) DEFAULT NULL'); + } +} diff --git a/Server/migrations/Version20250331065720.php b/Server/migrations/Version20250331065720.php new file mode 100644 index 0000000000000000000000000000000000000000..429cdd5d9c5966e9c74bb80884c7cf373159096f --- /dev/null +++ b/Server/migrations/Version20250331065720.php @@ -0,0 +1,61 @@ +<?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 Version20250331065720 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 TABLE `invitation` (id INT AUTO_INCREMENT NOT NULL, accepted TINYINT(1) DEFAULT NULL, wishlist_id INT NOT NULL, sender_id INT NOT NULL, receiver_id INT DEFAULT NULL, INDEX IDX_F11D61A2FB8E54CD (wishlist_id), INDEX IDX_F11D61A2F624B39D (sender_id), INDEX IDX_F11D61A2CD53EDB6 (receiver_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE notification (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL, sender_id INT DEFAULT NULL, recipient_id INT DEFAULT NULL, invitation_id INT DEFAULT NULL, item_id INT DEFAULT NULL, INDEX IDX_BF5476CAF624B39D (sender_id), INDEX IDX_BF5476CAE92F8F78 (recipient_id), INDEX IDX_BF5476CAA35D7AF0 (invitation_id), INDEX IDX_BF5476CA126F525E (item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE test (id INT AUTO_INCREMENT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2FB8E54CD FOREIGN KEY (wishlist_id) REFERENCES wishlist (id)'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2F624B39D FOREIGN KEY (sender_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE `invitation` ADD CONSTRAINT FK_F11D61A2CD53EDB6 FOREIGN KEY (receiver_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAF624B39D FOREIGN KEY (sender_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAE92F8F78 FOREIGN KEY (recipient_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAA35D7AF0 FOREIGN KEY (invitation_id) REFERENCES `invitation` (id)'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA126F525E FOREIGN KEY (item_id) REFERENCES item (id)'); + $this->addSql('ALTER TABLE proof ADD created_at DATETIME NOT NULL, ADD item_id INT NOT NULL, CHANGE proof proof_image_path VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE proof ADD CONSTRAINT FK_FBF940DD126F525E FOREIGN KEY (item_id) REFERENCES item (id) ON DELETE CASCADE'); + $this->addSql('CREATE INDEX IDX_FBF940DD126F525E ON proof (item_id)'); + $this->addSql('ALTER TABLE wishlist CHANGE collaboration_token collaboration_token VARCHAR(36) DEFAULT NULL, CHANGE public_token public_token VARCHAR(36) DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A3124CB2F13 ON wishlist (collaboration_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_9CE12A31AE981E3B ON wishlist (public_token)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2FB8E54CD'); + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2F624B39D'); + $this->addSql('ALTER TABLE `invitation` DROP FOREIGN KEY FK_F11D61A2CD53EDB6'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAF624B39D'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAE92F8F78'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAA35D7AF0'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CA126F525E'); + $this->addSql('DROP TABLE `invitation`'); + $this->addSql('DROP TABLE notification'); + $this->addSql('DROP TABLE test'); + $this->addSql('DROP INDEX UNIQ_9CE12A3124CB2F13 ON wishlist'); + $this->addSql('DROP INDEX UNIQ_9CE12A31AE981E3B ON wishlist'); + $this->addSql('ALTER TABLE wishlist CHANGE collaboration_token collaboration_token VARCHAR(1024) DEFAULT NULL, CHANGE public_token public_token VARCHAR(1024) DEFAULT NULL'); + $this->addSql('ALTER TABLE proof DROP FOREIGN KEY FK_FBF940DD126F525E'); + $this->addSql('DROP INDEX IDX_FBF940DD126F525E ON proof'); + $this->addSql('ALTER TABLE proof DROP created_at, DROP item_id, CHANGE proof_image_path proof VARCHAR(255) DEFAULT NULL'); + } +} diff --git a/Server/migrations/Version20250331071151.php b/Server/migrations/Version20250331071151.php new file mode 100644 index 0000000000000000000000000000000000000000..25f059f38aea2fee55954ae644924d0873057215 --- /dev/null +++ b/Server/migrations/Version20250331071151.php @@ -0,0 +1,41 @@ +<?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 Version20250331071151 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 item ADD proof_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE item ADD CONSTRAINT FK_1F1B251ED7086615 FOREIGN KEY (proof_id) REFERENCES proof (id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1F1B251ED7086615 ON item (proof_id)'); + $this->addSql('ALTER TABLE proof DROP FOREIGN KEY FK_FBF940DD126F525E'); + $this->addSql('DROP INDEX IDX_FBF940DD126F525E ON proof'); + $this->addSql('ALTER TABLE proof DROP item_id'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE proof ADD item_id INT NOT NULL'); + $this->addSql('ALTER TABLE proof ADD CONSTRAINT FK_FBF940DD126F525E FOREIGN KEY (item_id) REFERENCES item (id) ON UPDATE NO ACTION ON DELETE CASCADE'); + $this->addSql('CREATE INDEX IDX_FBF940DD126F525E ON proof (item_id)'); + $this->addSql('ALTER TABLE item DROP FOREIGN KEY FK_1F1B251ED7086615'); + $this->addSql('DROP INDEX UNIQ_1F1B251ED7086615 ON item'); + $this->addSql('ALTER TABLE item DROP proof_id'); + } +} diff --git a/Server/migrations/Version20250331072241.php b/Server/migrations/Version20250331072241.php new file mode 100644 index 0000000000000000000000000000000000000000..79e3b80dbfc84dd3157bdf05265b6be22d947568 --- /dev/null +++ b/Server/migrations/Version20250331072241.php @@ -0,0 +1,39 @@ +<?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 Version20250331072241 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 notification DROP FOREIGN KEY FK_BF5476CA126F525E'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAA35D7AF0'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAE92F8F78'); + $this->addSql('ALTER TABLE notification DROP FOREIGN KEY FK_BF5476CAF624B39D'); + $this->addSql('DROP TABLE notification'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE notification (id INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_0900_ai_ci`, message LONGTEXT CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_0900_ai_ci`, created_at DATETIME NOT NULL, sender_id INT DEFAULT NULL, recipient_id INT DEFAULT NULL, invitation_id INT DEFAULT NULL, item_id INT DEFAULT NULL, INDEX IDX_BF5476CA126F525E (item_id), INDEX IDX_BF5476CAA35D7AF0 (invitation_id), INDEX IDX_BF5476CAE92F8F78 (recipient_id), INDEX IDX_BF5476CAF624B39D (sender_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_0900_ai_ci` ENGINE = InnoDB COMMENT = \'\' '); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA126F525E FOREIGN KEY (item_id) REFERENCES item (id) ON UPDATE NO ACTION ON DELETE NO ACTION'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAA35D7AF0 FOREIGN KEY (invitation_id) REFERENCES invitation (id) ON UPDATE NO ACTION ON DELETE NO ACTION'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAE92F8F78 FOREIGN KEY (recipient_id) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE NO ACTION'); + $this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CAF624B39D FOREIGN KEY (sender_id) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE NO ACTION'); + } +} diff --git a/Server/public/assets/image (1).png b/Server/public/assets/image (1).png new file mode 100644 index 0000000000000000000000000000000000000000..eb1ba1797cfa82371d9bbca6bdb8526d89427000 Binary files /dev/null and b/Server/public/assets/image (1).png differ diff --git a/Server/public/assets/image (10).png b/Server/public/assets/image (10).png new file mode 100644 index 0000000000000000000000000000000000000000..c29a04b34555b7f6cefcf48859a6b860f42853f6 Binary files /dev/null and b/Server/public/assets/image (10).png differ diff --git a/Server/public/assets/image (2).png b/Server/public/assets/image (2).png new file mode 100644 index 0000000000000000000000000000000000000000..92c9ce04993d5dab1f09300a5f88034b34b746d1 Binary files /dev/null and b/Server/public/assets/image (2).png differ diff --git a/Server/public/assets/image (3).png b/Server/public/assets/image (3).png new file mode 100644 index 0000000000000000000000000000000000000000..e646d0a48de3e637e9b14fd340331d74cdeaa523 Binary files /dev/null and b/Server/public/assets/image (3).png differ diff --git a/Server/public/assets/image (4).png b/Server/public/assets/image (4).png new file mode 100644 index 0000000000000000000000000000000000000000..5e0e797f35883e45a49f323c2d511561952a911f Binary files /dev/null and b/Server/public/assets/image (4).png differ diff --git a/Server/public/assets/image (5).png b/Server/public/assets/image (5).png new file mode 100644 index 0000000000000000000000000000000000000000..f89509bc237ec5c2d013062637c5ebab27f76af0 Binary files /dev/null and b/Server/public/assets/image (5).png differ diff --git a/Server/public/assets/image (6).png b/Server/public/assets/image (6).png new file mode 100644 index 0000000000000000000000000000000000000000..17d90af571e1c821e2588fcaeb63abc1184f6815 Binary files /dev/null and b/Server/public/assets/image (6).png differ diff --git a/Server/public/assets/image (7).png b/Server/public/assets/image (7).png new file mode 100644 index 0000000000000000000000000000000000000000..86ba2facbe546504b261b0db0e662362e4155be2 Binary files /dev/null and b/Server/public/assets/image (7).png differ diff --git a/Server/public/assets/image (8).png b/Server/public/assets/image (8).png new file mode 100644 index 0000000000000000000000000000000000000000..a8aab9642750594a14085329a0599bdfd30825ca Binary files /dev/null and b/Server/public/assets/image (8).png differ diff --git a/Server/public/assets/image (9).png b/Server/public/assets/image (9).png new file mode 100644 index 0000000000000000000000000000000000000000..14e5e0d52f1594906b39baa79bed72fc4503a3a5 Binary files /dev/null and b/Server/public/assets/image (9).png differ diff --git a/Server/public/css/home.css b/Server/public/css/home.css new file mode 100644 index 0000000000000000000000000000000000000000..08dbe2db6da5ea929ec2b85257ff66d3b4025395 --- /dev/null +++ b/Server/public/css/home.css @@ -0,0 +1,251 @@ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: 'Nunito', sans-serif; + background: #fff; + color: #333; + } + + /* NAVBAR */ + .navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + border-bottom: 1px solid #eee; + } + .nav-left, + .nav-right { + display: flex; + align-items: center; + } + .logo { + font-size: 1.5rem; + font-weight: 700; + color: #00a574; + } + .nav-right a { + margin-left: 1rem; + text-decoration: none; + color: #333; + } + .btn-signup { + background-color: #14223c; + color: white !important; + padding: 0.5rem 1rem; + border-radius: 4px; + } + +/* =============================== + HERO SECTION + =============================== */ + .hero { + margin-top: 3rem; + display: flex; + align-items: center; /* vertically center both sides */ + justify-content: center; /* center if total width is narrower */ + padding: 3rem 2rem; + gap: 2rem; /* spacing between the two columns */ + } + + + + .hero-img { + max-width: 250px; /* adjust as needed */ + height: auto; + } + + + + .hero-right h1 { + display: flex; + align-items: center; + justify-content: center; + font-size: 3.5rem; /* larger heading */ + line-height: 1.2; + font-weight: 700; + margin-bottom: 1.5rem; + } + + /* Search bar styling */ + .search-bar form { + display: inline-flex; + align-items: center; + } + + .search-bar input { + padding: 0.75rem 1rem; + border: 1px solid #ccc; + border-right: none; + border-radius: 4px 0 0 4px; + font-size: 1rem; + } + + .search-bar button { + padding: 0.75rem 1.5rem; + background-color: #14223c; + color: #fff; + border: none; + border-radius: 0 4px 4px 0; + cursor: pointer; + font-size: 1rem; + } + + /* =============================== + RESPONSIVE: STACK ON SMALL SCREENS + =============================== */ + @media (max-width: 768px) { + .hero { + flex-direction: column; + text-align: center; + } + + .hero-left, + .hero-right { + margin-bottom: 1.5rem; + } + + .hero-img { + max-width: 200px; /* smaller on mobile if desired */ + } + + .hero-right h1 { + font-size: 2rem; + } + } + + + /* HOW IT WORKS */ + .how-it-works { + padding: 2rem; + text-align: center; + } + .how-it-works h2 { + margin-bottom: 1.5rem; + font-size: 1.5rem; + font-weight: 700; + } + .features { + display: flex; + justify-content: space-around; + gap: 1rem; + flex-wrap: wrap; /* helps on smaller screens */ + } + .feature { + flex: 1; + max-width: 300px; + background: #fefefe; + padding: 1rem; + border: 1px solid #eee; + border-radius: 4px; + margin: 0.5rem; + } + .feature img { + max-width: 120px; + margin-bottom: 1rem; + } + .feature h3 { + margin-bottom: 0.5rem; + } + + /* COMMUNITY */ + .community { + padding: 2rem; + text-align: center; + } + .community h2 { + margin-bottom: 1.5rem; + font-size: 1.5rem; + font-weight: 700; + } + .community-cards { + display: flex; + justify-content: space-around; + gap: 1rem; + flex-wrap: wrap; + } + .community-card { + flex: 1; + max-width: 300px; + background: #fefefe; + padding: 1rem; + border: 1px solid #eee; + border-radius: 4px; + margin: 0.5rem; + } + .community-card img { + max-width: 120px; + margin-bottom: 1rem; + } + .community-card h3 { + margin-bottom: 0.5rem; + } + .btn { + display: inline-block; + margin-top: 0.5rem; + background-color: #00a574; + color: #fff; + padding: 0.5rem 1rem; + text-decoration: none; + border-radius: 4px; + } + + /* GET THE APP */ + .get-app { + padding: 2rem; + text-align: center; + } + .get-app h2 { + margin-bottom: 1.5rem; + font-size: 1.5rem; + font-weight: 700; + } + .app-wrapper { + display: flex; + justify-content: center; + align-items: center; + gap: 1.5rem; + flex-wrap: wrap; + margin-top: 1rem; + } + .app-img { + max-width: 150px; + height: auto; + } + .app-details { + max-width: 500px; + text-align: left; + } + .app-details h3 { + margin-bottom: 1rem; + } + + /* CTA */ + .cta { + padding: 2rem; + text-align: center; + } + .cta p { + margin-bottom: 1rem; + } + .btn-outline { + background-color: transparent; + color: #00a574; + border: 2px solid #00a574; + } + + /* FOOTER */ + .footer { + text-align: center; + padding: 1rem; + border-top: 1px solid #eee; + } + .footer p { + margin: 0.5rem 0; + } + \ No newline at end of file diff --git a/Server/public/css/login.css b/Server/public/css/login.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/Server/public/css/poopup.CSS b/Server/public/css/poopup.CSS new file mode 100644 index 0000000000000000000000000000000000000000..818e5535ada69a5805506ef2590ec701f6ebe523 --- /dev/null +++ b/Server/public/css/poopup.CSS @@ -0,0 +1,69 @@ +/* Styles de base du popup */ +.share-popup { + position: fixed; + top: 50%; /* Centrage vertical */ + left: 50%; /* Centrage horizontal */ + transform: translate(-50%, -50%); /* Ajuste pour centrer parfaitement */ + background-color: rgba(0, 0, 0, 0.5); /* Fond semi-transparent */ + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + width: 100%; /* Optionnel : peut être ajusté */ + height: 100%; /* Optionnel : peut être ajusté */ +} + +.share-popup.hidden { + display: none !important; /* Masque le popup */ +} + +.share-popup-content { + background-color: white; + padding: 25px; + border-radius: 12px; + width: 90%; + max-width: 550px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + text-align: center; /* Centrer le contenu */ +} + +.share-popup-content h2 { + margin-top: 0; + margin-bottom: 20px; + font-size: 1.5rem; + font-weight: 600; +} + +.share-popup-content p { + margin-bottom: 15px; + color: #555; +} + +/* Bouton Fermer */ +.popup-footer { + display: flex; + justify-content: center; + margin-top: 25px; +} + +.close-popup-btn { + padding: 10px 20px; + background-color: #f0f0f0; + color: #333; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +.close-popup-btn:hover { + background-color: #e0e0e0; +} + +/* Styles responsive */ +@media (max-width: 576px) { + .share-popup-content { + width: 95%; + padding: 20px; + } +} \ No newline at end of file diff --git a/Server/public/css/signup.css b/Server/public/css/signup.css new file mode 100644 index 0000000000000000000000000000000000000000..68161b682c64387cecb0f7586948174d4339ae78 --- /dev/null +++ b/Server/public/css/signup.css @@ -0,0 +1,192 @@ +/************************************** + GLOBAL STYLES +**************************************/ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: 'Work Sans', sans-serif; + background-color: #fff; + color: #333; + min-height: 100vh; + position: relative; /* so the absolute .member-link can position properly */ + } + + /************************************** + "ALREADY A MEMBER? SIGN IN" LINK + **************************************/ + .member-link { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 10; + text-decoration: none; + color: #333; + font-size: 1rem; + font-weight: 400; + } + .member-link span { + font-weight: 600; + text-decoration: underline; + } + + /************************************** + SIGNUP CONTAINER + **************************************/ + .signup-container { + display: flex; /* keep panels side by side */ + min-height: 100vh; /* full viewport height */ + width: 100%; + } + + /************************************** + LEFT PANEL (1/3) + **************************************/ + .left-panel { + position: relative; + width: 33.3333%; + background-size: cover; + background-position: center; + overflow: hidden; + } + + /* Dark overlay on the background image */ + .overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.4); + z-index: 1; + } + + /* Content (logo + tagline) on top of overlay */ + .left-content { + position: relative; + z-index: 2; /* above overlay */ + height: 100%; + display: flex; + flex-direction: column; + justify-content: start; /* center vertically */ + padding: 2rem; + color: #fff; + } + + /* Logo in Pacifico */ + .logo { + font-family: 'Pacifico', cursive; + font-size: 2.5rem; + margin-bottom: 2rem; + } + + /* Tagline text */ + .tagline { + font-size: 1.75rem; + font-weight: 500; + line-height: 1.3; + } + + /************************************** + RIGHT PANEL (2/3) + **************************************/ + .right-panel { + width: 66.6667%; + display: flex; + flex-direction: column; + justify-content: center; + padding: 3rem 8rem; + } + + /* Sign-up heading */ + .right-panel h1 { + font-size: 2rem; + margin-bottom: 2rem; + color: #0f1d36; /* your "primary" color */ + } + + /************************************** + SIGNUP FORM + **************************************/ + .signup-form { + display: flex; + flex-direction: column; + gap: 1rem; + max-width: 320px; /* limit form width */ + } + + /* Form inputs */ + .form-control { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 1rem; + } + + /* Submit button */ + .signup-btn { + width: 100%; + padding: 0.75rem; + background-color: #0f1d36; + color: #fff; + border: none; + border-radius: 5px; + font-size: 1rem; + cursor: pointer; + margin-top: 1rem; + } + + /************************************** + UPLOAD BUTTONS + **************************************/ + .upload-buttons { + margin-top: 2rem; + display: flex; + gap: 1rem; + } + + .upload-btn { + background-color: #e7e7e7; + color: #333; + border: none; + border-radius: 5px; + padding: 0.75rem 1rem; + cursor: pointer; + font-size: 0.9rem; + } + + /************************************** + FOOTER + **************************************/ + .footer { + position: absolute; + bottom: 1rem; + right: 1rem; + font-size: 0.8rem; + color: #999; + } + + /************************************** + RESPONSIVE + **************************************/ + /* + By default, we do NOT stack the panels vertically. + If you ever need to stack them, you could add: + + @media (max-width: 768px) { + .signup-container { + flex-direction: column; + } + .left-panel, + .right-panel { + width: 100%; + } + } + + But the user requested "no stacking," so we omit it. + */ + \ No newline at end of file diff --git a/Server/public/css/style.css b/Server/public/css/style.css new file mode 100644 index 0000000000000000000000000000000000000000..10658c5495003dbd6a739f8fc17ba3b3a9a5a3e6 --- /dev/null +++ b/Server/public/css/style.css @@ -0,0 +1,637 @@ +/* Global styles */ +:root { + --primary-color: #00B8DE; + --secondary-color: #ff4081; + --dark-color: #14223C; + --light-color: #f5f5f5; + --success-color: #9ACC99; + --info-color: #2196f3; + --warning-color: #ff9800; + --danger-color: #a12d25; +} + +/* Button icon styling */ +.btn-icon { + width: 40px; + height: 40px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; +} + +.btn-icon:hover { + background-color: var(--light-color); +} + +/* Override Bootstrap colors */ +.btn-primary { + background-color: var(--primary-color); + border-color: var(--primary-color); + margin: 2px; +} + +.btn-creer { + background-color: var(--success-color); + border-color: var(--success-color); + color: var(--light-color); + margin: 2px; +} + +.btn-supprimer { + background-color: var(--danger-color); + border-color: var(--danger-color); + color: var(--light-color); + margin: 2px; +} + + +.btn-warning { + background-color: var(--warning-color); + border-color: var(--warning-color); + margin: 2px; +} + +.back-to-lists-btn { + background-color: var(--dark-color); + border-color: var(--dark-color); + color: var(--light-color); + margin: 2px; +} + +.back-to-lists-btn:hover { + background-color: #f5f5f5; +} + +/* Style for expired wishlists */ +.card-expired { + opacity: 0.8; + background-color: #f8f9fa; + border: 1px solid #dee2e6; +} + +.card-expired .card-title { + color: #6c757d; +} + +.card-expired .badge { + margin-left: 10px; + font-size: 0.7rem; + vertical-align: middle; +} + +.text-primary { + color: var(--primary-color) !important; +} + +/* Navbar customization */ +.navbar-dark.bg-dark { + background-color: var(--primary-color) !important; +} + +/* Search container styling */ +.search-container { + background-color: var(--light-color); + padding: 15px; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.btn-search{ + background-color: var(--success-color); + color: var(--light-color); +} + +.title { + color: var(--dark-color); + font-size: 2rem; + justify-content: center; + margin: 2rem 0; + +} + +/* Card styling */ +.card { + border: none; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +} + +/* Footer styling */ +footer { + background-color: var(--primary-color) !important; +} + +/* Custom alert styling */ +.alert-success { + background-color: var(--success-color); + color: white; +} + +.alert-danger { + background-color: var(--danger-color); + color: white; +} + +.brand-logo { + font-family: 'Pacifico', cursive; +} + +.nav-item{ + color: var(--light-color); +} + +/* Footer styling */ +html { + height: 100%; +} + +body { + background-color: var(--light-color); + color: var(--dark-color); + min-height: 100%; + display: flex; + flex-direction: column; + margin: 0; +} + +.content-wrapper { + flex: 1 0 auto; +} + +.footer { + background-color: var(--dark-color); + color: var(--light-color); + margin-top: auto; + flex-shrink: 0; +} + + + +/* SIGN UP */ + +.signup-container { + display: flex; + min-height: 100vh; + width: 100%; + overflow: hidden; + position: relative; + margin-top: -4rem; /* Remove the navbar space */ +} + +.signup-sidebar { + flex: 0 0 50%; /* Fixed width at 50% */ + background-color: var(--dark-color); /* Fallback color */ + position: relative; + padding: 0; /* Remove padding */ + overflow: hidden; /* Ensure the image doesn't overflow */ +} + +.signup-sidebar::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: url('https://thumbs.dreamstime.com/b/close-up-hands-giving-beautifully-wrapped-gift-to-another-person-scene-conveys-warmth-care-symbolizes-person-352951009.jpg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + z-index: 0; +} + +.signup-content { + flex: 0 0 50%; /* Fixed width at 50% */ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 2rem; +} + +.signup-form { + width: 100%; + max-width: 450px; /* Make the form wider */ +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-control { + padding: 0.75rem 1rem; + font-size: 1rem; + border-radius: 0.5rem; + border: 1px solid #ced4da; +} + +.backdrop-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1; +} + +.sidebar-content { + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80%; + text-align: center; + color: white; +} + +.logo { + font-family: 'Pacifico', cursive; + font-size: 4rem; + color: white; + margin-bottom: 6rem; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); +} + +.tagline { + font-size: 1.8rem; + font-weight: 600; + color: white; + margin-bottom: 1rem; + line-height: 1.4; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6); +} + +.signup-btn { + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 0.5rem; + padding: 0.75rem 2.5rem; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + width: 100%; + margin-top: 1.5rem; + transition: background-color 0.3s ease; +} + +.signup-btn:hover { + background-color: #0095b3; +} + +.signin-prompt { + margin-top: 1.5rem; + text-align: center; +} + +.signin-link { + color: var(--primary-color); + font-weight: 600; + text-decoration: none; + margin-left: 0.5rem; +} + +.backdrop-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.3); + z-index: 1; +} + +.sidebar-content { + position: relative; + z-index: 2; + color: white; +} + +body { + font-family: Arial, sans-serif; + background: #f9f9f9; + color: #333; +} +.content { + max-width: 900px; + margin: 40px auto; + text-align: center; + background: #fff; + padding: 30px; + border-radius: 10px; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); +} +.features-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; +} +.feature-item { + background: #e3f2fd; + padding: 20px; + border-radius: 10px; +} +.team-members { + display: flex; + justify-content: center; + gap: 20px; +} +.member { + background: #f5f5f5; + padding: 20px; + border-radius: 10px; + text-align: center; + width: 30%; +} +.member img { + width: 100px; + height: 100px; + border-radius: 50%; + margin-bottom: 10px; +} + +.search-container { + max-width: 800px; + width: 100%; +} + +.form-control { + flex-grow: 1; + border: none; + box-shadow: none; + background-color: white; + padding: 10px 20px; + margin: 8px 0; + box-sizing: border-box; +} + +.input-group-text { + background-color: white; + border: none; + max-width: 800px; + width: 100%; +} + +.search-button { + background-color: #00A9E0; + color: white; + border: none; + padding: 10px 20px; + font-size: 16px; +} + +/* Section: How GiftWish Works */ +.giftwish-works { + padding: 60px 20px; +} + +.giftwish-works h2 { + font-size: 28px; + font-weight: bold; + margin-bottom: 40px; +} + +/* Conteneur Flexbox pour alignement horizontal */ +.giftwish-works .d-flex { + display: flex; + justify-content: space-between; + align-items: flex-start; /* Aligner en haut pour garder l’harmonie */ + flex-wrap: wrap; + gap: 20px; +} + +/* Boîte de chaque élément */ +.feature-box { + background: #ffffff; + padding: 20px; + border-radius: 10px; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease-in-out; + width: 30%; /* Ajuster pour un bon affichage */ + text-align: center; +} + +.feature-box:hover { + transform: translateY(-5px); +} + +/* Images bien alignées */ +.feature-img { + max-width: 100px; + margin-bottom: 15px; + display: block; + margin-left: auto; + margin-right: auto; +} + +.feature-box h3 { + font-size: 18px; + font-weight: 600; + margin-bottom: 10px; +} + +.feature-box p { + font-size: 14px; + color: #555; +} + +/* Adaptabilité pour mobile */ +@media (max-width: 768px) { + .giftwish-works .d-flex { + flex-direction: column; + align-items: center; + } + + .feature-box { + width: 100%; + max-width: 300px; + } +} + +/* giftwhish-community section */ +.giftwish-community { + padding: 40px 20px; + max-width: 1000px; + margin: auto; +} + +.giftwish-community h2 { + font-size: 28px; + font-weight: bold; + margin-bottom: 40px; + text-align: center; +} + +.community-title { + font-size: 24px; + font-weight: bold; + margin-bottom: 30px; + text-align: left; +} + +/* Liste des éléments */ +.community-list { + display: flex; + flex-direction: column; + gap: 30px; +} + +/* Chaque élément : image tout à gauche, texte + bouton tout à droite */ +.community-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px; + border-bottom: 1px solid #ddd; +} + +/* Image bien calée à gauche */ +.community-img { + flex-shrink: 0; + width: 180px; +} + +.community-img img { + width: 100%; + height: auto; + display: block; +} + +/* Texte bien calé à droite */ +.community-text { + flex: 1; + text-align: right; + padding-left: 40px; +} + +.community-text h3 { + font-size: 20px; + font-weight: bold; + margin-bottom: 10px; +} + +.community-text p { + font-size: 16px; + color: #555; + margin-bottom: 15px; +} + +/* Boutons */ +.community-text .btn { + background-color: #5cb85c; + color: white; + font-size: 16px; + padding: 10px 20px; + border-radius: 5px; +} + +/* Suppression du dernier trait */ +.community-item:last-child { + border-bottom: none; +} + +/* Responsive : en colonne sur mobile */ +@media (max-width: 768px) { + .community-item { + flex-direction: column; + text-align: center; + } + + .community-img { + width: 120px; + } + + .community-text { + text-align: center; + padding-left: 0; + } +} + +/* get-giftwish */ +.get-giftwish-app { + display: flex; + align-items: center; + justify-content: space-between; + max-width: 1000px; + margin: auto; + padding: 40px 20px; + border-bottom: 1px solid #E5E7EB; /* Ligne grise en bas */ +} + +/* Titre de la section */ +.get-giftwish-app h2 { + font-size: 28px; + font-weight: bold; + margin-bottom: 40px; + text-align: center; +} + +/* Texte */ +.get-giftwish-app p { + font-size: 16px; + color: #374151; /* Gris foncé */ + margin-bottom: 20px; +} + +/* Bouton vert */ +.get-giftwish-app .btn-green { + background-color: #98C99C; /* Vert pastel */ + color: #111827; + font-size: 16px; + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; +} + +/* Image */ +.get-giftwish-app img { + width: 250px; + max-width: 100%; +} + + +.profile-header { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.header-image { + width: 100%; + height: 200px; /* Adjust the height as needed */ + object-fit: cover; + border-radius: 8px; /* Rounded corners for the header image */ +} + +.profile-pic-container { + position: absolute; + bottom: -70px; /* Adjust to overlap the header */ + left: 70px; /* Adjust to position within the header */ +} + +.profile-pic { + width: 120px; /* Profile picture size */ + height: 120px; + object-fit: cover; + border-radius: 50%; /* Makes the image circular */ + border: 4px solid var(--light-color); /* Optional border for better aesthetics */ + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Add a slight shadow */ +} + +.username { + margin-top: 50px; /* Space between profile pic and username */ + font-weight: bold; + text-align: left; + margin-left: 40px; /* Align with the profile picture */ +} diff --git a/Server/public/css/wishlists.css b/Server/public/css/wishlists.css new file mode 100644 index 0000000000000000000000000000000000000000..5251fc605719da100e726df8b977c1d309eece06 --- /dev/null +++ b/Server/public/css/wishlists.css @@ -0,0 +1,51 @@ +.card { + margin-bottom: 20px; /* Ajoute un espace entre les cartes */ + padding: 15px; /* Ajoute un espace interne dans la carte */ +} + +.btn-group { + gap: 10px; /* Ajoute un espace entre les boutons */ +} + +.card-title { + font-size: 1.2rem; + font-weight: bold; + color: #333; + margin-bottom: 10px; /* Ajoute un espace sous le titre */ +} + +.card-text { + font-size: 0.9rem; + color: #666; +} +.badge { + font-size: 0.8rem; + padding: 0.4em 0.6em; + border-radius: 12px; +} + +.badge.bg-info { + background-color: #17a2b8; + color: #fff; +} +.btn { + transition: all 0.3s ease-in-out; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} +h4 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1rem; + color: #333; + border-bottom: 2px solid #ddd; + padding-bottom: 10px; +} \ No newline at end of file diff --git a/Server/public/images/avatars/default.png b/Server/public/images/avatars/default.png new file mode 100644 index 0000000000000000000000000000000000000000..9599920bc00e1fb05b754e423ff4f075cdad799f Binary files /dev/null and b/Server/public/images/avatars/default.png differ diff --git a/Server/public/images/blank_prof_pic.jpg b/Server/public/images/blank_prof_pic.jpg new file mode 100644 index 0000000000000000000000000000000000000000..31cf91847c0598ded7d173ad81d0c644afce8cd7 Binary files /dev/null and b/Server/public/images/blank_prof_pic.jpg differ diff --git a/Server/public/images/community.png b/Server/public/images/community.png new file mode 100644 index 0000000000000000000000000000000000000000..e5dbb8b1368a9c6131544473cb52dd504ce504dc Binary files /dev/null and b/Server/public/images/community.png differ diff --git a/Server/public/images/contributor.jpg b/Server/public/images/contributor.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c1d6a206af18743010b78378913d97f83173ad3 Binary files /dev/null and b/Server/public/images/contributor.jpg differ diff --git a/Server/public/images/expensive-gifts.jpg b/Server/public/images/expensive-gifts.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e0e16968c6470aaec6b6b89344d59a252f54dfd1 Binary files /dev/null and b/Server/public/images/expensive-gifts.jpg differ diff --git a/Server/public/images/gift-giver.jpg b/Server/public/images/gift-giver.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0cb7d3d3c66d19e35dfc5484f188459ef68efbde Binary files /dev/null and b/Server/public/images/gift-giver.jpg differ diff --git a/Server/public/images/gift-image.png b/Server/public/images/gift-image.png new file mode 100644 index 0000000000000000000000000000000000000000..a91d586cbb5dac7a38194bacd621edff61843231 Binary files /dev/null and b/Server/public/images/gift-image.png differ diff --git a/Server/public/images/gifts.jpg b/Server/public/images/gifts.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5a3041e431dabe6737d08109cee7d67734b76d3e Binary files /dev/null and b/Server/public/images/gifts.jpg differ diff --git a/Server/public/images/kdo.png b/Server/public/images/kdo.png new file mode 100644 index 0000000000000000000000000000000000000000..daaec2c0c3c6cb5a824822124b243359904bc26b Binary files /dev/null and b/Server/public/images/kdo.png differ diff --git a/Server/public/images/proofs/Dream-TradingCard-67e996c603df6.jpg b/Server/public/images/proofs/Dream-TradingCard-67e996c603df6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..872803357aeb08876ad05a026d723bac05df28e5 Binary files /dev/null and b/Server/public/images/proofs/Dream-TradingCard-67e996c603df6.jpg differ diff --git a/Server/public/images/proofs/Dream-TradingCard-67e9979c7087e.jpg b/Server/public/images/proofs/Dream-TradingCard-67e9979c7087e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..872803357aeb08876ad05a026d723bac05df28e5 Binary files /dev/null and b/Server/public/images/proofs/Dream-TradingCard-67e9979c7087e.jpg differ diff --git a/Server/public/images/proofs/Dream-TradingCard-67e998bf56995.jpg b/Server/public/images/proofs/Dream-TradingCard-67e998bf56995.jpg new file mode 100644 index 0000000000000000000000000000000000000000..872803357aeb08876ad05a026d723bac05df28e5 Binary files /dev/null and b/Server/public/images/proofs/Dream-TradingCard-67e998bf56995.jpg differ diff --git a/Server/public/images/proofs/Dream-TradingCard-67e9990df1f90.jpg b/Server/public/images/proofs/Dream-TradingCard-67e9990df1f90.jpg new file mode 100644 index 0000000000000000000000000000000000000000..872803357aeb08876ad05a026d723bac05df28e5 Binary files /dev/null and b/Server/public/images/proofs/Dream-TradingCard-67e9990df1f90.jpg differ diff --git a/Server/public/images/proofs/Dream-TradingCard-67e99921b31fc.jpg b/Server/public/images/proofs/Dream-TradingCard-67e99921b31fc.jpg new file mode 100644 index 0000000000000000000000000000000000000000..872803357aeb08876ad05a026d723bac05df28e5 Binary files /dev/null and b/Server/public/images/proofs/Dream-TradingCard-67e99921b31fc.jpg differ diff --git a/Server/public/images/proofs/Dream-TradingCard-67e9993b76858.jpg b/Server/public/images/proofs/Dream-TradingCard-67e9993b76858.jpg new file mode 100644 index 0000000000000000000000000000000000000000..872803357aeb08876ad05a026d723bac05df28e5 Binary files /dev/null and b/Server/public/images/proofs/Dream-TradingCard-67e9993b76858.jpg differ diff --git a/Server/public/images/proofs/Dream-TradingCard-67e99a8c4aee2.jpg b/Server/public/images/proofs/Dream-TradingCard-67e99a8c4aee2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..872803357aeb08876ad05a026d723bac05df28e5 Binary files /dev/null and b/Server/public/images/proofs/Dream-TradingCard-67e99a8c4aee2.jpg differ diff --git a/Server/public/images/proofs/Dream-TradingCard-67e9ad88f308f.jpg b/Server/public/images/proofs/Dream-TradingCard-67e9ad88f308f.jpg new file mode 100644 index 0000000000000000000000000000000000000000..872803357aeb08876ad05a026d723bac05df28e5 Binary files /dev/null and b/Server/public/images/proofs/Dream-TradingCard-67e9ad88f308f.jpg differ diff --git a/Server/public/images/purchased-items.jpg b/Server/public/images/purchased-items.jpg new file mode 100644 index 0000000000000000000000000000000000000000..454409ec4b6b7f31fba8a7b8b6f7ad09ab58c3c9 Binary files /dev/null and b/Server/public/images/purchased-items.jpg differ diff --git a/Server/public/images/team-member.jpg b/Server/public/images/team-member.jpg new file mode 100644 index 0000000000000000000000000000000000000000..77684c09343f3d43e9b6bf3385bc4dec11b0ab11 Binary files /dev/null and b/Server/public/images/team-member.jpg differ diff --git a/Server/public/images/wishlist.jpg b/Server/public/images/wishlist.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b059a8d2121efa3f318673bb42a1e1c3252d78d1 Binary files /dev/null and b/Server/public/images/wishlist.jpg differ diff --git a/Server/public/index.php b/Server/public/index.php new file mode 100644 index 0000000000000000000000000000000000000000..9982c218d6969c72d4c91e3834e3f535e2dfe68b --- /dev/null +++ b/Server/public/index.php @@ -0,0 +1,9 @@ +<?php + +use App\Kernel; + +require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + +return function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); +}; diff --git a/Server/src/.DS_Store b/Server/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..524596811e8d1a036219368fc79c9427bdcddc94 Binary files /dev/null and b/Server/src/.DS_Store differ diff --git a/Server/src/Controller/.gitignore b/Server/src/Controller/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/Server/src/Controller/AdminController.php b/Server/src/Controller/AdminController.php new file mode 100644 index 0000000000000000000000000000000000000000..302036184fa73c70fa507c1462df904f0999d227 --- /dev/null +++ b/Server/src/Controller/AdminController.php @@ -0,0 +1,100 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Controller; + +use App\Repository\ItemRepository; +use App\Repository\WishlistRepository; +use App\Repository\UserRepository; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; + +#[Route('/admin')] +#[IsGranted('ROLE_USER')] +class AdminController extends AbstractController +{ + #[Route('/top3', name: 'admin_top3')] + public function top3( + ItemRepository $itemRepo, + WishlistRepository $wishlistRepo + ): Response { + // (a) Top-3 most expensive items bought for a wishlist + $topExpensiveItems = $itemRepo->findTop3MostExpensiveItems(); + + // (b) Top-3 wishlists by total value of purchased gifts + $topWishlists = $wishlistRepo->findTop3WishlistsByTotalValue(); + + return $this->render('admin/top3.html.twig', [ + 'topItems' => $topExpensiveItems, + 'topWishlists' => $topWishlists, + ]); + } + + #[Route('/dashboard', name: 'admin_dashboard')] + public function dashboard(): Response + { + return $this->render('admin/dashboard.html.twig', [ + 'controller_name' => 'AdminController', + ]); + } + + #[Route('/users', name: 'admin_users')] + public function listUsers(UserRepository $userRepo): Response + { + $currentUser = $this->getUser(); + + $allUsers = $userRepo->findAll(); + $users = array_filter($allUsers, function($user) use ($currentUser) { + return $user->getId() !== $currentUser->getId(); + }); + + return $this->render('admin/users.html.twig', [ + 'users' => $users, + ]); + } + + #[Route('/users/{id}/lock', name: 'admin_user_lock', methods: ['POST'])] + public function lockUser($id, UserRepository $userRepo, EntityManagerInterface $entityManager): Response + { + $user = $userRepo->find($id); + if ($user) { + $user->setLockStatus(true); + $entityManager->persist($user); + $entityManager->flush(); + $this->addFlash('success', 'User locked.'); + } + return $this->redirectToRoute('admin_users'); + } + + #[Route('/users/{id}/unlock', name: 'admin_user_unlock', methods: ['POST'])] + public function unlockUser($id, UserRepository $userRepo, EntityManagerInterface $entityManager): Response + { + $user = $userRepo->find($id); + if ($user) { + $user->setLockStatus(false); + $entityManager->persist($user); + $entityManager->flush(); + $this->addFlash('success', 'User unlocked.'); + } + return $this->redirectToRoute('admin_users'); + } + + #[Route('/users/{id}/delete', name: 'admin_user_delete', methods: ['POST'])] + public function deleteUser($id, UserRepository $userRepo, EntityManagerInterface $entityManager): Response + { + $user = $userRepo->find($id); + if ($user) { + $entityManager->remove($user); + $entityManager->flush(); + $this->addFlash('success', 'User deleted.'); + } + return $this->redirectToRoute('admin_users'); + } +} \ No newline at end of file diff --git a/Server/src/Controller/HomeController.php b/Server/src/Controller/HomeController.php new file mode 100644 index 0000000000000000000000000000000000000000..db80ef1ae94f26c26a0be5c74763b00857236ef0 --- /dev/null +++ b/Server/src/Controller/HomeController.php @@ -0,0 +1,49 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use App\Repository\ItemRepository; + + +class HomeController extends AbstractController +{ + #[Route('/home', name: 'app_home')] + public function index(): Response + { + return $this->render('home/index.html.twig'); + } + + #[Route('/about', name: 'app_about')] + public function about(): Response + { + return $this->render('home/about.html.twig', [ + 'controller_name' => 'HomeController', + ]); + } + + #[Route('/search', name: 'app_search', methods: ['GET'])] + public function search(Request $request, ItemRepository $itemRepository): Response + { + $query = $request->query->get('q', ''); + $results = []; + + if ($query) { + $results = $itemRepository->searchItems($query); + } + + return $this->render('home/search.html.twig', [ + 'query' => $query, + 'results' => $results, + ]); + } +} \ No newline at end of file diff --git a/Server/src/Controller/InvitationController.php b/Server/src/Controller/InvitationController.php new file mode 100644 index 0000000000000000000000000000000000000000..c7ade8f0cd8123583fc7a44fbcd68922dea73e9b --- /dev/null +++ b/Server/src/Controller/InvitationController.php @@ -0,0 +1,164 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Controller; + +use App\Entity\Invitation; +use App\Form\InvitationType; +use App\Repository\InvitationRepository; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +class InvitationController extends AbstractController +{ + #[Route("/invitations", name: "invitation_list", methods: ["GET"])] + public function index(InvitationRepository $invitationRepository): Response + { + // Retrieve invitations where the current user is the receiver + $user = $this->getUser(); + if (!$user) { + throw $this->createAccessDeniedException('Accès refusé.'); + } + + $invitations = $invitationRepository->findBy([ + 'receiver' => $user + ]); + + return $this->render('invitation/listshow.html.twig', [ + 'invitations' => $invitations, + ]); + } + + #[Route("/invitations/new", name: "invitation_new", methods: ["GET", "POST"])] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $invitation = new Invitation(); + $form = $this->createForm(InvitationType::class, $invitation); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($invitation); + $entityManager->flush(); + + return $this->redirectToRoute('invitation_list'); + } + + return $this->render('invitation/new.html.twig', [ + 'invitation' => $invitation, + 'form' => $form->createView(), + ]); + } + + #[Route("/invitations/{id}", name: "invitation_show", methods: ["GET"])] + public function show(Invitation $invitation): Response + { + // Optionally verify that the current user is the receiver of the invitation + $user = $this->getUser(); + if ($invitation->getReceiver() !== $user) { + throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette invitation.'); + } + + return $this->render('invitation/show.html.twig', [ + 'invitation' => $invitation, + ]); + } + + #[Route("/invitations/{id}/edit", name: "invitation_edit", methods: ["GET", "POST"])] + public function edit(Request $request, Invitation $invitation, EntityManagerInterface $entityManager): Response + { + // Optionally verify that the current user is the receiver of the invitation + $user = $this->getUser(); + if ($invitation->getReceiver() !== $user) { + throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette invitation.'); + } + + $form = $this->createForm(InvitationType::class, $invitation); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + return $this->redirectToRoute('invitation_list'); + } + + return $this->render('invitation/edit.html.twig', [ + 'invitation' => $invitation, + 'form' => $form->createView(), + ]); + } + + #[Route("/invitations/{id}", name: "invitation_delete", methods: ["POST"])] + public function delete(Request $request, Invitation $invitation, EntityManagerInterface $entityManager): Response + { + // CSRF verification and optionally ensure that the current user is the receiver + $user = $this->getUser(); + if ($invitation->getReceiver() !== $user) { + throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette invitation.'); + } + + if ($this->isCsrfTokenValid('delete'.$invitation->getId(), $request->request->get('_token'))) { + $entityManager->remove($invitation); + $entityManager->flush(); + } + + return $this->redirectToRoute('invitation_list'); + } + + #[Route("/invitations/{id}/accept", name: "invitation_accept", methods: ["POST"])] + public function accept(Invitation $invitation, EntityManagerInterface $entityManager): Response + { + $user = $this->getUser(); + + // Verify that the current user is the receiver of the invitation + if ($invitation->getReceiver() !== $user) { + throw $this->createAccessDeniedException("Vous n'avez pas accès à cette invitation."); + } + + // If the invitation has already been accepted, show an info message + if ($invitation->isAccepted()) { + $this->addFlash('info', 'Cette invitation a déjà été acceptée.'); + return $this->redirectToRoute('app_wishlists_index'); + } + + // Accept the invitation. The entity method will add the wishlist to the user's collaborative lists. + $invitation->setAccepted(true); + + $entityManager->flush(); + + $this->addFlash('success', 'Invitation acceptée. La wishlist a été ajoutée à vos listes partagées.'); + + return $this->redirectToRoute('app_wishlists_index'); + } + + #[Route("/invitations/{id}/reject", name: "invitation_reject", methods: ["POST"])] + public function reject(Invitation $invitation, EntityManagerInterface $entityManager): Response + { + $user = $this->getUser(); + + // Verify that the current user is the receiver of the invitation + if ($invitation->getReceiver() !== $user) { + throw $this->createAccessDeniedException("Vous n'avez pas accès à cette invitation."); + } + + // If the invitation is already rejected or accepted, we might show a message + if ($invitation->isAccepted() === false) { + $this->addFlash('info', 'Cette invitation a déjà été rejetée.'); + return $this->redirectToRoute('invitation_list'); + } + + // Set invitation as rejected (you may choose to store false, or handle rejection in your own way) + $invitation->setAccepted(false); + $entityManager->flush(); + + $this->addFlash('success', 'Invitation rejetée.'); + + return $this->redirectToRoute('invitation_list'); + } +} diff --git a/Server/src/Controller/ItemController.php b/Server/src/Controller/ItemController.php new file mode 100644 index 0000000000000000000000000000000000000000..82e661e35a50135e894fa4a51182823c4229d2c1 --- /dev/null +++ b/Server/src/Controller/ItemController.php @@ -0,0 +1,110 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Controller; + +use App\Entity\Item; +use App\Entity\Wishlist; +use App\ItemForm\ItemFormType; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Http\Attribute\IsGranted; + +#[Route('/wishlists/{idWishlist}/items')] +#[IsGranted('ROLE_USER')] +final class ItemController extends AbstractController +{ + #[Route('/{idItem}/delete', name: 'delete_item', methods: ['POST'])] + public function delete(int $idWishlist, int $idItem, EntityManagerInterface $entityManager): Response + { + $wishlist = $entityManager->getRepository(Wishlist::class)->find($idWishlist); + $item = $entityManager->getRepository(Item::class)->find($idItem); + + // Vérification des permissions via le voter + $this->denyAccessUnlessGranted('DELETE', $item); + + if ($wishlist->getOwner() == $this->getUser() || $wishlist->getCollaborators()->contains($this->getUser())) { + // S'il y a une preuve associée, on la supprime et on effectue un flush + if ($item->getProof()) { + $proof = $item->getProof(); + $item->setProof(null); + $entityManager->remove($proof); + $entityManager->flush(); + } + // Supprime l'item + $entityManager->remove($item); + $entityManager->flush(); + } + + // Redirige vers la wishlist + return $this->redirectToRoute('app_wishlist_show', ['id' => $idWishlist]); + } + + + #[Route('/add', name: 'add_item')] + public function add(int $idWishlist, Request $request, EntityManagerInterface $entityManager): Response + { + $wishlist = $entityManager->getRepository(Wishlist::class)->find($idWishlist); + $item = new Item(); + $this->denyAccessUnlessGranted('ADD', $item); + $form = $this->createForm(ItemFormType::class, $item); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $item->setWishlist($wishlist); + $item->setHasPurchased(false); + if ($wishlist->getOwner() == $this->getUser() || $wishlist->getCollaborators()->contains($this->getUser())) { + $entityManager->persist($item); + $entityManager->flush(); + return $this->redirectToRoute('app_wishlist_show', [ + 'id' => $idWishlist, + ]); + } + } + + return $this->render('item/creatingItem.html.twig', [ + 'wishlist' => $wishlist, + 'form' => $form->createView(), + ]); + } + + #[Route('/{idItem}/edit', name: 'edit_item')] + public function edit(int $idWishlist, int $idItem, Request $request, EntityManagerInterface $entityManager): Response + { + $wishlist = $entityManager->getRepository(Wishlist::class)->find($idWishlist); + $item = $entityManager->getRepository(Item::class)->find($idItem); + $this->denyAccessUnlessGranted('EDIT', $item); + + // Vérifie que l'item existe et qu'il appartient à la wishlist + if (!$item || $item->getWishlist()->getId() !== $wishlist->getId()) { + throw $this->createNotFoundException('Item not found in this wishlist'); + } + + $form = $this->createForm(ItemFormType::class, $item); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + if ($wishlist->getOwner() == $this->getUser() || $wishlist->getCollaborators()->contains($this->getUser())) { + $entityManager->persist($item); + $entityManager->flush(); + } + return $this->redirectToRoute('app_wishlist_show', [ + 'id' => $idWishlist, + ]); + } + + return $this->render('item/editItem.html.twig', [ + 'wishlist' => $wishlist, + 'item' => $item, + 'form' => $form->createView(), + ]); + } +} diff --git a/Server/src/Controller/ItemsController.php b/Server/src/Controller/ItemsController.php new file mode 100644 index 0000000000000000000000000000000000000000..2814690949b18ac91149d7ebfec4998520abe461 --- /dev/null +++ b/Server/src/Controller/ItemsController.php @@ -0,0 +1,35 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Controller; + +use App\Entity\Wishlist; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; + + +final class ItemsController extends AbstractController +{ + + #[Route('/wishlists/{id}/items', name: 'app_wishlist_show')] + public function show(Wishlist $wishlist): Response + { + // Check if user is owner or collaborator + if ($wishlist->getOwner() !== $this->getUser() && !$wishlist->getCollaborators()->contains($this->getUser())) { + throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette liste'); + } + + return $this->render('wishlist/index.html.twig', [ + 'wishlist' => $wishlist, + 'items' => $wishlist->getItems(), + ]); + } + +} diff --git a/Server/src/Controller/LoginFormAuthenticator.php b/Server/src/Controller/LoginFormAuthenticator.php new file mode 100644 index 0000000000000000000000000000000000000000..4b237776e5f3d45e54fc8445a8ad53ab85771920 --- /dev/null +++ b/Server/src/Controller/LoginFormAuthenticator.php @@ -0,0 +1,63 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Controller; + +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\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\SecurityRequestAttributes; +use Symfony\Component\Security\Http\Util\TargetPathTrait; + +class LoginFormAuthenticator extends AbstractLoginFormAuthenticator +{ + use TargetPathTrait; + + public const LOGIN_ROUTE = 'app_login'; + + public function __construct(private UrlGeneratorInterface $urlGenerator) + { + } + + public function authenticate(Request $request): Passport + { + $email = $request->request->get('email', ''); + + $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $email); + + return new Passport( + new UserBadge($email), + new PasswordCredentials($request->request->get('password', '')), + [ + new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')), + ] + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + return new RedirectResponse($targetPath); + } + + // Redirection après connexion réussie + return new RedirectResponse($this->urlGenerator->generate('app_home')); + } + + protected function getLoginUrl(Request $request): string + { + return $this->urlGenerator->generate(self::LOGIN_ROUTE); + } +} \ No newline at end of file diff --git a/Server/src/Controller/ProofController.php b/Server/src/Controller/ProofController.php new file mode 100644 index 0000000000000000000000000000000000000000..cf4827544f3fdfa64cfd7fc6e8b3c4d6258d7843 --- /dev/null +++ b/Server/src/Controller/ProofController.php @@ -0,0 +1,138 @@ +<?php +// src/Controller/ProofController.php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Controller; + +use App\Entity\Item; +use App\Entity\Proof; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\String\Slugger\SluggerInterface; + +class ProofController extends AbstractController +{ + #[Route('/gift/proof/{id}', name: 'gift_proof_form', methods: ['GET'])] + public function showProofForm(int $id, EntityManagerInterface $entityManager): Response + { + // Récupération de l'item en base + $item = $entityManager->getRepository(Item::class)->find($id); + + if (!$item) { + throw new NotFoundHttpException('Item not found'); + } + + // Affichage du formulaire de preuve + return $this->render('proof/proof.html.twig', [ + 'itemId' => $id, + 'item' => $item, + ]); + } + + #[Route('/gift/proof', name: 'upload_proof', methods: ['POST'])] + public function uploadProof(Request $request, SluggerInterface $slugger, EntityManagerInterface $entityManager): Response + { + $itemId = $request->request->get('itemId'); + $item = $entityManager->getRepository(Item::class)->find($itemId); + + if (!$item) { + $this->addFlash('error', 'Invalid item'); + return $this->redirectToRoute('homepage'); // Redirection vers la page d'accueil ou autre + } + + $uploadedFile = $request->files->get('proofFile'); + $congratsMsg = $request->request->get('congratsMsg'); + + if ($uploadedFile) { + $originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = $slugger->slug($originalFilename); + $newFilename = $safeFilename.'-'.uniqid().'.'.$uploadedFile->guessExtension(); + + try { + $uploadedFile->move( + $this->getParameter('proofs_directory'), + $newFilename + ); + + // Met à jour le statut de l'item + $item->setHasPurchased(true); + + // Création et configuration de l'entité Proof + $proof = new Proof(); + $proof->setItem($item); + $proof->setProofImagePath($newFilename); + $proof->setMessage($congratsMsg); + + if ($this->getUser()) { + $proof->setBuyer($this->getUser()); + } + + $entityManager->persist($proof); + $item->setProof($proof); + + $entityManager->flush(); + + $this->addFlash('success', 'Proof uploaded successfully!'); + } catch (\Exception $e) { + $this->addFlash('error', 'Could not upload file. Please try again.'); + return $this->redirectToRoute('gift_proof_form', ['id' => $itemId]); + } + } else { + $this->addFlash('error', 'No file selected. Please choose a file to upload.'); + } + + // Redirection vers le formulaire de preuve (ou une autre page) + return $this->redirectToRoute('gift_proof_form', ['id' => $itemId]); + } + + #[Route('/gift/proof/{id}/edit', name: 'edit_proof', methods: ['GET', 'POST'])] + public function editProof(Request $request, Proof $proof, SluggerInterface $slugger, EntityManagerInterface $entityManager): Response + { + // Vérifie que l'utilisateur connecté est bien le buyer + if (!$this->getUser() || $this->getUser()->getId() !== $proof->getBuyer()?->getId()) { + throw $this->createAccessDeniedException('You are not allowed to edit this proof.'); + } + + if ($request->isMethod('POST')) { + $congratsMsg = $request->request->get('congratsMsg'); + $uploadedFile = $request->files->get('proofFile'); + + if ($uploadedFile) { + $originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = $slugger->slug($originalFilename); + $newFilename = $safeFilename.'-'.uniqid().'.'.$uploadedFile->guessExtension(); + + try { + $uploadedFile->move( + $this->getParameter('proofs_directory'), + $newFilename + ); + $proof->setProofImagePath($newFilename); + } catch (\Exception $e) { + $this->addFlash('error', 'Could not upload file. Please try again.'); + } + } + $proof->setMessage($congratsMsg); + + $entityManager->flush(); + $this->addFlash('success', 'Proof updated successfully!'); + + // Redirection vers le formulaire de preuve de l'item + return $this->redirectToRoute('gift_proof_form', ['id' => $proof->getItem()->getId()]); + } + + // Affichage du formulaire d'édition de la preuve + return $this->render('proof/edit_proof.html.twig', [ + 'proof' => $proof, + 'item' => $proof->getItem(), + ]); + } +} diff --git a/Server/src/Controller/RegistrationController.php b/Server/src/Controller/RegistrationController.php new file mode 100644 index 0000000000000000000000000000000000000000..b414d36d12b378d7c217320fb58acf161ffec9d4 --- /dev/null +++ b/Server/src/Controller/RegistrationController.php @@ -0,0 +1,51 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Controller; + +use App\Entity\User; +use App\FormRegister\RegistrationFormType; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\Routing\Annotation\Route; + +class RegistrationController extends AbstractController +{ + #[Route('/register', name: 'app_register')] + public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response + { + $user = new User(); + $form = $this->createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // encode the plain password + $user->setPassword( + $userPasswordHasher->hashPassword( + $user, + $form->get('password')->getData() + ) + ); + $user->setType('user'); + $user->setLockStatus(false); + + $entityManager->persist($user); + $entityManager->flush(); + + // redirects to login page after registration + return $this->redirectToRoute('app_login'); + } + + return $this->render('registration/register.html.twig', [ + 'registrationForm' => $form->createView(), + ]); + } +} \ No newline at end of file diff --git a/Server/src/Controller/SecurityController.php b/Server/src/Controller/SecurityController.php new file mode 100644 index 0000000000000000000000000000000000000000..133b15a1d35e5705d2e8d51f96ddbafb32c84149 --- /dev/null +++ b/Server/src/Controller/SecurityController.php @@ -0,0 +1,102 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Controller; + +use App\Entity\User; +use App\FormRegister\RegistrationFormType; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; +use Symfony\Component\String\Slugger\SluggerInterface; +class SecurityController extends AbstractController +{ + #[Route('/signup', name: 'app_signup',)] + public function signup(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager, SluggerInterface $slugger): Response + { + // Check if user is already logged in + if ($this->getUser()) { + // Redirect to myWishlist page + return $this->redirectToRoute('app_home'); + } + + $user = new User(); + $form = $this->createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // Handle profile photo upload + /* + $profilePhotoFile = $form->get('profilePhoto')->getData(); + if ($profilePhotoFile) { + $originalFilename = pathinfo($profilePhotoFile->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = $slugger->slug($originalFilename); + $newFilename = $safeFilename.'-'.uniqid().'.'.$profilePhotoFile->guessExtension(); + + try { + $profilePhotoFile->move( + $this->getParameter('profile_photos_directory'), + $newFilename + ); + $user->setPhoto($newFilename); + } catch (\Exception $e) { + // Handle exception if file upload fails + } + }*/ + // Encode the plain password + $user->setPassword( + $userPasswordHasher->hashPassword( + $user, + $form['plainPassword']['first']->getData()) + ); + $user->setEmail($form['email']->getData()); + $user->setLockStatus(false); + $user->setType('user'); + $user->setUsername($form['username']->getData()); + $user->setFirstName($form['firstName']->getData()); + $user->setLastName($form['lastName']->getData()); + $entityManager->persist($user); + $entityManager->flush(); + + $this->addFlash('success', 'Votre compte a été créé avec succès'); + return $this->redirectToRoute('app_login'); + } + + return $this->render('security/signup.html.twig', [ + 'registrationForm' => $form->createView(), + ]); + } + #[Route(path: '/login', name: 'app_login')] + public function login(AuthenticationUtils $authenticationUtils): Response + { + // Check if user is already logged in + if ($this->getUser()) { + // Redirect to myWishlist page + return $this->redirectToRoute('app_home'); + } + $error = $authenticationUtils->getLastAuthenticationError(); + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error + ]); + } + + #[Route('/logout', name: 'app_logout')] + public function logout(): void + { + // This method can be empty - it will be intercepted by the logout key on your firewall + // The actual logout logic is handled by Symfony's security system + throw new \LogicException(message: 'This method can be blank - it will be intercepted by the logout key on your firewall.'); + } +} \ No newline at end of file diff --git a/Server/src/Controller/WishlistController.php b/Server/src/Controller/WishlistController.php new file mode 100644 index 0000000000000000000000000000000000000000..4c7bcd013a75ad92c9f55dc6c899d1cc9e6d93cf --- /dev/null +++ b/Server/src/Controller/WishlistController.php @@ -0,0 +1,223 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Controller; + +use App\Entity\Wishlist; +use App\Entity\Item; +use App\Entity\User; +use App\Entity\Invitation; // Ajout de l'import de l'entité Invitation +use App\Repository\ItemRepository; +use App\Form\ItemType; +use App\ItemForm\WishlistType; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Http\Attribute\IsGranted; +use Symfony\Component\HttpFoundation\JsonResponse; + +#[Route('/myWishlist')] +class WishlistController extends AbstractController +{ + #[IsGranted('ROLE_USER')] + #[Route('/create', name: 'app_wishlist_create')] + public function create(Request $request, EntityManagerInterface $entityManager): Response + { + $wishlist = new Wishlist(); + $this->denyAccessUnlessGranted('ADD', $wishlist); + + $wishlist->setOwner($this->getUser()); + $wishlist->setPublicToken(); + $wishlist->setCollaborationToken(); + + // Création du formulaire (le reste des champs est géré par votre FormType) + $form = $this->createForm(WishlistType::class, $wishlist); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // Récupérer le champ caché qui contient les usernames + $collaboratorsInput = $request->request->get('collaborators_hidden', ''); + if (!empty($collaboratorsInput)) { + $usernames = array_map('trim', explode(',', $collaboratorsInput)); + foreach ($usernames as $username) { + if (!empty($username)) { + $user = $entityManager->getRepository(\App\Entity\User::class) + ->findOneBy(['username' => $username]); + if ($user) { + // Ancien code pour ajouter directement le collaborateur : + // $wishlist->addCollaborator($user); + // $entityManager->flush(); + + // Nouveau code : envoyer une invitation + $invitation = new Invitation($wishlist, $this->getUser(), $user); + $entityManager->persist($invitation); + } + } + } + } + + $entityManager->persist($wishlist); + $entityManager->flush(); + + $this->addFlash('success', 'La liste de souhaits a été créée avec succès.'); + return $this->redirectToRoute('app_wishlists_index'); + } + + return $this->render('wishlist/create.html.twig', [ + 'form' => $form->createView(), + ]); + } + + #[Route('/token', name: 'app_wishlist_public')] + public function public(Request $request, EntityManagerInterface $entityManager): Response + { + $token = $request->query->get('token'); + $wishlist = $entityManager->getRepository(Wishlist::class)->findOneBy(['publicToken' => $token]); + + if (!$wishlist) { + throw $this->createNotFoundException('La liste de souhaits n\'existe pas'); + } + $user = $this->getUser(); + + return $this->render('wishlist/index.html.twig', [ + 'wishlist' => $wishlist, + 'items' => $wishlist->getItems(), + 'user' => $user, + 'aim' => 'toBuy' + ]); + } + + #[Route('/check-username', name: 'app_check_username', methods: ['POST'])] + public function checkUsername(Request $request, EntityManagerInterface $entityManager): JsonResponse + { + $username = $request->request->get('username'); + if (!$username) { + return $this->json(['success' => false, 'message' => 'Aucun username reçu'], 400); + } + $user = $entityManager->getRepository(User::class)->findOneBy(['username' => $username]); + if ($user) { + return $this->json(['exists' => true, 'username' => $username]); + } else { + return $this->json(['exists' => false, 'message' => 'Utilisateur non trouvé'], 404); + } + } + + #[IsGranted('ROLE_USER')] + #[Route('/{id}', name: 'app_wishlist_show', requirements: ['id' => '\d+'])] + public function show(Wishlist $wishlist, Request $request, ItemRepository $itemRepository): Response + { + // Check if user is owner or collaborator + if ($wishlist->getOwner() !== $this->getUser() && !$wishlist->getCollaborators()->contains($this->getUser())) { + throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette liste'); + } + + // Get the 'sort' query parameter (default to ascending order) + $sortParam = $request->query->get('sort', 'asc'); + $sortOrder = ($sortParam === 'desc') ? 'DESC' : 'ASC'; + + // Fetch items for this wishlist, sorted by price + $items = $itemRepository->findBy( + ['wishlist' => $wishlist], + ['price' => $sortOrder] + ); + + return $this->render('wishlist/index.html.twig', [ + 'wishlist' => $wishlist, + // 'items' => $wishlist->getItems(), + 'items' => $items, + ]); + } + + + #[IsGranted('ROLE_USER')] + #[Route('/{id}/deleteWishlist', name: 'app_wishlist_delete_wishlist', methods: ['POST'])] + public function deleteWishlist(Request $request, Wishlist $wishlist, EntityManagerInterface $entityManager): Response + { + $this->denyAccessUnlessGranted('DELETE', $wishlist); + + // Check if user is the owner + if ($wishlist->getOwner() !== $this->getUser() && $wishlist->getCollaborators()->contains($this->getUser())) { + throw $this->createAccessDeniedException('Vous n\'êtes pas autorisé à supprimer cette liste'); + } + + if ($this->isCsrfTokenValid('delete' . $wishlist->getId(), $request->request->get('_token'))) { + $entityManager->remove($wishlist); + $entityManager->flush(); + + $this->addFlash('success', 'Votre liste de souhaits a été supprimée'); + } + + return $this->redirectToRoute('app_wishlists_index'); + } + + #[Route('/{wishlistId}/goToOfficialWebsite/{itemId}', name: 'app_wishlist_go_to_official_website')] + public function goToOfficialWebsite(int $wishlistId, int $itemId, EntityManagerInterface $entityManager): RedirectResponse + { + $wishlist = $entityManager->getRepository(Wishlist::class)->find($wishlistId); + $item = $entityManager->getRepository(Item::class)->find($itemId); + + if (!$wishlist || !$item || $item->getWishlist() !== $wishlist) { + throw $this->createNotFoundException('L\'élément ou la liste de souhaits n\'existe pas'); + } + + // Check if user is owner or collaborator + if ($wishlist->getOwner() !== $this->getUser() && !$wishlist->getCollaborators()->contains($this->getUser())) { + throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette liste'); + } + + $url = $item->getUrl(); + + if (!$url) { + $this->addFlash('error', 'Cet élément n\'a pas d\'URL définie'); + return $this->redirectToRoute('app_wishlist_show', ['id' => $wishlistId]); + } + + // Ensure URL has protocol + if (!preg_match("~^(?:f|ht)tps?://~i", $url)) { + $url = "https://" . $url; + } + + return new RedirectResponse($url); + } + + #[IsGranted('ROLE_USER')] + #[Route('/wishlist/{id}/share', name: 'wishlist_share')] + public function share(Wishlist $wishlist): Response + { + $this->denyAccessUnlessGranted('SHARE', $wishlist); + + // Check if current user is owner + if ($wishlist->getOwner() !== $this->getUser()) { + $this->addFlash('error', 'Vous n\'avez pas l\'autorisation de partager cette liste.'); + return $this->redirectToRoute('app_wishlist_show', ['id' => $wishlist->getId()]); + } + + // Generate URLs + $collaborationUrl = $this->generateUrl( + 'wishlist_collaborate', + ['token' => $wishlist->getCollaborationToken()], + UrlGeneratorInterface::ABSOLUTE_URL + ); + + $publicUrl = $this->generateUrl( + 'ViewUserWishlist', + ['token' => $wishlist->getPublicToken()], + UrlGeneratorInterface::ABSOLUTE_URL + ); + + return $this->render('wishlist/share.html.twig', [ + 'wishlist' => $wishlist, + 'collaborationUrl' => $collaborationUrl, + 'publicUrl' => $publicUrl + ]); + } +} diff --git a/Server/src/Controller/WishlistShareController.php b/Server/src/Controller/WishlistShareController.php new file mode 100644 index 0000000000000000000000000000000000000000..021758400d8a58ada4f26d514e16fef14f70347d --- /dev/null +++ b/Server/src/Controller/WishlistShareController.php @@ -0,0 +1,69 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Controller; + +use App\Entity\Wishlist; +use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; + +#[Route('/wishlist')] +class WishlistShareController extends AbstractController +{ + #[IsGranted('ROLE_USER')] + #[Route('/{id}/add-collaborator', name: 'wishlist_add_collaborator', methods: ['POST'])] + public function addCollaborator(Wishlist $wishlist, Request $request, EntityManagerInterface $em): JsonResponse + { + // Vérifier que l'utilisateur est propriétaire ou admin + if ($wishlist->getOwner() !== $this->getUser() && !$this->isGranted('ROLE_ADMIN')) { + return $this->json([ + 'success' => false, + 'message' => 'Vous n\'avez pas l\'autorisation de partager cette liste.' + ], 403); + } + + // Récupérer le username + $username = $request->request->get('username'); + if (!$username) { + return $this->json([ + 'success' => false, + 'message' => 'Aucun username reçu.' + ], 400); + } + + // Vérifier si l'utilisateur existe + $user = $em->getRepository(User::class)->findOneBy(['username' => $username]); + if (!$user) { + return $this->json([ + 'success' => false, + 'message' => 'L\'utilisateur "' . $username . '" n\'existe pas.' + ], 404); + } + + // Vérifier si déjà collaborateur + if ($wishlist->getCollaborators()->contains($user)) { + return $this->json([ + 'success' => false, + 'message' => 'Cet utilisateur est déjà collaborateur.' + ], 400); + } + + // Ajouter le collaborateur (relation ManyToMany) + $wishlist->addCollaborator($user); + $em->flush(); + + return $this->json([ + 'success' => true, + 'message' => 'Collaborateur ajouté avec succès.' + ]); + } +} diff --git a/Server/src/Controller/WishlistsController.php b/Server/src/Controller/WishlistsController.php new file mode 100644 index 0000000000000000000000000000000000000000..cc0aa90ce3b0456c9173fda3e5339607337c2809 --- /dev/null +++ b/Server/src/Controller/WishlistsController.php @@ -0,0 +1,296 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Controller; + +use App\Entity\Wishlist; +use App\Entity\Invitation; +use App\ItemForm\WishlistType; +use App\Repository\UserRepository; +use App\Entity\User; +use App\Repository\WishlistRepository; +use App\Repository\InvitationRepository; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\String\Slugger\SluggerInterface; +use Doctrine\Common\Collections\ArrayCollection; +use Symfony\Component\Form\FormError; + +#[Route('/myWishlists')] +#[IsGranted('ROLE_USER')] +class WishlistsController extends AbstractController +{ + #[Route('/', name: 'app_wishlists_index')] + public function index( + WishlistRepository $wishlistRepository, + UserRepository $userRepository, + InvitationRepository $invitationRepository + ): Response { + // Récupérer l'utilisateur connecté + $user = $this->getUser(); + + // Si aucun utilisateur n'est connecté, on affiche une page basique ou on redirige + if (!$user) { + return $this->render('security/login.html.twig'); + } + + $users = $userRepository->findAll(); + + // Récupérer toutes les wishlists dont l'utilisateur est propriétaire + $wishlists = $wishlistRepository->findBy(['owner' => $user]); + $wishlistsIamCollaborator = $wishlistRepository->findCollaboratorWishlists($user); + + // Récupérer les invitations où l'utilisateur est destinataire + $invitations = $invitationRepository->findBy(['receiver' => $user]); + + return $this->render('wishlists/index.html.twig', [ + 'wishlists' => $wishlists, + 'wishlistsIamCollaborator' => $wishlistsIamCollaborator, + 'users' => $users, + 'invitations' => $invitations, + ]); + } + + #[Route('/create', name: 'app_wishlists_create')] + public function create(Request $request, EntityManagerInterface $entityManager): Response + { + $wishlist = new Wishlist(); + $this->denyAccessUnlessGranted('ADD', $wishlist); + + $wishlist->setOwner($this->getUser()); + $wishlist->setPublicToken(); + $wishlist->setCollaborationToken(); + + $form = $this->createForm(WishlistType::class, $wishlist); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // Validate deadline: must be strictly greater than today if set + if ($wishlist->getDeadline() !== null && $wishlist->getDeadline() <= new \DateTime('today')) { + $form->get('deadline')->addError(new FormError('La date limite doit être supérieure à la date du jour.')); + } else { + // Process collaborators from the hidden input field + $collaboratorsInput = $request->request->get('collaborators_hidden', ''); + if (!empty($collaboratorsInput)) { + $usernames = array_filter(array_map('trim', explode(',', $collaboratorsInput))); + foreach ($usernames as $username) { + if (!empty($username)) { + $user = $entityManager->getRepository(User::class)->findOneBy(['username' => $username]); + if ($user) { + // Ancien code pour ajouter directement le collaborateur : + // $wishlist->addCollaborator($user); + + // Nouveau code : envoyer une invitation + $invitation = new Invitation($wishlist, $this->getUser(), $user); + $entityManager->persist($invitation); + } else { + // Optionnel : ajouter un flash ou logger si l'utilisateur n'est pas trouvé + $this->addFlash('error', "L'utilisateur '{$username}' n'a pas été trouvé."); + } + } + } + } + $entityManager->persist($wishlist); + $entityManager->flush(); + return $this->redirectToRoute('app_wishlists_index'); + } + } + + return $this->render('wishlists/create.html.twig', [ + 'form' => $form->createView(), + ]); + } + + #[Route('/{id}/edit', name: 'app_wishlists_edit')] + public function edit(Request $request, Wishlist $wishlist, EntityManagerInterface $entityManager): Response + { + $this->denyAccessUnlessGranted('EDIT', $wishlist); + + // Allow access if the user is either the owner or is already a collaborator + if ($wishlist->getOwner() !== $this->getUser() && !$wishlist->getCollaborators()->contains($this->getUser())) { + throw $this->createAccessDeniedException('Vous n\'êtes pas autorisé à modifier cette liste'); + } + + // Pass the option to include collaborators field in the form. + $form = $this->createForm(WishlistType::class, $wishlist, [ + 'include_collaborators' => true + ]); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // Process collaborators from the hidden field + $collaboratorsInput = $request->request->get('collaborators_hidden', ''); + if (!empty($collaboratorsInput)) { + $usernames = array_filter(array_map('trim', explode(',', $collaboratorsInput))); + + // Ancien code pour mettre à jour les collaborateurs directement : + /* + // Remove collaborators that are no longer in the hidden field + foreach ($wishlist->getCollaborators() as $collaborator) { + if (!in_array($collaborator->getUsername(), $usernames)) { + $wishlist->removeCollaborator($collaborator); + } + } + // Add new collaborators not already in the collection + foreach ($usernames as $username) { + $user = $entityManager->getRepository(User::class)->findOneBy(['username' => $username]); + if ($user && !$wishlist->getCollaborators()->contains($user)) { + $wishlist->addCollaborator($user); + } + } + */ + + // Nouveau code : envoyer une invitation pour chaque username non déjà collaborateur + foreach ($usernames as $username) { + $user = $entityManager->getRepository(User::class)->findOneBy(['username' => $username]); + if ($user) { + // On peut vérifier ici si une invitation existe déjà ou si l'utilisateur est déjà collaborateur + if ( + !$wishlist->getCollaborators()->contains($user) + // Vous pouvez ajouter ici une vérification d'existence d'invitation non acceptée si nécessaire + ) { + $invitation = new Invitation($wishlist, $this->getUser(), $user); + $entityManager->persist($invitation); + } + } else { + $this->addFlash('error', "L'utilisateur '{$username}' n'a pas été trouvé."); + } + } + } else { + // Si le champ caché est vide, vous pouvez décider de ne rien faire ou gérer la suppression d'invitations/collaborateurs + // Ancien code pour supprimer tous les collaborateurs : + /* + foreach ($wishlist->getCollaborators() as $collaborator) { + $wishlist->removeCollaborator($collaborator); + } + */ + } + + $entityManager->flush(); + return $this->redirectToRoute('app_wishlists_index'); + } + + return $this->render('wishlists/edit.html.twig', [ + 'form' => $form->createView(), + 'wishlist' => $wishlist, + ]); + } + + #[Route('/{id}/delete', name: 'app_wishlists_delete', methods: ['POST'])] + public function delete(Request $request, Wishlist $wishlist, EntityManagerInterface $entityManager): Response + { + $this->denyAccessUnlessGranted('DELETE', $wishlist); + + if ($wishlist->getOwner() !== $this->getUser()) { + throw $this->createAccessDeniedException('Vous n\'êtes pas autorisé à supprimer cette liste'); + } + + if ($this->isCsrfTokenValid('delete' . $wishlist->getId(), $request->request->get('_token'))) { + $entityManager->remove($wishlist); + $entityManager->flush(); + } + + return $this->redirectToRoute('app_wishlists_index'); + } + + #[Route('/{id}/getUrl', name: 'app_wishlists_get_url')] + public function getUrl(Wishlist $wishlist, SluggerInterface $slugger): Response + { + if ($wishlist->getOwner() !== $this->getUser() && !$wishlist->getCollaborators()->contains($this->getUser())) { + throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette liste'); + } + + $slug = $slugger->slug($wishlist->getName() . '-' . $wishlist->getId()); + $url = $this->generateUrl('app_wishlist_show', ['id' => $wishlist->getId(), 'slug' => $slug], true); + + return $this->render('wishlists/get_url.html.twig', [ + 'wishlist' => $wishlist, + 'url' => $url, + ]); + } + + #[Route('/store', name: 'app_wishlists_store', methods: ['POST'])] + public function store(Request $request, EntityManagerInterface $entityManager): JsonResponse + { + $data = json_decode($request->getContent(), true); + + $wishlist = new Wishlist(); + $wishlist->setName($data['name']); + $wishlist->setOwner($this->getUser()); + + if (isset($data['deadline'])) { + $deadline = new \DateTime($data['deadline']); + if ($deadline <= new \DateTime('today')) { + return $this->json([ + 'success' => false, + 'message' => 'La date limite doit être supérieure à la date du jour.' + ], Response::HTTP_BAD_REQUEST); + } + $wishlist->setDeadline($deadline); + } + + $entityManager->persist($wishlist); + $entityManager->flush(); + + return $this->json([ + 'success' => true, + 'id' => $wishlist->getId(), + 'message' => 'Liste de souhaits créée avec succès' + ]); + } + + #[Route('/{id}/share', name: 'wishlist_share', methods: ['POST'])] + public function shareWishlist(Request $request, Wishlist $wishlist, EntityManagerInterface $entityManager): Response + { + $this->denyAccessUnlessGranted('SHARE', $wishlist); + + // Vérifier que l'utilisateur connecté est bien le propriétaire + if ($wishlist->getOwner() !== $this->getUser()) { + $this->addFlash('error', 'Vous n\'avez pas l\'autorisation de partager cette liste.'); + return $this->redirectToRoute('app_wishlists_show', ['id' => $wishlist->getId()]); + } + + // Récupérer les noms d'utilisateur soumis + // On s'attend à recevoir un tableau de strings (les usernames) + $usernames = $request->request->get('usernames', []); + if (!is_array($usernames)) { + // Si c'est une seule chaîne, on le transforme en tableau + $usernames = [$usernames]; + } + + $invalidUsernames = []; + foreach ($usernames as $username) { + $username = trim($username); + if (empty($username)) { + continue; + } + $user = $entityManager->getRepository(User::class)->findOneBy(['username' => $username]); + if (!$user) { + $invalidUsernames[] = $username; + } else { + // Ajouter une invitation pour l'utilisateur trouvé + $user->addInvitation(new Invitation($wishlist, $this->getUser(), $user)); + } + } + + if (count($invalidUsernames) > 0) { + $this->addFlash('error', 'Les noms d\'utilisateur suivants n\'existent pas : ' . implode(', ', $invalidUsernames)); + return $this->redirectToRoute('app_wishlists_show', ['id' => $wishlist->getId()]); + } + + $entityManager->flush(); + + $this->addFlash('success', 'La liste de souhaits a été partagée avec succès.'); + return $this->redirectToRoute('app_wishlists_show', ['id' => $wishlist->getId()]); + } +} diff --git a/Server/src/Entity/.gitignore b/Server/src/Entity/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/Server/src/Entity/Invitation.php b/Server/src/Entity/Invitation.php new file mode 100644 index 0000000000000000000000000000000000000000..7e6480bf07ea36164da766a484677b8392ededfe --- /dev/null +++ b/Server/src/Entity/Invitation.php @@ -0,0 +1,114 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use App\Repository\InvitationRepository; +use App\Entity\User; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity(repositoryClass: InvitationRepository::class)] +#[ORM\Table(name: '`invitation`')] +class Invitation +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\ManyToOne(targetEntity: Wishlist::class)] + #[ORM\JoinColumn(nullable: false)] + private ?Wishlist $wishlist = null; + + + #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\JoinColumn(nullable: false)] + private ?User $sender = null; + + #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'myInvitations')] + private ?User $receiver = null; + + // Tant que l'utilisateur n'a pas accepté l'invitation, il n'est pas collaborateur et accepted est null + // Quand l'utilisateur accepte l'invitation, il devient collaborateur et accepted est true + // Quand l'utilisateur refuse l'invitation, il n'est pas collaborateur et accepted est false + #[ORM\Column(type: "boolean", nullable: true)] + private ?bool $accepted = null; + + + public function __construct(?Wishlist $wishlist, ?User $sender, ?User $receiver) + { + if($receiver == null){ + throw new \Exception("Receiver can't be null"); + } + $this->wishlist = $wishlist; + $this->sender = $sender; + $this->receiver = $receiver; + $this->accepted = null; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getWishlist(): ?Wishlist + { + return $this->wishlist; + } + + public function setWishlist(?Wishlist $wishlist): static + { + $this->wishlist = $wishlist; + + return $this; + } + + public function getSender(): ?User + { + return $this->sender; + } + + public function setSender(?User $sender): static + { + $this->sender = $sender; + + return $this; + } + + public function isAccepted(): ?bool + { + return $this->accepted; + } + + public function setAccepted(?bool $accepted): static + { + $this->accepted = $accepted; + if ($accepted) { + $this->receiver->addCollaborativeWishlist($this->wishlist); + } + + return $this; + } + + public function getReceiver(): ?User + { + return $this->receiver; + } + + public function setReceiver(?User $receiver): static + { + if($receiver == null){ + throw new \Exception("Receiver can't be null"); + } + $this->receiver = $receiver; + + return $this; + } + +} diff --git a/Server/src/Entity/Item.php b/Server/src/Entity/Item.php new file mode 100644 index 0000000000000000000000000000000000000000..6b94b06f71b956d055f9dbd951c5148a948b477e --- /dev/null +++ b/Server/src/Entity/Item.php @@ -0,0 +1,153 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Entity; + +use App\Repository\ItemRepository; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; + +#[ORM\Entity(repositoryClass: ItemRepository::class)] +class Item +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Assert\NotBlank(message: 'Le nom est obligatoire')] + private ?string $name = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $description = null; + + #[ORM\Column] + #[Assert\PositiveOrZero(message: 'Le prix doit être positif ou nul')] + private ?float $price = null; + + #[ORM\Column] + private ?bool $hasPurchased = false; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $url = null; + + #[ORM\ManyToOne(inversedBy: 'items')] + #[ORM\JoinColumn(nullable: false)] + private ?Wishlist $wishlist = null; + + #[ORM\OneToOne(mappedBy: 'item', targetEntity: Proof::class, cascade: ['persist', 'remove'])] + private ?Proof $proof = null; + + + public function __construct(string $name = null, string $description = null, float $price = null, string $url = null) + { + $this->name = $name; + $this->description = $description; + $this->price = $price; + $this->url = $url; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): self + { + $this->description = $description; + return $this; + } + + public function getPrice(): ?float + { + return $this->price; + } + + public function setPrice(float $price): self + { + $this->price = $price; + return $this; + } + + public function getHasPurchased(): ?bool + { + return $this->hasPurchased; + } + + public function setHasPurchased(bool $hasPurchased): self + { + $this->hasPurchased = $hasPurchased; + return $this; + } + + public function getUrl(): ?string + { + return $this->url; + } + + public function setUrl(?string $url): self + { + $this->url = $url; + return $this; + } + + public function getWishlist(): ?Wishlist + { + return $this->wishlist; + } + + public function setWishlist(?Wishlist $wishlist): self + { + $this->wishlist = $wishlist; + return $this; + } + + public function hasPurchased(): ?bool + { + return $this->hasPurchased; + } + + public function getProof(): ?Proof + { + return $this->proof; + } + + public function setProof(?Proof $proof): self + { + // unset the owning side of the relation if necessary + if ($proof === null && $this->proof !== null) { + $this->proof->setItem(null); + } + + // set the owning side of the relation if necessary + if ($proof !== null && $proof->getItem() !== $this) { + $proof->setItem($this); + } + + $this->proof = $proof; + + return $this; + } +} \ No newline at end of file diff --git a/Server/src/Entity/Proof.php b/Server/src/Entity/Proof.php new file mode 100644 index 0000000000000000000000000000000000000000..7dc3f45ac5f8e40c093b2957f313983e31c5ce77 --- /dev/null +++ b/Server/src/Entity/Proof.php @@ -0,0 +1,117 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Entity; + +use App\Repository\ProofRepository; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity(repositoryClass: ProofRepository::class)] +class Proof +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\Column(length: 255)] + private ?string $congratsMessage = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $proofImagePath = null; + + #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\JoinColumn(nullable: true)] + private ?User $buyer = null; + + #[ORM\ManyToOne(targetEntity: Item::class, inversedBy: 'proofs')] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] + private ?Item $item = null; + + #[ORM\Column] + private ?\DateTimeImmutable $createdAt = null; + + public function __construct(string $message = null, string $proofImagePath = null, User $buyer = null, Item $item = null) + { + $this->congratsMessage = $message; + $this->proofImagePath = $proofImagePath; + $this->buyer = $buyer; + $this->item = $item; + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getProofImagePath(): ?string + { + return $this->proofImagePath; + } + + public function setProofImagePath(?string $proofImagePath): self + { + $this->proofImagePath = $proofImagePath; + return $this; + } + + public function getMessage(): ?string + { + return $this->congratsMessage; + } + + public function setMessage(?string $message): self + { + $this->congratsMessage = $message; + return $this; + } + + public function getBuyer(): ?User + { + return $this->buyer; + } + + public function setBuyer(?User $buyer): self + { + $this->buyer = $buyer; + return $this; + } + + public function getCongratsMessage(): ?string + { + return $this->congratsMessage; + } + + public function setCongratsMessage(string $congratsMessage): static + { + $this->congratsMessage = $congratsMessage; + return $this; + } + + public function getItem(): ?Item + { + return $this->item; + } + + public function setItem(?Item $item): self + { + $this->item = $item; + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): self + { + $this->createdAt = $createdAt; + return $this; + } +} \ No newline at end of file diff --git a/Server/src/Entity/Test.php b/Server/src/Entity/Test.php new file mode 100644 index 0000000000000000000000000000000000000000..79bf66b8e06d457b8a2795728ba79f57498b1572 --- /dev/null +++ b/Server/src/Entity/Test.php @@ -0,0 +1,25 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Entity; + +use App\Repository\TestRepository; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity(repositoryClass: TestRepository::class)] +class Test +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/Server/src/Entity/User.php b/Server/src/Entity/User.php new file mode 100644 index 0000000000000000000000000000000000000000..a8e6d875fd51dd019fffa2ad595059f46c4f1f25 --- /dev/null +++ b/Server/src/Entity/User.php @@ -0,0 +1,319 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Entity; + +use App\Interfaces\MyWishlistsListPage; +use App\Repository\UserRepository; +use App\Interfaces\MyInvitationPage; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use App\Entity\Wishlist; + + +#[ORM\Entity(repositoryClass: UserRepository::class)] +#[ORM\Table(name: '`user`')] +#[UniqueEntity(fields: ['username'], message: 'Le nom d\'utilisateur est déjà pris.')] +#[UniqueEntity(fields: ['email'], message: 'Cet email est déjà utilisé.')] +class User implements UserInterface, PasswordAuthenticatedUserInterface, MyInvitationPage, MyWishlistsListPage +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\Column(length: 50)] + #[Assert\NotBlank(message: 'Le prénom est obligatoire')] + private ?string $firstName = null; + + #[ORM\Column(length: 50)] + #[Assert\NotBlank(message: 'Le nom est obligatoire')] + private ?string $lastName = null; + + #[ORM\Column(length: 50, unique: true)] + #[Assert\NotBlank(message: 'Le nom d\'utilisateur est obligatoire')] + private ?string $username = null; + + #[ORM\Column(length: 180, unique: true)] + #[Assert\NotBlank(message: 'L\'email est obligatoire')] + #[Assert\Email(message: 'L\'email {{ value }} n\'est pas valide')] + private ?string $email = null; + + #[ORM\Column(length: 255)] + private ?string $password = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $photo = null; + + #[ORM\Column] + private ?bool $lockStatus = false; + + #[ORM\Column(type: 'string', length: 20)] + private ?string $type = 'user'; + + #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Wishlist::class, orphanRemoval: true)] + private Collection $ownedWishlists; + + #[ORM\ManyToMany(targetEntity: Wishlist::class, mappedBy: 'collaborators')] + private Collection $collaborativeWishlists; + + + private Collection $myInvitations; + + // #[ORM\OneToMany(mappedBy: 'recipient', targetEntity: Notification::class, orphanRemoval: true)] + // private Collection $receivedNotifications; + + public function __construct() + { + $this->ownedWishlists = new ArrayCollection(); + $this->collaborativeWishlists = new ArrayCollection(); + $this->myInvitations = new ArrayCollection(); + $this->receivedNotifications = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): static + { + $this->firstName = $firstName; + return $this; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(string $lastName): static + { + $this->lastName = $lastName; + return $this; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(string $username): static + { + $this->username = $username; + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + return $this; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + return $this; + } + + public function getPhoto(): ?string + { + return $this->photo; + } + + public function setPhoto(?string $photo): static + { + $this->photo = $photo; + return $this; + } + + public function isLockStatus(): ?bool + { + return $this->lockStatus; + } + + public function setLockStatus(bool $lockStatus): static + { + $this->lockStatus = $lockStatus; + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + return $this; + } + + // public function getReceivedNotifications(): Collection + // { + // return $this->receivedNotifications; + // } + + // public function addReceivedNotification(Notification $notification): self + // { + // if (!$this->receivedNotifications->contains($notification)) { + // $this->receivedNotifications->add($notification); + // $notification->setRecipient($this); + // } + // return $this; + // } + + // public function setReceivedNotifications(Collection $notifications): self + // { + // $this->receivedNotifications = $notifications; + // return $this; + // } + + /** + * @return Collection<int, Wishlist> + */ + public function getOwnedWishlists(): Collection + { + return $this->ownedWishlists; + } + + public function addOwnedWishlist(Wishlist $wishlist): self + { + if (!$this->ownedWishlists->contains($wishlist)) { + $this->ownedWishlists->add($wishlist); + $wishlist->setOwner($this); + } + return $this; + } + + public function removeOwnedWishlist(Wishlist $wishlist): self + { + if ($this->ownedWishlists->removeElement($wishlist)) { + if ($wishlist->getOwner() === $this) { + $wishlist->setOwner(null); + } + } + return $this; + } + + /** + * @return Collection<int, Wishlist> + */ + public function getCollaborativeWishlists(): Collection + { + return $this->collaborativeWishlists; + } + + public function addCollaborativeWishlist(Wishlist $wishlist): self + { + if (!$this->collaborativeWishlists->contains($wishlist)) { + $this->collaborativeWishlists->add($wishlist); + $wishlist->addCollaborator($this); + } + return $this; + } + + public function removeCollaborativeWishlist(Wishlist $wishlist): self + { + if ($this->collaborativeWishlists->removeElement($wishlist)) { + $wishlist->removeCollaborator($this); + } + return $this; + } + + /** + * @return Collection<int, Invitation> + */ + public function getMyInvitations(): Collection + { + return $this->myInvitations; + } + + public function addInvitation(Invitation $invitation): self + { + if (!$this->myInvitations->contains($invitation)) { + $this->myInvitations->add($invitation); + $invitation->setReceiver($this); + } + return $this; + } + + // Required methods for UserInterface + public function getRoles(): array + { + $roles = ['ROLE_USER']; + if ($this->type === 'admin') { + $roles[] = 'ROLE_ADMIN'; + } + return array_unique($roles); + } + + public function eraseCredentials(): void + { + // Clear any temporary, sensitive data if stored + } + + public function getUserIdentifier(): string + { + return $this->email; + } + + // MyInvitationPage interface methods + public function acceptInvitation(?Invitation $invitation): void + { + $invitation->setAccepted(true); + } + + // MyWishlistListPage interface methods + public function createWishlist(string $title, ?\DateTimeInterface $deadline): ?Wishlist + { + $wishlist = new Wishlist($title, $deadline); + $wishlist->setName($title); + $wishlist->setDeadline($deadline); + $wishlist->setOwner($this); + $this->addOwnedWishlist($wishlist); + return $wishlist; + } + + public function deleteWishlist(?Wishlist $wishlist): void + { + $this->removeOwnedWishlist($wishlist); + } + + public function shareWishlist(?Wishlist $wishlist, Collection $users): void + { + foreach ($users as $user) { + $wishlist->addCollaborator($user); + } + } + + public function displayWishlist(?Wishlist $wishlist, Collection $users): void + { + // To be implemented! + } +} \ No newline at end of file diff --git a/Server/src/Entity/Wishlist.php b/Server/src/Entity/Wishlist.php new file mode 100644 index 0000000000000000000000000000000000000000..7e840cd611cc581a3ae17011cebab3621f28b2a0 --- /dev/null +++ b/Server/src/Entity/Wishlist.php @@ -0,0 +1,232 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Entity; + +use App\Repository\WishlistRepository; +use DateTimeInterface; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use MyWishlistPage; +use Symfony\Component\Validator\Constraints as Assert; +use ViewUserWishlist; +use App\Entity\Item; +use Symfony\Component\Uid\Uuid; + +#[ORM\Entity(repositoryClass: WishlistRepository::class)] +class Wishlist implements \App\Interfaces\ViewUserWishlist, \App\Interfaces\MyWishlistPage +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Assert\NotBlank(message: 'Le nom est obligatoire')] + private ?string $name = null; + + #[ORM\Column(type: Types::DATE_MUTABLE, nullable: true)] + private ?\DateTimeInterface $deadline = null; + + #[ORM\OneToMany(mappedBy: 'wishlist', targetEntity: Item::class, orphanRemoval: true)] + private Collection $items; + + #[ORM\ManyToOne(inversedBy: 'ownedWishlists')] + #[ORM\JoinColumn(nullable: false)] + private ?User $owner = null; + + #[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'collaborativeWishlists')] + #[ORM\JoinTable(name: 'wishlist_user')] + private Collection $collaborators; + + #[ORM\Column(length: 36, unique: true, nullable: true ,name: 'collaboration_token')] + private ?string $collaborationToken = null; + + #[ORM\Column(length: 36, unique: true, nullable: true, name:'public_token')] + private ?string $publicToken = null; + + public function getCollaborationToken(): ?string + { + if ($this->collaborationToken === null) { + $this->collaborationToken = Uuid::v4()->toRfc4122(); + } + return $this->collaborationToken; + } + public function refreshCollaborationToken(): self + { + $this->collaborationToken = Uuid::v4()->toRfc4122(); + return $this; + } + + public function getPublicToken(): ?string + { + if ($this->publicToken === null) { + $this->publicToken = Uuid::v4()->toRfc4122(); + } + return $this->publicToken; + } + public function setPublicToken(): void + { + if ($this->publicToken === null) { + $this->publicToken = Uuid::v4()->toRfc4122(); + } + } + public function setCollaborationToken(){ + if ($this->collaborationToken === null) { + $this->collaborationToken = Uuid::v4()->toRfc4122(); + } + } + + public function refreshPublicToken(): self + { + $this->publicToken = Uuid::v4()->toRfc4122(); + return $this; + } + + public function isExpired(): bool + { + if ($this->deadline === null) { + return false; + } + + return $this->deadline < new \DateTime(); + } +// composer require symfony/uid + public function __construct(?string $name=null, ?DateTimeInterface $deadline=null ) + { + $this->name = $name; + $this->deadline = $deadline; + $this->items = new ArrayCollection(); + $this->collaborators = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + return $this; + } + + public function getDeadline(): ?\DateTimeInterface + { + return $this->deadline; + } + + public function setDeadline(?\DateTimeInterface $deadline): self + { + $this->deadline = $deadline; + return $this; + } + + /** + * @return Collection<int, Item> + */ + public function getItems(): Collection + { + return $this->items; + } + + public function addItem(?Item $item): void + { + if (!$this->items->contains($item)) { + $this->items->add($item); + $item->setWishlist($this); + } + } + + public function removeItem(?Item $item): void + { + if ($this->items->removeElement($item)) { + // set the owning side to null (unless already changed) + if ($item->getWishlist() === $this) { + $item->setWishlist(null); + } + } + } + public function editItem(?Item $item, string $newName, string $newDescription, int $newPrice, string $newUrl): void + { + $item->setName($newName); + $item->setDescription($newDescription); + $item->setPrice($newPrice); + $item->setUrl($newUrl); + } + + public function getOwner(): ?User + { + return $this->owner; + } + + public function setOwner(?User $owner): self + { + $this->owner = $owner; + return $this; + } + + public function editWishlist(Wishlist $wishlist): void + { + $this->name = $wishlist->getName(); + $this->deadline = $wishlist->getDeadline(); + } + + public function delete(): void + { + // Cette méthode sera implémentée dans le service ou le contrôleur + } + + public function sortItemsAsc(): void + { + $iterator = $this->items->getIterator(); + $itemsArray = $this->items->toArray(); + usort($itemsArray, function (Item $a, Item $b) { + return ($a->getName() <=> $b->getName()); + }); + $this->items = new ArrayCollection($itemsArray); + } + + public function sortItemsDesc(): void + { + $iterator = $this->items->getIterator(); + $itemsArray = $this->items->toArray(); + usort($itemsArray, function (Item $a, Item $b) { + return ($b->getName() <=> $a->getName()); + }); + $this->items = new ArrayCollection($itemsArray); + $this->items = new ArrayCollection(iterator_to_array($iterator)); + } + + public function getCollaborators(): Collection + { + return $this->collaborators; + } + + public function addCollaborator(User $user): self + { + if (!$this->collaborators->contains($user)) { + $this->collaborators->add($user); + } + return $this; + } + + public function removeCollaborator(User $user): self + { + $this->collaborators->removeElement($user); + return $this; + } + +} diff --git a/Server/src/Exceptions/AccessDeniedListener.php b/Server/src/Exceptions/AccessDeniedListener.php new file mode 100644 index 0000000000000000000000000000000000000000..8eb46658b037de49b6c2e8748b9f45807ea8be68 --- /dev/null +++ b/Server/src/Exceptions/AccessDeniedListener.php @@ -0,0 +1,38 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Exceptions; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Twig\Environment; + +class AccessDeniedListener +{ + private Environment $twig; + + public function __construct(Environment $twig) + { + $this->twig = $twig; + } + + public function onKernelException(ExceptionEvent $event): void + { + $exception = $event->getThrowable(); + + // Vérifie si l'exception est une AccessDeniedHttpException + if ($exception instanceof AccessDeniedHttpException) { + $response = new Response( + $this->twig->render('bundles/TwigBundle/Exception/error403.html.twig'), + Response::HTTP_FORBIDDEN + ); + + $event->setResponse($response); + } + } +} \ No newline at end of file diff --git a/Server/src/FormRegister/RegistrationFormType.php b/Server/src/FormRegister/RegistrationFormType.php new file mode 100644 index 0000000000000000000000000000000000000000..2776f293d0a36f201f4d88cca4f1714e535950e2 --- /dev/null +++ b/Server/src/FormRegister/RegistrationFormType.php @@ -0,0 +1,127 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\FormRegister; + +use App\Entity\User; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\Extension\Core\Type\RepeatedType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +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; + +class RegistrationFormType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('email', EmailType::class, [ + 'label' => 'Email', + 'attr' => [ + 'placeholder' => 'Votre email' + ] + ]) + ->add('username', TextType::class, [ + 'label' => 'Nom d\'utilisateur', + 'attr' => [ + 'placeholder' => 'Votre nom d\'utilisateur' + ], + 'constraints' => [ + new NotBlank([ + 'message' => 'Veuillez entrer un nom d\'utilisateur', + ]), + new Length([ + 'min' => 3, + 'minMessage' => 'Votre nom d\'utilisateur doit comporter au moins {{ limit }} caractères', + 'max' => 50, + ]), + ], + ]) + // ->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) { + // $form = $event->getForm(); + // $data = $form->getData(); + // $username = $data['username'] ?? null; + + // if ($username) { + // $userRepository = $event->getForm()->getConfig()->getOption('user_repository'); + // if ($userRepository->findOneBy(['username' => $username])) { + // $form->get('username')->addError(new FormError('Ce nom d\'utilisateur est déjà pris.')); + // } + // } + // }) + ->add('firstName', TextType::class, [ + 'label' => 'Prénom', + 'attr' => [ + 'placeholder' => 'Votre prénom' + ], + 'required' => false, + ]) + ->add('lastName', TextType::class, [ + 'label' => 'Nom', + 'attr' => [ + 'placeholder' => 'Votre nom' + ], + 'required' => false, + ]) + ->add('plainPassword', RepeatedType::class, [ + 'type' => PasswordType::class, + 'mapped' => false, + 'first_options' => [ + 'label' => 'Mot de passe', + 'attr' => [ + 'placeholder' => 'Votre mot de passe' + ] + ], + 'second_options' => [ + 'label' => 'Confirmer le mot de passe', + 'attr' => [ + 'placeholder' => 'Confirmez votre mot de passe' + ] + ], + 'invalid_message' => 'Les mots de passe doivent correspondre', + 'constraints' => [ + new NotBlank([ + 'message' => 'Veuillez entrer un mot de passe', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Votre mot de passe doit comporter au moins {{ limit }} caractères', + 'max' => 4096, + ]), + ], + ]) + ->add('agreeTerms', CheckboxType::class, [ + 'mapped' => false, + 'label' => 'J\'accepte les conditions d\'utilisation', + 'constraints' => [ + new IsTrue([ + 'message' => 'Vous devez accepter les conditions d\'utilisation.', + ]), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} + + +?> + + diff --git a/Server/src/Interfaces/myInvitationPage.php b/Server/src/Interfaces/myInvitationPage.php new file mode 100644 index 0000000000000000000000000000000000000000..470c7c79596784dcf37e010e2e6c441a5e785237 --- /dev/null +++ b/Server/src/Interfaces/myInvitationPage.php @@ -0,0 +1,19 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Interfaces; + + +namespace App\Interfaces; + +use App\Entity\Invitation; + + +interface MyInvitationPage +{ + public function acceptInvitation(?Invitation $invitation): void; +} \ No newline at end of file diff --git a/Server/src/Interfaces/myWishlistPage.php b/Server/src/Interfaces/myWishlistPage.php new file mode 100644 index 0000000000000000000000000000000000000000..41a867e28fd51bc295101effc164b1b5a4bf0db4 --- /dev/null +++ b/Server/src/Interfaces/myWishlistPage.php @@ -0,0 +1,19 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Interfaces; + +namespace App\Interfaces; + +use App\Entity\Item; + +interface MyWishlistPage +{ + public function addItem(?Item $item): void; + public function editItem(?Item $item, string $newName, string $newDescription, int $newPrice, string $newUrl): void; + public function removeItem(?Item $item): void; +} diff --git a/Server/src/Interfaces/myWishlistsListPage.php b/Server/src/Interfaces/myWishlistsListPage.php new file mode 100644 index 0000000000000000000000000000000000000000..70ba381a8962fe0aa025bce39651d1742f619db3 --- /dev/null +++ b/Server/src/Interfaces/myWishlistsListPage.php @@ -0,0 +1,26 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Interfaces; + + +namespace App\Interfaces; + +use App\Entity\Wishlist; // Adjust the namespace as needed +use App\Entity\User; +use Doctrine\Common\Collections\Collection; + +interface MyWishlistsListPage +{ + public function createWishlist(string $title, \DateTimeInterface $deadline): ?Wishlist; + + public function deleteWishlist(?Wishlist $wishlist): void; + + public function shareWishlist(?Wishlist $wishlist, Collection $users): void; + + public function displayWishlist(?Wishlist $wishlist, Collection $users): void; +} diff --git a/Server/src/Interfaces/purchasePage.php b/Server/src/Interfaces/purchasePage.php new file mode 100644 index 0000000000000000000000000000000000000000..92078676f0c78eef28c64b638a58f75c2b2488f1 --- /dev/null +++ b/Server/src/Interfaces/purchasePage.php @@ -0,0 +1,15 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Interfaces; + + +interface PurchasePage +{ + public function addProof() : Bool; + public function sendMessage(String $message) : void; +} \ No newline at end of file diff --git a/Server/src/Interfaces/viewuserWishlist.php b/Server/src/Interfaces/viewuserWishlist.php new file mode 100644 index 0000000000000000000000000000000000000000..93df801386301b1000fa7a6165d1d661496fbe59 --- /dev/null +++ b/Server/src/Interfaces/viewuserWishlist.php @@ -0,0 +1,15 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Interfaces; + + +interface ViewUserWishlist +{ + public function sortItemsAsc(): void; + public function sortItemsDesc(): void; +} \ No newline at end of file diff --git a/Server/src/ItemForm/ItemFormType.php b/Server/src/ItemForm/ItemFormType.php new file mode 100644 index 0000000000000000000000000000000000000000..5c4e44e525fc8503ad1734d85ff74f628e733c73 --- /dev/null +++ b/Server/src/ItemForm/ItemFormType.php @@ -0,0 +1,35 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\ItemForm; + +use App\Entity\Item; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ItemFormType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('name') + ->add('description') + ->add('price') + ->add('url') + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Item::class, + ]); + } +} diff --git a/Server/src/ItemForm/WishlistType.php b/Server/src/ItemForm/WishlistType.php new file mode 100644 index 0000000000000000000000000000000000000000..972779258855d38665686b346947d79324ad45ce --- /dev/null +++ b/Server/src/ItemForm/WishlistType.php @@ -0,0 +1,49 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\ItemForm; + +use App\Entity\Wishlist; +use App\Entity\User; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\NotBlank; + +class WishlistType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('name', TextType::class, [ + 'label' => 'Nom de la liste', + 'constraints' => [ + new NotBlank([ + 'message' => 'Veuillez entrer un nom pour cette liste', + ]), + ], + ]) + ->add('deadline', DateType::class, [ + 'label' => 'Date limite (optionnelle)', + 'widget' => 'single_text', + 'required' => false, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Wishlist::class, + 'include_collaborators' => false, // cette option n'a plus d'effet + ]); + } +} diff --git a/Server/src/Kernel.php b/Server/src/Kernel.php new file mode 100644 index 0000000000000000000000000000000000000000..779cd1f2b12e0d30731787539ba67645b73ef796 --- /dev/null +++ b/Server/src/Kernel.php @@ -0,0 +1,11 @@ +<?php + +namespace App; + +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Component\HttpKernel\Kernel as BaseKernel; + +class Kernel extends BaseKernel +{ + use MicroKernelTrait; +} diff --git a/Server/src/Repository/.gitignore b/Server/src/Repository/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/Server/src/Repository/InvitationRepository.php b/Server/src/Repository/InvitationRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..2255d5a90195c14e823d53eb7ece021fdc8b4946 --- /dev/null +++ b/Server/src/Repository/InvitationRepository.php @@ -0,0 +1,24 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Repository; + +use App\Entity\Invitation; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @extends ServiceEntityRepository<Invitation> + */ +class InvitationRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Invitation::class); + } +} \ No newline at end of file diff --git a/Server/src/Repository/ItemRepository.php b/Server/src/Repository/ItemRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..19bb6ad2dff66d17fda373fdcae966af6c3e63ef --- /dev/null +++ b/Server/src/Repository/ItemRepository.php @@ -0,0 +1,48 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Repository; + +use App\Entity\Item; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @extends ServiceEntityRepository<Item> + */ +class ItemRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Item::class); + } + + public function findTop3MostExpensiveItems(): array + { + return $this->createQueryBuilder('i') + ->select('i.id', 'i.name', 'i.price', 'u.username as recipientName') + ->join('i.wishlist', 'w') + ->join('w.owner', 'u') // the owner of the wishlist is the recipient + ->where('i.hasPurchased = true') + ->orderBy('i.price', 'DESC') + ->setMaxResults(3) + ->getQuery() + ->getResult(); + } + public function searchItems(string $query): array + { + // Use a manual query selecting only the fields we need + return $this->createQueryBuilder('i') + ->select('i.id', 'i.name', 'i.description', 'i.price', 'i.hasPurchased', 'i.url') + ->where('i.name LIKE :query OR i.description LIKE :query') + ->setParameter('query', '%' . $query . '%') + ->orderBy('i.name', 'ASC') + ->getQuery() + ->getArrayResult(); // Return array instead of entities to avoid relationship loading + } +} diff --git a/Server/src/Repository/ProofRepository.php b/Server/src/Repository/ProofRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..9f85c43a2537a26c3634dc9ce2ca182985990d79 --- /dev/null +++ b/Server/src/Repository/ProofRepository.php @@ -0,0 +1,48 @@ +<?php +/** + * + * @authors + * - YAO Jean-David (Binôme 13) + * - AROUISSI Khaoula (Binôme 13) + */ +namespace App\Repository; + +use App\Entity\Proof; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @extends ServiceEntityRepository<Proof> + */ +class ProofRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Proof::class); + } + +// /** +// * @return Proof[] Returns an array of Proof objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('p') +// ->andWhere('p.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('p.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?Proof +// { +// return $this->createQueryBuilder('p') +// ->andWhere('p.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/Server/src/Repository/TestRepository.php b/Server/src/Repository/TestRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..cfc3092d2d0ec56918f47177e998a2ecd47980f5 --- /dev/null +++ b/Server/src/Repository/TestRepository.php @@ -0,0 +1,49 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Repository; + +use App\Entity\Test; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @extends ServiceEntityRepository<Test> + */ +class TestRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Test::class); + } + +// /** +// * @return Test[] Returns an array of Test objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('t') +// ->andWhere('t.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('t.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?Test +// { +// return $this->createQueryBuilder('t') +// ->andWhere('t.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/Server/src/Repository/UserRepository.php b/Server/src/Repository/UserRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..fe8f5d6eb0fe44f0020e88a4431df86489044486 --- /dev/null +++ b/Server/src/Repository/UserRepository.php @@ -0,0 +1,63 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Repository; + +use App\Entity\User; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @extends ServiceEntityRepository<User> + */ +class UserRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + // /** + // * @return User[] Returns an array of User objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?User + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } + + + + public function findByUsername(string $query): array + { + return $this->createQueryBuilder('u') + ->where('u.username LIKE :query') + ->orWhere('u.email LIKE :query') + ->setParameter('query', '%' . $query . '%') + ->setMaxResults(10) + ->getQuery() + ->getResult(); + } +} + diff --git a/Server/src/Repository/WishlistRepository.php b/Server/src/Repository/WishlistRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..27ccd9cc1607ca901c6d5b854edf75330f3655e8 --- /dev/null +++ b/Server/src/Repository/WishlistRepository.php @@ -0,0 +1,47 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Repository; + +use App\Entity\User; +use App\Entity\Wishlist; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @extends ServiceEntityRepository<Wishlist> + */ +class WishlistRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Wishlist::class); + } + public function findTop3WishlistsByTotalValue(): array +{ + return $this->createQueryBuilder('w') + ->select('w', 'SUM(i.price) as totalValue', 'u.username as ownerName') + ->join('w.items', 'i') // Jointure avec les items + ->join('w.owner', 'u') // Jointure avec le propriétaire (relation ManyToOne ou OneToMany) + ->groupBy('w') + ->orderBy('totalValue', 'DESC') + ->setMaxResults(3) + ->getQuery() + ->getResult(); +} + +public function findCollaboratorWishlists(User $user): array +{ + return $this->createQueryBuilder('w') + ->innerJoin('w.collaborators', 'c') + ->where('c = :user') + ->setParameter('user', $user) + ->getQuery() + ->getResult(); +} +} diff --git a/Server/src/Security/Voter/ItemVoter.php b/Server/src/Security/Voter/ItemVoter.php new file mode 100644 index 0000000000000000000000000000000000000000..544d4af780ebd21b957bc46d2b42499a8fee42fb --- /dev/null +++ b/Server/src/Security/Voter/ItemVoter.php @@ -0,0 +1,49 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Security\Voter; + +use App\Entity\Item; +use App\Entity\User; +use App\Entity\Wishlist; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\User\UserInterface; + +class ItemVoter extends Voter +{ + public const DELETE = 'DELETE'; + public const EDIT = 'EDIT'; + public const ADD = 'ADD'; + public const SHARE = 'SHARE'; + + protected function supports(string $attribute, $subject): bool + { + // Vérifie si l'attribut correspond à une action et si le sujet est une instance de Wishlist ou Item + return in_array($attribute, [self::DELETE, self::EDIT, self::ADD , self::SHARE]) + && ($subject instanceof Wishlist || $subject instanceof Item); + } + + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + { + $user = $token->getUser(); + + if (!$user instanceof UserInterface) { + return false; // L'utilisateur n'est pas authentifié + } + return $this->canAccess($attribute, $subject, $user); + } +private function canAccess(string $attribute, $subject, User $user): bool +{ + return !$user->isLockStatus(); +} + + + +} \ No newline at end of file diff --git a/Server/src/Twig/AppExtension.php b/Server/src/Twig/AppExtension.php new file mode 100644 index 0000000000000000000000000000000000000000..b7f194e51e088bb911326371b1d2deaf4e261ac3 --- /dev/null +++ b/Server/src/Twig/AppExtension.php @@ -0,0 +1,34 @@ +<?php +/** + * + * @authors + * - CHAHBOUNE Nawal (Binôme 15) + * - GHALLAB Houda (Binôme 15) + * +*/ +namespace App\Twig; + +use App\Entity\Wishlist; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; + +class AppExtension extends AbstractExtension +{ + public function getFunctions(): array + { + return [ + new TwigFunction('is_wishlist_expired', [$this, 'isWishlistExpired']), + ]; + } + + public function isWishlistExpired(Wishlist $wishlist): bool + { + if ($wishlist->getDeadline() === null) { + return false; + } + + return $wishlist->getDeadline() < new \DateTime(); + } +} + +?> \ No newline at end of file diff --git a/Server/symfony.lock b/Server/symfony.lock new file mode 100644 index 0000000000000000000000000000000000000000..2d70ac66578f053eeea5ebe8f07fda2138f6ceb9 --- /dev/null +++ b/Server/symfony.lock @@ -0,0 +1,191 @@ +{ + "doctrine/doctrine-bundle": { + "version": "2.13", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.13", + "ref": "8d96c0b51591ffc26794d865ba3ee7d193438a83" + }, + "files": [ + "./config/packages/doctrine.yaml", + "./src/Entity/.gitignore", + "./src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-fixtures-bundle": { + "version": "4.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "1f5514cfa15b947298df4d771e694e578d4c204d" + }, + "files": [ + "./src/DataFixtures/AppFixtures.php" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "3.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.1", + "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" + }, + "files": [ + "./config/packages/doctrine_migrations.yaml", + "./migrations/.gitignore" + ] + }, + "symfony/console": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461" + }, + "files": [ + "bin/console" + ] + }, + "symfony/flex": { + "version": "2.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.4", + "ref": "52e9754527a15e2b79d9a610f98185a1fe46622a" + }, + "files": [ + ".env", + ".env.dev" + ] + }, + "symfony/form": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.2", + "ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b" + }, + "files": [ + "./config/packages/csrf.yaml" + ] + }, + "symfony/framework-bundle": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.2", + "ref": "87bcf6f7c55201f345d8895deda46d2adbdbaa89" + }, + "files": [ + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/preload.php", + "config/routes/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/maker-bundle": { + "version": "1.62", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, + "symfony/monolog-bundle": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "aff23899c4440dd995907613c1dd709b6f59503f" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, + "symfony/routing": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "21b72649d5622d8f7da329ffb5afb232a023619d" + }, + "files": [ + "config/packages/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/security-bundle": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "2ae08430db28c8eb4476605894296c82a642028f" + }, + "files": [ + "./config/packages/security.yaml", + "./config/routes/security.yaml" + ] + }, + "symfony/twig-bundle": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/uid": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "0df5844274d871b37fc3816c57a768ffc60a43a5" + } + }, + "symfony/validator": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd" + }, + "files": [ + "./config/packages/validator.yaml" + ] + }, + "symfony/web-profiler-bundle": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.1", + "ref": "e42b3f0177df239add25373083a564e5ead4e13a" + }, + "files": [ + "config/packages/web_profiler.yaml", + "config/routes/web_profiler.yaml" + ] + } +} diff --git a/Server/templates/.DS_Store b/Server/templates/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..be199683dcd0e7a309aac497e0a2639bc5f4d66a Binary files /dev/null and b/Server/templates/.DS_Store differ diff --git a/Server/templates/admin/dashboard.html.twig b/Server/templates/admin/dashboard.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..af081ebbe9b4c2bf7a5b3fb602861da0fa4c0f1d --- /dev/null +++ b/Server/templates/admin/dashboard.html.twig @@ -0,0 +1,50 @@ +{# templates/admin/dashboard.html.twig #} +{# + @authors + - YAO Jean-David (Binôme 13) + - AROUISSI Khaoula (Binôme 13) +#} +{% extends 'base.html.twig' %} + +{% block title %}Admin Dashboard - GiftWish{% endblock %} + +{% block stylesheets %} + {{ parent() }} + <link rel="stylesheet" href="{{ asset('css/style.css') }}"> +{% endblock %} + +{% block body %} +<div class="container mt-5"> + <div class="text-center mb-4"> + <h1 class="display-6">Administrator Dashboard</h1> + <p class="text-muted">Choose an option below:</p> + </div> + + <div class="row g-4"> + <!-- Top 3 Statistics Section --> + <div class="col-md-6"> + <a href="{{ path('admin_top3') }}" class="text-decoration-none"> + <div class="card shadow-sm h-100"> + <div class="card-body text-center"> + <i class="fas fa-chart-line fa-3x text-primary mb-3"></i> + <h4 class="card-title">Top 3 Statistics</h4> + <p class="card-text">View the top-3 most expensive items and wishlists by total value.</p> + </div> + </div> + </a> + </div> + <!-- Manage Users Section --> + <div class="col-md-6"> + <a href="{{ path('admin_users') }}" class="text-decoration-none"> + <div class="card shadow-sm h-100"> + <div class="card-body text-center"> + <i class="fas fa-users-cog fa-3x text-success mb-3"></i> + <h4 class="card-title">Manage Users</h4> + <p class="card-text">View, lock/unlock, or remove user accounts.</p> + </div> + </div> + </a> + </div> + </div> +</div> +{% endblock %} diff --git a/Server/templates/admin/top3.html.twig b/Server/templates/admin/top3.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..6c081b5b385ad2601fc1d899f83310c3703a29c4 --- /dev/null +++ b/Server/templates/admin/top3.html.twig @@ -0,0 +1,65 @@ +{# filepath: templates/admin/top3.html.twig #} +{# + @authors + - YAO Jean-David (Binôme 13) + - AROUISSI Khaoula (Binôme 13) +#} +{% extends 'base.html.twig' %} + +{% block title %}Top 3 Statistics - GiftWish Admin{% endblock %} + +{% block stylesheets %} + {{ parent() }} + <link rel="stylesheet" href="{{ asset('css/style.css') }}"> +{% endblock %} + +{% block body %} +<div class="container mt-5"> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h1 class="display-6">Top 3 Statistics</h1> + <p class="text-muted">View top items and wishlists data</p> + </div> + + <div class="row g-4"> + <!-- Top 3 Most Expensive Items --> + <div class="col-md-6"> + <h4 class="mb-3">Top 3 Most Expensive Items</h4> + <div class="list-group shadow-sm"> + {% for item in topItems %} + <div class="list-group-item d-flex justify-content-between align-items-center"> + <div> + <h5 class="mb-1">{{ item["name"] }}</h5> + <small>Recipient: {{ item["recipientName"] }}</small> + </div> + <span class="badge bg-primary">{{ item["price"]|number_format(2) }} $</span> + </div> + {% else %} + <div class="list-group-item">No items found.</div> + {% endfor %} + </div> + </div> + + <!-- Top 3 Wishlists by Total Value --> + <div class="col-md-6"> + <h4 class="mb-3">Top 3 Wishlists by Total Value</h4> + <div class="list-group shadow-sm"> + {% for wishlist in topWishlists %} + <div class="list-group-item d-flex justify-content-between align-items-center"> + <div> + <h5 class="mb-1">{{ wishlist[0].name }}</h5> + <small>Owner: {{ wishlist["ownerName"] }}</small> + </div> + <span class="badge bg-success">Total Purchased: {{ wishlist.totalValue|number_format(2) }} $</span> + </div> + {% else %} + <div class="list-group-item">No wishlists found.</div> + {% endfor %} + </div> + </div> + </div> + + <div class="mt-4"> + <a href="{{ path('admin_dashboard') }}" class="btn btn-secondary">Back to Dashboard</a> + </div> +</div> +{% endblock %} \ No newline at end of file diff --git a/Server/templates/admin/users.html.twig b/Server/templates/admin/users.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..bc30c88d5d518a38c1a87ecb37d7f9eb2e67ca28 --- /dev/null +++ b/Server/templates/admin/users.html.twig @@ -0,0 +1,94 @@ +{# templates/admin/users.html.twig #} +{# + @authors + - YAO Jean-David (Binôme 13) + - AROUISSI Khaoula (Binôme 13) +#} +{% extends 'base.html.twig' %} + +{% block title %}User Management - GiftWish{% endblock %} + +{% block stylesheets %} + {{ parent() }} + <link rel="stylesheet" href="{{ asset('css/style.css') }}"> +{% endblock %} + +{% block body %} +<div class="container mt-5"> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h1 class="display-6">Users list</h1> + + {# Optional search bar #} + <form action="{{ path('admin_users') }}" method="get" class="d-flex"> + <input type="text" name="q" placeholder="Search user" class="form-control me-2"> + <button class="btn btn-primary" type="submit">Search</button> + </form> + </div> + + {# Success/Error messages #} + {% for message in app.flashes('success') %} + <div class="alert alert-success">{{ message }}</div> + {% endfor %} + {% for message in app.flashes('error') %} + <div class="alert alert-danger">{{ message }}</div> + {% endfor %} + + <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3"> + {% for user in users %} + <div class="col"> + <div class="card shadow-sm"> + <div class="card-body"> + <div class="d-flex justify-content-between align-items-center mb-2"> + <div> + <h5 class="card-title mb-0">{{ user.username }}</h5> + {% if user.type == 'admin' %} + <span class="badge bg-info">Admin</span> + {% else %} + <span class="badge bg-secondary">User</span> + {% endif %} + </div> + {# Locked/Active badge #} + {% if user.lockStatus %} + <span class="badge bg-danger">Locked</span> + {% else %} + <span class="badge bg-success">Active</span> + {% endif %} + </div> + <p class="card-text text-muted"> + Member since: 01/01/2001 + <br> + Email: {{ user.email }} + </p> + + <div class="mt-2"> + {% if user.lockStatus %} + <form action="{{ path('admin_user_unlock', {'id': user.id}) }}" method="post" class="d-inline"> + <button type="submit" class="btn btn-sm btn-success">Unlock</button> + </form> + {% else %} + <form action="{{ path('admin_user_lock', {'id': user.id}) }}" method="post" class="d-inline"> + <button type="submit" class="btn btn-sm btn-warning">Lock</button> + </form> + {% endif %} + + <form action="{{ path('admin_user_delete', {'id': user.id}) }}" method="post" class="d-inline" + onsubmit="return confirm('Are you sure you want to remove this user?');"> + <button type="submit" class="btn btn-sm btn-danger">Remove</button> + </form> + </div> + </div> + </div> + </div> + {% else %} + <div class="col-12"> + <div class="alert alert-info text-center">No users found.</div> + </div> + {% endfor %} + </div> + + <div class="mt-4"> + <a href="{{ path('admin_dashboard') }}" class="btn btn-secondary">Back to Dashboard</a> + </div> + +</div> +{% endblock %} diff --git a/Server/templates/api/index.html.twig b/Server/templates/api/index.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..0821e2f7a860b4f6ebcf92030b3f3459118a8808 --- /dev/null +++ b/Server/templates/api/index.html.twig @@ -0,0 +1,27 @@ +{# + + @authors + - CHAHBOUNE Nawal (Binôme 15) + - GHALLAB Houda (Binôme 15) +#} +{% extends 'base.html.twig' %} + + +{% block title %}Hello ApiController!{% endblock %} + +{% block body %} +<style> + .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } + .example-wrapper code { background: #F5F5F5; padding: 2px 6px; } +</style> + +<div class="example-wrapper"> + <h1>Hello {{ controller_name }}! ✅</h1> + + This friendly message is coming from: + <ul> + <li>Your controller at <code>C:/wrk/perso/webapp/symfony-docker/symfony-docker/project/src/Controller/ApiController.php</code></li> + <li>Your template at <code>C:/wrk/perso/webapp/symfony-docker/symfony-docker/project/templates/api/index.html.twig</code></li> + </ul> +</div> +{% endblock %} diff --git a/Server/templates/base.html.twig b/Server/templates/base.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..86bcd41ad73f6138cc4863f086df3d0a7f000bea --- /dev/null +++ b/Server/templates/base.html.twig @@ -0,0 +1,112 @@ + +{# + + @authors + - CHAHBOUNE Nawal (Binôme 15) + - GHALLAB Houda (Binôme 15) +#} +{# filepath: c:\WEBAPP\tp-symfony\projetUEG\templates\base.html.twig #} +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="csrf-token" content="{{ csrf_token('wishlist-share') }}"> + + <title>{% block title %}Welcome!{% endblock %}</title> + <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>"> + + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Pacifico&display=swap" rel="stylesheet"> + + {% block stylesheets %} + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> + <link rel="stylesheet" href="{{ asset('css/style.css') }}"> + {% endblock %} + + {% block javascripts %} + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> + {% endblock %} + </head> + <body> + <div class="content-wrapper"> + <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4"> + <div class="container"> + <a class="navbar-brand brand-logo" href="{{ path('app_home') }}">GiftWish</a> + <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarNav"> + <ul class="navbar-nav me-auto"> + {% if is_granted('ROLE_USER') %} + <li class="nav-item"> + <a class="nav-link" href="{{ path('app_wishlists_index') }}">My Wishlists</a> + </li> + {% endif %} + <li class="nav-item"> + <a class="nav-link" href="{{ path('app_about') }}">About</a> + </li> + {% if is_granted('ROLE_ADMIN') %} + <li class="nav-item"> + <a class="nav-link" href="{{ path('admin_dashboard') }}">Dashboard</a> + </li> + {% endif %} + </ul> + {# <div class="btn-group me-3"> + <button type="button" class="btn btn-link position-relative text-decoration-none" data-bs-toggle="dropdown" aria-expanded="false"> + <i class="fa-solid fa-bell"></i> + </button> + <ul class="dropdown-menu dropdown-menu-end"> + <li><a class="dropdown-item" href="#">user545 invites you to her wishlist</a></li> + <li><a class="dropdown-item" href="#">user456 wants to show item</a></li> + </ul> + </div> #} + <div class="navbar-nav"> + {% if is_granted('ROLE_USER') %} + <span class="nav-link"> + {{ app.user.firstname ?? app.user.email }} + {% if is_granted('ROLE_ADMIN') %} + <span class="badge bg-info ms-2">Admin</span> + {% else %} + <span class="badge bg-secondary ms-2">User</span> + {% endif %} + </span> +<form action="{{ path('app_logout') }}" method="post" class="logout-form"> + <input type="hidden" name="_csrf_token" value="{{ csrf_token('logout') }}"> + <button type="submit" class="btn btn-link nav-link border-0 bg-transparent p-0">Logout</button> +</form> + {% else %} + <div class="d-flex"> + <a href="{{ path('app_login') }}" class="btn btn-primary btn-sm me-3">Login</a> + <a href="{{ path('app_signup') }}" class="btn btn-primary btn-sm">Sign Up</a> + </div> + {% endif %} + </div> + </div> + </div> + </nav> + + <div class="container"> + {% for message in app.flashes('success') %} + <div class="alert alert-success"> + {{ message }} + </div> + {% endfor %} + + {% for message in app.flashes('error') %} + <div class="alert alert-danger"> + {{ message }} + </div> + {% endfor %} + + {% block body %}{% endblock %} + </div> + </div> + + <footer class="py-3 footer text-center"> + <p>Gift Exchange © {{ "now"|date("Y") }}</p> + </footer> + </body> +</html> diff --git a/Server/templates/bundles/TwigBundle/Exception/error403.html.twig b/Server/templates/bundles/TwigBundle/Exception/error403.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..de5d256f660d06b0f652aed6a284323e1dacd8bd --- /dev/null +++ b/Server/templates/bundles/TwigBundle/Exception/error403.html.twig @@ -0,0 +1,47 @@ +{# + + @authors + - CHAHBOUNE Nawal (Binôme 15) + - GHALLAB Houda (Binôme 15) +#} +{% extends 'base.html.twig' %} + +{% block title %}Accès refusé{% endblock %} + +{% block body %} + <div class="error-page"> + <h1>403 - Accès refusé</h1> + <p>Vous n'avez pas les permissions nécessaires pour accéder à cette page.</p> + <a href="{{ path('app_home') }}" class="btn btn-primary">Retour à l'accueil</a> + </div> +{% endblock %} + +{% block stylesheets %} + {{ parent() }} + <style> + .error-page { + text-align: center; + margin-top: 50px; + } + .error-page h1 { + font-size: 3rem; + color: #ff4d4f; + } + .error-page p { + font-size: 1.2rem; + color: #555; + } + .error-page .btn { + margin-top: 20px; + padding: 10px 20px; + font-size: 1rem; + background-color: #007bff; + color: #fff; + text-decoration: none; + border-radius: 5px; + } + .error-page .btn:hover { + background-color: #0056b3; + } + </style> +{% endblock %} \ No newline at end of file diff --git a/Server/templates/home/about.html.twig b/Server/templates/home/about.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..d235a04f4f658712ca31bae62b4cc01e10f0bbab --- /dev/null +++ b/Server/templates/home/about.html.twig @@ -0,0 +1,100 @@ +{# + + @authors + - KELA Wydie (Binôme 14) + - LALE Ny (Binôme 14) +#} +{# templates/home/about.html.twig #} +{% extends 'base.html.twig' %} + +{% block title %} À Propos {% endblock %} + +{% block stylesheets %} + {{ parent() }} + <link rel="stylesheet" href="{{ asset('css/style.css') }}"> +{% endblock %} + + +{% block body %} + <section class="about-container"> + <div class="content"> + <h1>À Propos de Notre Application</h1> + <p> + Bienvenue sur <strong>WishlistApp</strong>, la solution ultime pour gérer et partager vos listes de souhaits. + Que ce soit pour les anniversaires, Noël ou toute autre occasion spéciale, nous vous aidons à organiser vos envies + et à les partager facilement avec vos proches. + </p> + </div> + </section> + + <section class="features"> + <div class="content"> + <h2>Nos Fonctionnalités</h2> + <div class="features-grid"> + <div class="feature-item"> + <h3>📌 Création de Wishlists</h3> + <p>Ajoutez et personnalisez vos listes de souhaits selon vos besoins.</p> + </div> + <div class="feature-item"> + <h3>🔗 Partage Facile</h3> + <p>Envoyez vos listes à vos amis et votre famille en un clic.</p> + </div> + <div class="feature-item"> + <h3>💡 Suggestions Intelligentes</h3> + <p>Bénéficiez de recommandations basées sur vos préférences.</p> + </div> + <div class="feature-item"> + <h3>📊 Suivi des Achats</h3> + <p>Voyez quels cadeaux ont été achetés et ce qui reste à offrir.</p> + </div> + </div> + </div> + </section> + + <section class="team"> + <div class="content"> + <h2>Rencontrez Notre Équipe</h2> + <div class="team-members"> + <div class="member"> + <img src="/images/team1.jpg" alt="jd"> + <h3>Jean David</h3> + <p>📌 Chef de projet</p> + </div> + <div class="member"> + <img src="/images/team2.jpg" alt="Nw"> + <h3>Nawal</h3> + <p>👨💻 Developper </p> + </div> + <div class="member"> + <img src="/images/team2.jpg" alt="Kwn"> + <h3>Wydie</h3> + <p>👨💻 Developper</p> + </div> + <div class="member"> + <img src="/images/team2.jpg" alt="Hd"> + <h3>Houda</h3> + <p>👨💻 Developper</p> + </div> + <div class="member"> + <img src="/images/team2.jpg" alt="kl"> + <h3>Khaoula</h3> + <p>👨💻 Developper</p> + </div> + <div class="member"> + <img src="/images/team2.jpg" alt="En"> + <h3>Erwan</h3> + <p>👨💻 Developper</p> + </div> + </div> + </div> + </section> + + <section class="contact"> + <div class="content"> + <h2>Contactez-Nous</h2> + <p>📧 Email : <a href="mailto:support@imt-atlantique.net">support@wishlistapp.com</a></p> + <p>📍 Adresse : 655 Avenue du Technopôle, Plouzané</p> + <p>📞 Téléphone : +33 1 23 45 67 89</p> + </div> + </section> +{% endblock %} diff --git a/Server/templates/home/index.html.twig b/Server/templates/home/index.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..f2b5c4596e26ee7bfb770533c41c8f13cd2ab346 --- /dev/null +++ b/Server/templates/home/index.html.twig @@ -0,0 +1,106 @@ +{# + + @authors + - KELA Wydie (Binôme 14) + - LALE Ny (Binôme 14) +#} +{# templates/home.html.twig #} +{% extends 'base.html.twig' %} + +{% block title %}GiftWish - Home{% endblock %} + +{% block stylesheets %} + {{ parent() }} + <link rel="stylesheet" href="{{ asset('css/home.css') }}"> +{% endblock %} + + +{% block body %} + +<section class="hero"> + <!-- Left: Gift Image --> + <div class="hero-left"> + <img src="{{ asset('assets/image (1).png') }}" alt="GiftWish Box" class="hero-img"> + </div> + + <div class="hero-right"> + <h1> + Discover the perfect gift <br> + from a wishlist + </h1> + <div class="search-bar"> +<form action="{{ path('app_search') }}" method="get" class="d-flex search-container"> + <div class="input-group"> + <input class="form-control" type="search" name="q" placeholder="Rechercher des articles..." aria-label="Rechercher"> + <button class="btn btn-primary search-button" type="submit">Rechercher</button> + </div> +</form> + </div> + </div> +</section> + + + + + + <section class="how-it-works"> + <h2>How GiftWish works</h2> + <div class="features"> + <div class="feature"> + <img src="{{ asset('assets/image (2).png') }}" alt="Top expensive gifts"> + <h3>Top expensive gifts</h3> + <p>Enter a wishlist. Find the most exclusive gifts for special occasions.</p> + </div> + <div class="feature"> + <img src="{{ asset('assets/image (3).png') }}" alt="Top wishlists"> + <h3>Top wishlists</h3> + <p>View top items and wishlists. Explore profiles and receive updates.</p> + </div> + <div class="feature"> + <img src="{{ asset('assets/image (4).png') }}" alt="Top purchased items"> + <h3>Top purchased items</h3> + <p>Complete your purchase. See best sellers for every event.</p> + </div> + </div> + </section> + + + <section class="community"> + <h2>Join GiftWish community</h2> + <div class="community-cards"> + <div class="community-card"> + <img src="{{ asset('assets/image (5).png') }}" alt="As a gift giver"> + <h3>As a gift giver</h3> + <p>Surprise loved ones with wishlist items. Make dreams come true.</p> + <a href="#" class="btn">Gift now</a> + </div> + <div class="community-card"> + <img src="{{ asset('assets/image (6).png') }}" alt="As a contributor"> + <h3>As a contributor</h3> + <p>Help fulfill wishes with gift contributions and support the community.</p> + <a href="#" class="btn">Contribute now</a> + </div> + <div class="community-card"> + <img src="{{ asset('assets/image (7).png') }}" alt="As a team member"> + <h3>As a team member</h3> + <p>Join a team that's dedicated to making wishes a reality.</p> + <a href="#" class="btn">Join us</a> + </div> + </div> + </section> + + + <section class="get-app"> + <h2>Shop effortlessly!</h2> + <div class="app-wrapper"> + <img src="{{ asset('assets/image (8).png') }}" alt="GiftWish app" class="app-img"> + <div class="app-details"> + <p>Surprise your loved ones with gifts from their wishlist. Explore a wide range of options from top brands.</p> + + <a href="{{ path('app_login') }}" class="btn">Log In</a> + <a href="{{ path('app_signup') }}" class="btn btn-outline">Sign Up</a> + + </div> + </div> + </section> +{% endblock %} diff --git a/Server/templates/home/search.html.twig b/Server/templates/home/search.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..a280043c24d0368af6b581a1e5938d12857b6292 --- /dev/null +++ b/Server/templates/home/search.html.twig @@ -0,0 +1,47 @@ +{# + + @authors + - CHAHBOUNE Nawal (Binôme 15) + - GHALLAB Houda (Binôme 15) +#} +{% extends 'base.html.twig' %} + +{% block title %}Résultats de recherche{% endblock %} + +{% block body %} +<div class="container mt-4"> + <h1>Résultats de recherche pour "{{ query }}"</h1> + + <form class="mb-4" action="{{ path('app_search') }}" method="get" class="d-flex search-container"> + <div class="input-group"> + <input type="text" class="form-control" name="q" value="{{ query }}" placeholder="Rechercher des articles..." aria-label="Rechercher"> + <button class="btn btn-primary search-button" type="submit">Rechercher</button> + </div> + </form> + + {% if results is empty and query %} + <div class="alert alert-info"> + Aucun résultat trouvé pour "{{ query }}". + </div> + {% elseif results %} + <div class="row"> + {% for item in results %} + <div class="col-md-4 mb-4"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">{{ item.name }}</h5> + {% if item.description %} + <p class="card-text">{{ item.description }}</p> + {% endif %} + <p class="card-text"><strong>Prix:</strong> {{ item.price }} €</p> + {% if item.url %} + <a href="{{ item.url }}" target="_blank" class="btn btn-primary">Voir le produit</a> + {% endif %} + </div> + </div> + </div> + {% endfor %} + </div> + {% endif %} +</div> +{% endblock %} \ No newline at end of file diff --git a/Server/templates/home/search_results.html.twig b/Server/templates/home/search_results.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..73a02e29c65cee4e9a8d039e2ca30b73ee686d22 --- /dev/null +++ b/Server/templates/home/search_results.html.twig @@ -0,0 +1,44 @@ +{# + + @authors + - CHAHBOUNE Nawal (Binôme 15) + - GHALLAB Houda (Binôme 15) +#} +{% extends 'base.html.twig' %} + +{% block title %}Search Results{% endblock %} + +{% block body %} + <div class="container"> + <h1>Search Results for "{{ query }}"</h1> + + <div class="my-4"> + <form action="{{ path('app_gift_search') }}" method="GET" class="d-flex"> + <input type="text" name="q" class="form-control" value="{{ query }}" placeholder="Search for gifts..." aria-label="Search"> + <button class="btn btn-primary ms-2" type="submit"> + <i class="fas fa-search"></i> Search + </button> + </form> + </div> + + {% if results|length > 0 %} + <div class="row"> + {% for result in results %} + <div class="col-md-4 mb-4"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">{{ result.name }}</h5> + <p class="card-text">{{ result.description }}</p> + <a href="#" class="btn btn-primary">View Details</a> + </div> + </div> + </div> + {% endfor %} + </div> + {% else %} + <div class="alert alert-info"> + No gifts found matching "{{ query }}". Try a different search term. + </div> + {% endif %} + </div> +{% endblock %} \ No newline at end of file diff --git a/Server/templates/invitation/listshow.html.twig b/Server/templates/invitation/listshow.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..29dc71eb23d8a2551ac1e75d554fb88fed033a5e --- /dev/null +++ b/Server/templates/invitation/listshow.html.twig @@ -0,0 +1,146 @@ +{# + @authors + - YAO Jean-David (Binôme 13) + - AROUISSI Khaoula (Binôme 13) +#} +{% extends 'base.html.twig' %} + +{% block stylesheets %} + {{ parent() }} + <style> + .invitation-list { + margin: 40px auto; + max-width: 1200px; + } + .invitation-card { + background-color: #e8f4fa; /* soft light blue */ + border: 1px solid rgb(50, 139, 233); /* Atlantic blue */ + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; + } + .invitation-card:hover { + transform: translateY(-3px); + } + .invitation-title { + color: rgb(126, 222, 134); + font-size: 1.5rem; + margin-bottom: 15px; + text-align: center; + } + .invitation-info p { + font-size: 1rem; + line-height: 1.5; + margin: 5px 0; + } + .invitation-info strong { + color: rgb(0, 0, 0); + } + .status-pending { + color: #f0ad4e; + font-weight: bold; + } + .status-accepted { + color: #5cb85c; + font-weight: bold; + } + .status-rejected { + color: #d9534f; + font-weight: bold; + } + .btn-action { + display: inline-block; + background-color: rgb(126, 222, 134); + color: #fff; + border: none; + padding: 8px 16px; + border-radius: 4px; + text-decoration: none; + font-size: 0.9rem; + margin: 5px 3px; + transition: background-color 0.3s ease; + cursor: pointer; + } + .btn-action:hover { + background-color: #0056b3; + } + .btn-back { + display: inline-block; + background-color: #007bff; + color: #fff; + border: none; + padding: 8px 16px; + border-radius: 4px; + text-decoration: none; + font-size: 0.9rem; + margin: 5px 3px; + transition: background-color 0.3s ease; + } + .btn-back:hover { + background-color: #0056b3; + } + .card-actions { + text-align: center; + margin-top: 10px; + } + .navigation-buttons { + text-align: center; + margin-top: 30px; + } + </style> +{% endblock %} + +{% block title %}My Invitations{% endblock %} + +{% block body %} + <div class="invitation-list"> + <h1 style="text-align: center; color: rgb(126, 222, 134); margin-bottom: 30px;">My Invitations</h1> + {% if invitations is empty %} + <div class="alert alert-info" style="text-align: center;"> + You have no invitations at this time. + </div> + {% else %} + <div class="row"> + {% for invitation in invitations %} + <div class="col-md-4"> + <div class="invitation-card"> + <h2 class="invitation-title">{{ invitation.wishlist ? invitation.wishlist.name : 'N/A' }}</h2> + <div class="invitation-info"> + <p><strong>Sender:</strong> {{ invitation.sender ? invitation.sender.username : 'N/A' }}</p> + <p> + <strong>Status:</strong> + {% if invitation.accepted is null %} + <span class="status-pending">Pending</span> + {% elseif invitation.accepted %} + <span class="status-accepted">Accepted</span> + {% else %} + <span class="status-rejected">Rejected</span> + {% endif %} + </p> + </div> + <div class="card-actions"> + <a href="{{ path('invitation_show', {'id': invitation.id}) }}" class="btn-action">View</a> + {% if invitation.accepted is null %} + <form action="{{ path('invitation_accept', {'id': invitation.id}) }}" method="post" style="display:inline;"> + <input type="hidden" name="_token" value="{{ csrf_token('accept' ~ invitation.id) }}"> + <button type="submit" class="btn-action">Accept</button> + </form> + <form action="{{ path('invitation_reject', {'id': invitation.id}) }}" method="post" style="display:inline;"> + <input type="hidden" name="_token" value="{{ csrf_token('reject' ~ invitation.id) }}"> + <button type="submit" class="btn-action">Reject</button> + </form> + {% endif %} + </div> + </div> + </div> + {% endfor %} + </div> + {% endif %} + <div class="navigation-buttons"> + <a href="{{ path('invitation_list') }}" class="btn-back">Back to Invitations List</a> + <a href="{{ path('app_wishlists_index') }}" class="btn-back">Back to MyWishlists</a> + </div> + </div> +{% endblock %} diff --git a/Server/templates/invitation/show.html.twig b/Server/templates/invitation/show.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..3d2cc5c9ac3062853a54993c1aec0d8988960749 --- /dev/null +++ b/Server/templates/invitation/show.html.twig @@ -0,0 +1,125 @@ +{# + @authors + - YAO Jean-David (Binôme 13) + - AROUISSI Khaoula (Binôme 13) +#} +{% extends 'base.html.twig' %} + +{% block stylesheets %} + {{ parent() }} + <style> + .invitation-card { + background-color: #e8f4fa; /* soft light blue */ + border: 1px solid rgb(50, 139, 233); /* Atlantic blue */ + border-radius: 8px; + padding: 20px; + margin: 40px auto; + max-width: 600px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + } + .invitation-title { + color: rgb(126, 222, 134); + font-size: 2rem; + margin-bottom: 20px; + text-align: center; + } + .invitation-info p { + font-size: 1.1rem; + line-height: 1.5; + margin: 10px 0; + } + .invitation-info strong { + color: rgb(0, 0, 0); + } + .status-pending { + color: #f0ad4e; + font-weight: bold; + } + .status-accepted { + color: #5cb85c; + font-weight: bold; + } + .status-rejected { + color: #d9534f; + font-weight: bold; + } + .btn-action { + display: inline-block; + background-color: rgb(126, 222, 134); + color: #fff; + border: none; + padding: 10px 20px; + border-radius: 4px; + text-decoration: none; + font-size: 1rem; + margin: 10px 5px; + transition: background-color 0.3s ease; + cursor: pointer; + } + .btn-action:hover { + background-color: #0056b3; + } + .btn-back { + display: inline-block; + background-color: #007bff; + color: #fff; + border: none; + padding: 10px 20px; + border-radius: 4px; + text-decoration: none; + font-size: 1rem; + margin: 10px 5px; + transition: background-color 0.3s ease; + } + .btn-back:hover { + background-color: #0056b3; + } + .action-buttons { + text-align: center; + margin: 20px 0; + } + .navigation-buttons { + text-align: center; + margin-top: 30px; + } + </style> +{% endblock %} + +{% block title %}Invitation Details{% endblock %} + +{% block body %} + <div class="invitation-card"> + <h1 class="invitation-title">Invitation Details</h1> + <div class="invitation-info"> + <p><strong>Wishlist:</strong> {{ invitation.wishlist ? invitation.wishlist.name : 'N/A' }}</p> + <p><strong>Sender:</strong> {{ invitation.sender ? invitation.sender.username : 'N/A' }}</p> + <p><strong>Receiver:</strong> {{ invitation.receiver ? invitation.receiver.username : 'N/A' }}</p> + <p> + <strong>Status:</strong> + {% if invitation.accepted is same as(null) %} + <span class="status-pending">Pending</span> + {% elseif invitation.accepted == true %} + <span class="status-accepted">Accepted</span> + {% elseif invitation.accepted == false %} + <span class="status-rejected">Rejected</span> + {% endif %} + </p> + </div> + {% if invitation.accepted is null %} + <div class="action-buttons"> + <form action="{{ path('invitation_accept', {'id': invitation.id}) }}" method="post" style="display:inline;"> + <input type="hidden" name="_token" value="{{ csrf_token('accept' ~ invitation.id) }}"> + <button type="submit" class="btn-action">Accept</button> + </form> + <form action="{{ path('invitation_reject', {'id': invitation.id}) }}" method="post" style="display:inline;"> + <input type="hidden" name="_token" value="{{ csrf_token('reject' ~ invitation.id) }}"> + <button type="submit" class="btn-action">Reject</button> + </form> + </div> + {% endif %} + <div class="navigation-buttons"> + <a href="{{ path('invitation_list') }}" class="btn-back">Back to Invitations List</a> + <a href="{{ path('app_wishlists_index') }}" class="btn-back">Back to MyWishlists</a> + </div> + </div> +{% endblock %} diff --git a/Server/templates/item/creatingItem.html.twig b/Server/templates/item/creatingItem.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..cb4b303f9c2c16156a5473d881d1c0285cb59f46 --- /dev/null +++ b/Server/templates/item/creatingItem.html.twig @@ -0,0 +1,125 @@ +{# + + @authors + - CHAHBOUNE Nawal (Binôme 15) + - GHALLAB Houda (Binôme 15) +#} +{% extends 'base.html.twig' %} + +{% block title %}Ajouter un Item{% endblock %} + +{% block stylesheets %} +{{ parent() }} +<style> + .form-container { + max-width: 800px; + margin: 2rem auto; + padding: 2rem; + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + } + + .form-title { + font-size: 1.8rem; + color: #0f1d36; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid #eee; + } + + .form-group { + margin-bottom: 1.5rem; + } + + .form-label { + display: block; + font-weight: 600; + margin-bottom: 0.5rem; + color: #333; + } + + .form-control { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + } + + textarea.form-control { + min-height: 120px; + } + + .form-actions { + display: flex; + justify-content: space-between; + margin-top: 2rem; + } + + .btn { + padding: 0.75rem 1.5rem; + border-radius: 4px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + border: none; + font-size: 1rem; + } + + .btn-primary { + background-color: #0f1d36; + color: white; + } + + .btn-primary:hover { + background-color: #1a2e4c; + } + + .btn-secondary { + background-color: #e5e5e5; + color: #333; + } + + .btn-secondary:hover { + background-color: #d5d5d5; + } +</style> +{% endblock %} + +{% block body %} +<div class="form-container"> + <h1 class="form-title">Add New Item to {{ wishlist.name }}</h1> + + {{ form_start(form, {'attr': {'class': 'item-form'}}) }} + <div class="form-group"> + {{ form_label(form.name, 'Item Name', {'label_attr': {'class': 'form-label'}}) }} + {{ form_widget(form.name, {'attr': {'class': 'form-control', 'placeholder': 'Enter item name'}}) }} + {{ form_errors(form.name) }} + </div> + + <div class="form-group"> + {{ form_label(form.description, 'Description', {'label_attr': {'class': 'form-label'}}) }} + {{ form_widget(form.description, {'attr': {'class': 'form-control', 'placeholder': 'Describe the item (optional)'}}) }} + {{ form_errors(form.description) }} + </div> + + <div class="form-group"> + {{ form_label(form.price, 'Price (€)', {'label_attr': {'class': 'form-label'}}) }} + {{ form_widget(form.price, {'attr': {'class': 'form-control', 'placeholder': 'Enter price (optional)'}}) }} + {{ form_errors(form.price) }} + </div> + + <div class="form-group"> + {{ form_label(form.url, 'URL', {'label_attr': {'class': 'form-label'}}) }} + {{ form_widget(form.url, {'attr': {'class': 'form-control', 'placeholder': 'Enter a link where to buy this item (optional)'}}) }} + {{ form_errors(form.url) }} + </div> + + <div class="form-actions"> + <a href="{{ path('app_wishlist_show', {'id': wishlist.id}) }}" class="btn btn-secondary">Annuler</a> + <button type="submit" class="btn btn-primary">Enregistrer</button> + </div> + {{ form_end(form) }} +</div> +{% endblock %} \ No newline at end of file diff --git a/Server/templates/item/editItem.html.twig b/Server/templates/item/editItem.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..4bd3d2884a78facf64dbdaa707075605add60da4 --- /dev/null +++ b/Server/templates/item/editItem.html.twig @@ -0,0 +1,55 @@ +{# + + @authors + - CHAHBOUNE Nawal (Binôme 15) + - GHALLAB Houda (Binôme 15) +#} +{% extends 'base.html.twig' %} + +{% block title %}Modifier un item{% endblock %} + +{% block body %} +<div class="container mt-4"> + <div class="row justify-content-center"> + <div class="col-md-8"> + <div class="card"> + <div class="card-header"> + <h1 class="mb-0">Modifier l'item</h1> + </div> + <div class="card-body"> + {{ form_start(form) }} + <div class="mb-3"> + {{ form_label(form.name, 'Nom') }} + {{ form_widget(form.name, {'attr': {'class': 'form-control'}}) }} + {{ form_errors(form.name) }} + </div> + + <div class="mb-3"> + {{ form_label(form.description, 'Description') }} + {{ form_widget(form.description, {'attr': {'class': 'form-control'}}) }} + {{ form_errors(form.description) }} + </div> + + <div class="mb-3"> + {{ form_label(form.url, 'URL') }} + {{ form_widget(form.url, {'attr': {'class': 'form-control'}}) }} + {{ form_errors(form.url) }} + </div> + + <div class="mb-3"> + {{ form_label(form.price, 'Prix') }} + {{ form_widget(form.price, {'attr': {'class': 'form-control'}}) }} + {{ form_errors(form.price) }} + </div> + + <div class="d-flex justify-content-between"> + <a href="{{ path('app_wishlist_show', {'id': wishlist.id}) }}" class="btn btn-secondary">Annuler</a> + <button type="submit" class="btn btn-primary">Enregistrer les modifications</button> + </div> + {{ form_end(form) }} + </div> + </div> + </div> + </div> +</div> +{% endblock %} \ No newline at end of file diff --git a/Server/templates/item/index.html.twig b/Server/templates/item/index.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..bc864d4db638c2484bb68d4eebb3d51558d3ff72 --- /dev/null +++ b/Server/templates/item/index.html.twig @@ -0,0 +1,26 @@ +{# + + @authors + - CHAHBOUNE Nawal (Binôme 15) + - GHALLAB Houda (Binôme 15) +#} +{% extends 'base.html.twig' %} + +{% block title %}Hello ItemController!{% endblock %} + +{% block body %} +<style> + .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } + .example-wrapper code { background: #F5F5F5; padding: 2px 6px; } +</style> + +<div class="example-wrapper"> + <h1>Hello {{ controller_name }}! ✅</h1> + + This friendly message is coming from: + <ul> + <li>Your controller at <code>C:/wrk/perso/webapp/symfony-docker/symfony-docker/project/src/Controller/ItemController.php</code></li> + <li>Your template at <code>C:/wrk/perso/webapp/symfony-docker/symfony-docker/project/templates/item/index.html.twig</code></li> + </ul> +</div> +{% endblock %} diff --git a/Server/templates/items/index.html.twig b/Server/templates/items/index.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..3fb293b4f1ba64fd64427c29e8c459e2c43f236c --- /dev/null +++ b/Server/templates/items/index.html.twig @@ -0,0 +1,323 @@ +{# + + @authors + - KELA Wydie (Binôme 14) + - LALE Ny (Binôme 14) +#} +{% extends 'base.html.twig' %} + +{% block title %}{{ item.name }}{% endblock %} + +{% block stylesheets %} +{{ parent() }} +<style> + .item-detail-container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + } + + .item-detail { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3rem; + margin-top: 2rem; + background-color: white; + border-radius: 10px; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); + overflow: hidden; + } + + @media (max-width: 768px) { + .item-detail { + grid-template-columns: 1fr; + } + } + + .item-detail-image { + width: 100%; + height: 100%; + object-fit: cover; + max-height: 500px; + } + + .item-detail-content { + padding: 2rem; + display: flex; + flex-direction: column; + } + + .item-detail-title { + font-size: 2rem; + font-weight: 700; + color: #0f1d36; + margin-bottom: 1rem; + } + + .item-detail-price { + font-size: 1.75rem; + font-weight: 700; + color: #0f1d36; + margin-bottom: 2rem; + } + + .item-detail-description { + color: #444; + font-size: 1.1rem; + line-height: 1.6; + margin-bottom: 2rem; + } + + .item-detail-info { + background-color: #f9f9f9; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; + } + + .item-info-row { + display: flex; + margin-bottom: 0.8rem; + } + + .item-info-label { + font-weight: 600; + width: 150px; + color: #555; + } + + .item-detail-actions { + display: flex; + gap: 1rem; + margin-top: auto; + } + + .primary-btn { + padding: 0.8rem 1.5rem; + background-color: #0f1d36; + color: white; + border: none; + border-radius: 5px; + font-size: 1rem; + cursor: pointer; + text-decoration: none; + transition: background-color 0.3s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + } + + .primary-btn:hover { + background-color: #1a2e4c; + } + + .secondary-btn { + padding: 0.8rem 1.5rem; + background-color: white; + color: #0f1d36; + border: 1px solid #0f1d36; + border-radius: 5px; + font-size: 1rem; + cursor: pointer; + text-decoration: none; + transition: background-color 0.3s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + } + + .secondary-btn:hover { + background-color: #f5f5f5; + } + + .back-link { + display: inline-flex; + align-items: center; + color: #555; + text-decoration: none; + font-weight: 500; + margin-bottom: 1rem; + } + + .back-link:hover { + color: #0f1d36; + } + + .back-link i { + margin-right: 0.5rem; + } + + /* Pour les features spécifiques de l'item */ + .item-features { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1.5rem; + margin-top: 2rem; + } + + .feature-card { + background-color: white; + border-radius: 8px; + box-shadow: 0 3px 6px rgba(0,0,0,0.08); + padding: 1.5rem; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + } + + .feature-icon { + font-size: 2.5rem; + color: #0f1d36; + margin-bottom: 1rem; + } + + .feature-title { + font-weight: 600; + color: #333; + margin-bottom: 0.5rem; + } + + .feature-description { + color: #666; + font-size: 0.9rem; + } +</style> +{% endblock %} + +{% block body %} +<div class="item-detail-container"> + <a href="{{ path('app_items') }}" class="back-link"> + <i class="fas fa-arrow-left"></i> Retour aux cadeaux + </a> + + <div class="item-detail"> + <div class="item-image-wrapper"> + {% if item.imageUrl %} + <img src="{{ item.imageUrl }}" alt="{{ item.name }}" class="item-detail-image"> + {% else %} + <img src="{{ asset('images/gift-placeholder.jpg') }}" alt="Image non disponible" class="item-detail-image"> + {% endif %} + </div> + + <div class="item-detail-content"> + <h1 class="item-detail-title">{{ item.name }}</h1> + + {% if item.price %} + <div class="item-detail-price">{{ item.price|number_format(2, ',', ' ') }} €</div> + {% endif %} + + <div class="item-detail-description"> + {{ item.description|default('Aucune description disponible') }} + </div> + + <div class="item-detail-info"> + {% if item.category %} + <div class="item-info-row"> + <div class="item-info-label">Catégorie:</div> + <div>{{ item.category.name }}</div> + </div> + {% endif %} + + {% if item.storeLink %} + <div class="item-info-row"> + <div class="item-info-label">Où l'acheter:</div> + <div><a href="{{ item.storeLink }}" target="_blank">Voir en boutique</a></div> + </div> + {% endif %} + + <div class="item-info-row"> + <div class="item-info-label">Ajouté par:</div> + <div>{{ item.user ? item.user.username : 'Utilisateur inconnu' }}</div> + </div> + + <div class="item-info-row"> + <div class="item-info-label">Date d'ajout:</div> + <div>{{ item.createdAt ? item.createdAt|date('d/m/Y') : 'Date inconnue' }}</div> + </div> + </div> + + <div class="item-detail-actions"> + {% if app.user %} + <button class="primary-btn" id="add-to-wishlist-btn"> + <i class="fa-{% if item in app.user.wishlist %}solid{% else %}regular{% endif %} fa-heart"></i> + {% if item in app.user.wishlist %} + Retirer de ma liste + {% else %} + Ajouter à ma liste + {% endif %} + </button> + {% endif %} + + {% if item.storeLink %} + <a href="{{ item.storeLink }}" target="_blank" class="secondary-btn"> + <i class="fas fa-shopping-cart"></i> Acheter maintenant + </a> + {% endif %} + + {% if app.user and (app.user == item.user or is_granted('ROLE_ADMIN')) %} + <a href="#" class="secondary-btn"> + <i class="fas fa-edit"></i> Modifier + </a> + {% endif %} + </div> + </div> + </div> + + <div class="item-features"> + <div class="feature-card"> + <div class="feature-icon"> + <i class="fas fa-gift"></i> + </div> + <h3 class="feature-title">Idéal pour offrir</h3> + <p class="feature-description">Un cadeau parfait pour faire plaisir à vos proches.</p> + </div> + + <div class="feature-card"> + <div class="feature-icon"> + <i class="fas fa-medal"></i> + </div> + <h3 class="feature-title">Qualité garantie</h3> + <p class="feature-description">Des produits sélectionnés pour leur qualité et leur durabilité.</p> + </div> + + <div class="feature-card"> + <div class="feature-icon"> + <i class="fas fa-truck"></i> + </div> + <h3 class="feature-title">Livraison rapide</h3> + <p class="feature-description">Commandez directement depuis les sites partenaires pour une livraison rapide.</p> + </div> + </div> +</div> +{% endblock %} + +{% block javascripts %} +{{ parent() }} +<script> + document.addEventListener('DOMContentLoaded', function() { + const wishlistBtn = document.getElementById('add-to-wishlist-btn'); + + if (wishlistBtn) { + wishlistBtn.addEventListener('click', function() { + // On pourrait ajouter ici une requête AJAX pour mettre à jour la liste de souhaits + + // Animation visuelle temporaire + const icon = this.querySelector('i'); + icon.classList.toggle('fa-regular'); + icon.classList.toggle('fa-solid'); + + // Mettre à jour le texte du bouton + const text = this.textContent.trim(); + if (text.includes('Ajouter')) { + this.innerHTML = this.innerHTML.replace('Ajouter à ma liste', 'Retirer de ma liste'); + } else { + this.innerHTML = this.innerHTML.replace('Retirer de ma liste', 'Ajouter à ma liste'); + } + }); + } + }); +</script> +{% endblock %} \ No newline at end of file diff --git a/Server/templates/olditem/index.html.twig b/Server/templates/olditem/index.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..e260395d5a2e23013df76ceed09df35c9c2803cc --- /dev/null +++ b/Server/templates/olditem/index.html.twig @@ -0,0 +1,20 @@ +{% extends 'base.html.twig' %} + +{% block title %}Hello ItemController!{% endblock %} + +{% block body %} +<style> + .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } + .example-wrapper code { background: #F5F5F5; padding: 2px 6px; } +</style> + +<div class="example-wrapper"> + <h1>Hello {{ controller_name }}! ✅</h1> + + This friendly message is coming from: + <ul> + <li>Your controller at <code>C:/wrk/perso/webapp/symfony-docker/symfony-docker/project/src/Controller/ItemController.php</code></li> + <li>Your template at <code>C:/wrk/perso/webapp/symfony-docker/symfony-docker/project/templates/item/index.html.twig</code></li> + </ul> +</div> +{% endblock %} diff --git a/Server/templates/proof/edit_proof.html.twig b/Server/templates/proof/edit_proof.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..9812078d059e2f4d07d9b64eb507923e882c132b --- /dev/null +++ b/Server/templates/proof/edit_proof.html.twig @@ -0,0 +1,60 @@ +{# + @authors + - YAO Jean-David (Binôme 13) + - AROUISSI Khaoula (Binôme 13) +#} +{# filepath: templates/proof/edit_proof.html.twig #} +{% extends 'base.html.twig' %} + +{% block title %}Edit Proof | GiftWish{% endblock %} + +{% block stylesheets %} + {{ parent() }} + <link rel="stylesheet" href="{{ asset('css/style.css') }}"> + <style> + .item-details { + text-align: center; + margin-top: 20px; + } + .item-details h2 { + font-size: 2rem; + margin-bottom: 10px; + } + .item-details p { + margin: 5px 0; + } + </style> +{% endblock %} + +{% block body %} +<div class="container py-3"> + <div class="row g-4"> + <!-- Left side: Item details (text-only) --> + <div class="col-md-5 text-center item-details"> + <h2 class="mt-3">{{ item.name }}</h2> + <p class="text-muted">from wishlist of {{ item.wishlist.owner.username }}</p> + <p class="fs-3 fw-bold">${{ item.price }}</p> + </div> + + <!-- Right side: Proof edit form --> + <div class="col-md-7"> + <div class="p-3 rounded" style="background-color: #d7f7cb;"> + <h4 class="mb-3">Edit Your Proof</h4> + + <form action="{{ path('edit_proof', {'id': proof.id}) }}" method="post" enctype="multipart/form-data"> + <input type="hidden" name="itemId" value="{{ item.id }}"> + <div class="mb-3"> + <label for="proofFile" class="form-label">Change file (optional)</label> + <input type="file" class="form-control" id="proofFile" name="proofFile" accept="image/*"> + </div> + <div class="mb-3"> + <label for="congratsMsg" class="form-label">Edit your congratulatory message</label> + <textarea class="form-control" id="congratsMsg" name="congratsMsg" rows="3">{{ proof.congratsMessage }}</textarea> + </div> + <button type="submit" class="btn btn-primary">Update Proof</button> + </form> + </div> + </div> + </div> +</div> +{% endblock %} diff --git a/Server/templates/proof/proof.html.twig b/Server/templates/proof/proof.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..83d03e777b4b7b415a1b7c2e288043b867d2bd7c --- /dev/null +++ b/Server/templates/proof/proof.html.twig @@ -0,0 +1,60 @@ +{# + @authors + - YAO Jean-David (Binôme 13) + - AROUISSI Khaoula (Binôme 13) +#} +{# filepath: templates/proof/proof.html.twig #} +{% extends 'base.html.twig' %} + +{% block title %}Gift Proof | GiftWish{% endblock %} + +{% block stylesheets %} + {{ parent() }} + <link rel="stylesheet" href="{{ asset('css/style.css') }}"> + <style> + .item-details { + text-align: center; + margin-top: 20px; + } + .item-details h2 { + font-size: 2rem; + margin-bottom: 10px; + } + .item-details p { + margin: 5px 0; + } + </style> +{% endblock %} + +{% block body %} +<div class="container py-3"> + <div class="row g-4"> + <!-- Left side: Item details (text-only) --> + <div class="col-md-5 text-center item-details"> + <h2 class="mt-3">{{ item.name }}</h2> + <p class="text-muted">from wishlist of {{ item.wishlist.owner.username }}</p> + <p class="fs-3 fw-bold">${{ item.price }}</p> + </div> + + <!-- Right side: Proof upload form --> + <div class="col-md-7"> + <div class="p-3 rounded" style="background-color: #d7f7cb;"> + <h4 class="mb-3">Validate your purchase</h4> + <form action="{{ path('upload_proof') }}" method="post" enctype="multipart/form-data"> + <!-- Hidden input to pass the itemId --> + <input type="hidden" name="itemId" value="{{ itemId }}"> + <div class="mb-3"> + <label for="proofFile" class="form-label">Upload file (proof of purchase)</label> + <input type="file" class="form-control" id="proofFile" name="proofFile" accept="image/*"> + </div> + <div class="mb-3"> + <label for="congratsMsg" class="form-label">Add a congratulatory message</label> + <textarea class="form-control" id="congratsMsg" name="congratsMsg" rows="3"></textarea> + </div> + <button type="submit" class="btn btn-primary">Validate</button> + </form> + </div> + </div> + </div> +</div> +{% endblock %} diff --git a/Server/templates/security/login.html.twig b/Server/templates/security/login.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..baf94dcad689d830f30608ef619da976236e8f86 --- /dev/null +++ b/Server/templates/security/login.html.twig @@ -0,0 +1,159 @@ + +{# + + @authors + - KELA Wydie (Binôme 14) + - LALE Ny (Binôme 14) +#} +{% block stylesheets %} +<style> + :root { + --primary-color: rgb(29, 187, 226); + --accent-color: #f6f6f6; + --border-radius: 5px; + } + + .login-container { + display: flex; + min-height: 100vh; + } + + .login-sidebar { + flex: 1; + background-color: #f5f5f5; + display: flex; + flex-direction: column; + justify-content: center; + padding: 2rem; + position: relative; + } + + .login-content { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 2rem; + } + + .login-form { + width: 100%; + max-width: 320px; + } + + .form-control { + width: 100%; + padding: 0.75rem 1rem; + margin-bottom: 1.5rem; + border: 1px solid #ddd; + border-radius: var(--border-radius); + font-size: 1rem; + } + + .login-btn { + width: 100%; + padding: 0.75rem; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: var(--border-radius); + font-size: 1rem; + cursor: pointer; + margin-top: 1rem; + } + + .logo { + font-family: 'Pacifico', cursive; + font-size: 2.5rem; + color: white; + margin-bottom: 1rem; + position: absolute; + top: 2rem; + left: 2rem; + } + + .tagline { + font-size: 1.5rem; + font-weight: 600; + color: white; + margin-bottom: 1rem; + line-height: 1.3; + } + + .signup-prompt { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 3rem; + } + + .signup-link { + font-weight: 600; + padding: 0.5rem 1rem; + text-decoration: none; + color: var(--primary-color); + } + + .backdrop-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.3); + z-index: 1; + } + + .sidebar-content { + position: relative; + z-index: 2; + color: white; + } + + .footer { + position: absolute; + bottom: 1rem; + right: 1rem; + font-size: 0.8rem; + color: #999; + } +</style> +{% endblock %} + +{% block body %} +<div class="login-container"> + <div class="login-sidebar" style="background-image: url('https://thumbs.dreamstime.com/b/close-up-hands-giving-beautifully-wrapped-gift-to-another-person-scene-conveys-warmth-care-symbolizes-person-352951009.jpg'); background-size: cover; background-position: center;"> + <div class="backdrop-overlay"></div> + <div class="sidebar-content"> + <div class="tagline">Offrez du bonheur,<br>un cadeau à la fois !</div> + </div> + </div> + + <div class="login-content"> + <div class="login-form"> + <h1 style="font-size: 2rem; margin-bottom: 2rem; color: var(--primary-color); text-align: center;">Log in</h1> + + <form method="post"> + {% if error %} + <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div> + {% endif %} + + <input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus> + + <input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required> + + <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"> + + <button type="submit" class="login-btn">Log in</button> + </form> + + <div class="signup-prompt"> + <span>Not a member ?</span> + <a href="{{path('app_signup')}}" class="signup-link">Sign up</a> + </div> + </div> + + </div> +</div> +{% endblock %} \ No newline at end of file diff --git a/Server/templates/security/signup.html.twig b/Server/templates/security/signup.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..b1c80ef645beb37c37ceb060f54c68a9fbe3c668 --- /dev/null +++ b/Server/templates/security/signup.html.twig @@ -0,0 +1,188 @@ + +{# + + @authors + - KELA Wydie (Binôme 14) + - LALE Ny (Binôme 14) +#} +{% block stylesheets %} +<style> + :root { + --primary-color:rgb(29, 187, 226); + --accent-color: #f6f6f6; + --border-radius: 5px; + } + + .signup-container { + display: flex; + min-height: 100vh; + } + + .signup-sidebar { + flex: 1; + background-size: cover; + background-position: center; + display: flex; + flex-direction: column; + justify-content: center; + position: relative; + } + + .backdrop-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.3); + z-index: 1; + } + + .sidebar-content { + position: relative; + z-index: 2; + color: white; + padding: 2rem; + } + + .logo { + font-family: 'Pacifico', cursive; + font-size: 2.5rem; + color: white; + margin-bottom: 1rem; + } + + .tagline { + font-size: 1.5rem; + font-weight: 600; + color: white; + margin-bottom: 1rem; + line-height: 1.3; + } + + .signup-content { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 2rem; + background-color: var(--accent-color); + } + + .signup-form { + width: 100%; + max-width: 320px; + } + + .form-group { + margin-bottom: 1.5rem; + } + + .form-control { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid #ddd; + border-radius: var(--border-radius); + font-size: 1rem; + } + + .signup-btn { + width: 100%; + padding: 0.75rem; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: var(--border-radius); + font-size: 1rem; + cursor: pointer; + margin-top: 0.5rem; + } + + .signin-prompt { + text-align: center; + margin-top: 2rem; + } + + .signin-link { + font-weight: 600; + text-decoration: none; + color: var(--primary-color); + margin-left: 0.5rem; + } +</style> +{% endblock %} + +{% block body %} +<div class="signup-container"> + <div class="signup-sidebar" style="background: url('https://thumbs.dreamstime.com/b/close-up-hands-giving-beautifully-wrapped-gift-to-another-person-scene-conveys-warmth-care-symbolizes-person-352951009.jpg');"> + <div class="backdrop-overlay"></div> + <div class="sidebar-content"> + <div class="tagline">Offrez du bonheur,<br>un cadeau à la fois !</div> + </div> + </div> + + <div class="signup-content"> + <div class="signup-form"> + <h1 class="title" style="font-size: 2rem; margin-bottom: 2rem; color: var(--primary-color); text-align: center;">Sign up</h1> + + {% for flash_error in app.flashes('verify_email_error') %} + <div class="alert alert-danger" role="alert">{{ flash_error }}</div> + {% endfor %} + + {% for flash_message in app.flashes('success') %} + <div class="alert alert-success" role="alert">{{ flash_message }}</div> + {% endfor %} + + {{ form_start(registrationForm) }} + <div class="form-group"> + {{ form_row(registrationForm.email, { + 'attr': {'class': 'form-control', 'placeholder': 'Email address'} + }) }} + </div> + + <div class="form-group"> + {{ form_row(registrationForm.username, { + 'attr': {'class': 'form-control', 'placeholder': 'Nom d\'utilisateur'} + }) }} + </div> + + <div class="form-group"> + {{ form_row(registrationForm.firstName, { + 'attr': {'class': 'form-control', 'placeholder': 'Prénom'} + }) }} + </div> + + <div class="form-group"> + {{ form_row(registrationForm.lastName, { + 'attr': {'class': 'form-control', 'placeholder': 'Nom'} + }) }} + </div> + + <div class="form-group"> + {{ form_row(registrationForm.plainPassword.first, { + 'attr': {'class': 'form-control', 'placeholder': 'Mot de passe'} + }) }} + </div> + + <div class="form-group"> + {{ form_row(registrationForm.plainPassword.second, { + 'attr': {'class': 'form-control', 'placeholder': 'Confirmer le mot de passe'} + }) }} + </div> + + <div class="form-group"> + {{ form_row(registrationForm.agreeTerms) }} + </div> + + <button type="submit" class="signup-btn">Sign up</button> + {{ form_end(registrationForm) }} + + <div class="signin-prompt"> + <span>Already a member?</span> + <a href="{{ path('app_login') }}" class="signin-link">Sign in</a> + </div> + </div> + </div> +</div> +{% endblock %} \ No newline at end of file diff --git a/Server/templates/wishlist/index.html.twig b/Server/templates/wishlist/index.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..4cbd9f857f280e4147bc3b0573ad2a38013ae928 --- /dev/null +++ b/Server/templates/wishlist/index.html.twig @@ -0,0 +1,364 @@ +{# + + @authors + - KELA Wydie (Binôme 14) + - LALE Ny (Binôme 14) +#} +{% extends 'base.html.twig' %} + +{% block title %}{{ wishlist.name }} - Liste de souhaits{% endblock %} + +{% block stylesheets %} + {{ parent() }} + <style> + .shared-wishlist-container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + box-shadow: 0 3px 8px rgba(0,0,0,0.05); + } + + .shared-header { + background-color: #f9f9f9; + padding: 2rem; + border-radius: 8px; + margin-bottom: 2rem; + position: relative; + } + + .shared-title { + font-size: 2.5rem; + font-weight: 700; + color: #0f1d36; + margin-bottom: 1rem; + } + + .shared-meta { + display: flex; + gap: 2rem; + color: #777; + font-size: 0.9rem; + } + + .btn-group-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: flex-end; + align-items: center; + } + + .btn { + padding: 0.7rem 1.2rem; + border: none; + border-radius: 5px; + font-size: 1rem; + cursor: pointer; + text-decoration: none; + transition: background-color 0.3s ease; + white-space: nowrap; + } + + .btn-back { + background-color: rgb(77, 219, 124); + color: #fff; + } + + .btn-back:hover { + background-color: #0056b3; + } + + .btn-creer { + background-color: rgb(208, 144, 35); + color: #fff; + } + + .btn-creer:hover { + background-color: #1a2e4c; + } + + .btn-supprimer { + background-color: #d9534f; + color: #fff; + } + + .btn-supprimer:hover { + background-color: #c9302c; + } + + .btn-disabled { + background-color: #ccc !important; + cursor: not-allowed; + } + + .sort-buttons { + display: flex; + gap: 1rem; + margin-bottom: 1rem; + justify-content: center; + } + + /* List styling for items */ + .items-list { + list-style: none; + padding: 0; + margin: 0; + } + + .item-card { + display: flex; + align-items: center; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + transition: transform 0.3s ease; + position: relative; + } + + .item-card:hover { + transform: translateY(-2px); + } + + .item-card.purchased { + opacity: 0.6; + } + + .item-content { + flex: 1; + /* Pour aligner les éléments sur une même colonne */ + } + + .item-header { + display: flex; + align-items: center; + justify-content: space-between; + } + + .item-name { + font-size: 1.2rem; + font-weight: 600; + color: #0f1d36; + margin: 0; + } + + .item-price { + font-size: 1rem; + font-weight: bold; + color: #0f1d36; + background-color: #e0e0e0; + padding: 0.3rem 0.6rem; + border-radius: 20px; + } + + .item-description { + color: #666; + font-size: 0.95rem; + margin: 0.5rem 0; + } + + .item-action { + margin-top: 0.5rem; + } + + .item-action .view-btn { + text-decoration: none; + font-size: 0.95rem; + color: #007bff; + } + + /* Purchased overlay styles */ + .purchased-info { + display: none; + margin-top: 0.5rem; + background: rgba(255, 255, 255, 0.9); + padding: 0.5rem; + border-radius: 4px; + } + .item-card.purchased:hover .purchased-info { + display: block; + } + /* Style pour la section des collaborateurs */ +.wishlist-collaborators { + margin: 10px 0; + padding: 10px; + background-color: #f8f9fa; + border-radius: 5px; +} + +.wishlist-collaborators h5 { + font-size: 1rem; + margin-bottom: 5px; + color: #495057; +} + +.wishlist-owner { + font-size: 0.9rem; + color: #6c757d; + margin-bottom: 1rem; +} + +/* Badges pour les collaborateurs */ +.badge.bg-info { + background-color: #17a2b8 !important; + color: white; + padding: 5px 10px; + font-weight: 500; + font-size: 0.8rem; +} + </style> +{% endblock %} + +{% block body %} +<div class="shared-wishlist-container"> + <!-- Header avec titre et actions --> + <div class="d-flex justify-content-between align-items-center mb-4"> +<div class="wishlist-header"> + <h1>{{ wishlist.name }}</h1> + + {# Affichage du propriétaire si je suis collaborateur #} + {% if app.user != wishlist.owner %} + <p class="wishlist-owner"> + <i class="fas fa-user"></i> + Liste créée par <strong>{{ wishlist.owner.username }}</strong> + </p> + {% endif %} + + {# Affichage des collaborateurs si je suis propriétaire #} + {% if app.user == wishlist.owner and wishlist.collaborators|length > 0 %} + <div class="wishlist-collaborators mb-3"> + <h5>Partagée avec :</h5> + <div class="d-flex flex-wrap"> + {% for collaborator in wishlist.collaborators %} + <span class="badge bg-info me-2 mb-1">{{ collaborator.username }}</span> + {% endfor %} + </div> + </div> + {% endif %} + + {# Affichage de la date limite #} + <p class="wishlist-deadline {% if wishlist.isExpired() %}text-danger{% endif %}"> + <i class="far fa-calendar-alt"></i> + {% if wishlist.deadline %} + Date limite: {{ wishlist.deadline|date('d/m/Y') }} + {% if wishlist.isExpired() %} <span class="badge bg-danger">Expirée</span>{% endif %} + {% else %} + Sans date limite + {% endif %} + </p> + + +</div> + <div class="btn-group-actions"> + <a href="{{ path('app_wishlists_index') }}" class="btn btn-back"> + <i class="fas fa-arrow-left"></i> Retour à mes listes + </a> + + {% if not wishlist.isExpired() and (wishlist.owner == app.user or app.user in wishlist.collaborators) %} + <a href="{{ path('add_item', {'idWishlist': wishlist.id}) }}" class="btn btn-creer"> + Ajouter un cadeau + </a> + {% else %} + <button class="btn btn-creer btn-disabled" disabled> + Ajouter un cadeau + </button> + {% endif %} + + {% if wishlist.owner == app.user %} + <form method="post" action="{{ path('app_wishlist_delete_wishlist', {'id': wishlist.id}) }}" + onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cette liste?');" style="margin: 0;"> + <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ wishlist.id) }}"> + <button type="submit" class="btn btn-supprimer"> + Supprimer la liste + </button> + </form> + {% endif %} + </div> + </div> + + <!-- Header meta --> + <div class="shared-header"> + <div class="shared-meta"> + <div class="shared-meta-item"> + <i class="fas fa-gift"></i> + <span>{{ items|length }} cadeaux</span> + </div> + </div> + </div> + + <!-- Boutons de tri --> + <div class="sort-buttons"> + <a href="{{ path('app_wishlist_show', {'id': wishlist.id, 'sort': 'asc'}) }}" class="btn btn-creer"> + Prix Croissant + </a> + <a href="{{ path('app_wishlist_show', {'id': wishlist.id, 'sort': 'desc'}) }}" class="btn btn-creer"> + Prix Décroissant + </a> + </div> + + <!-- Liste des items --> + <ul class="items-list"> + {% if items|length > 0 %} + {% for item in items %} + <li class="item-card {{ item.hasPurchased ? 'purchased' : '' }}"> + <div class="item-content"> + <div class="item-header"> + <h3 class="item-name">{{ item.name }}</h3> + {% if item.price %} + <span class="item-price">{{ item.price|number_format(2, ',', ' ') }} €</span> + {% endif %} + </div> + <p class="item-description">{{ item.description|default('Aucune description disponible') }}</p> + + {# Bouton "Add Proof" s'il faut ajouter une preuve et que l'item n'est pas acheté #} + {% if aim is defined and aim == 'toBuy' and (not item.hasPurchased) %} + <button class="btn btn-primary" onclick="window.location.href='{{ path('gift_proof_form', {'id': item.id}) }}'"> + Add Proof + </button> + {% endif %} + + {# Affichage des infos de la preuve si l'item est acheté #} + {% if item.hasPurchased and item.proof is defined %} + <div class="purchased-info"> + <strong>{{ item.proof.buyer.username }}</strong> + <p>{{ item.proof.congratsMessage }}</p> + {% if app.user and item.proof.buyer.id == app.user.id %} + <a href="{{ path('edit_proof', {'id': item.proof.id}) }}">Edit Message</a> + {% endif %} + </div> + {% endif %} + + <div class="item-action"> + {% if item.url %} + {% set fixedUrl = item.url starts with 'http' ? item.url : 'https://' ~ item.url %} + <a href="{{ fixedUrl }}" target="_blank" class="view-btn"> + <i class="fas fa-shopping-cart"></i> Voir en boutique + </a> + {% else %} + <p class="view-btn">Aucun URL n'est fourni!</p> + {% endif %} + </div> + </div> + + <form method="post" action="{{ path('delete_item', {'idWishlist': wishlist.id, 'idItem': item.id}) }}" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cet élément ?');" style="margin-left: auto;"> + <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ item.id) }}"> + <button type="submit" class="btn btn-supprimer"> + <i class="fas fa-trash"></i> + </button> + </form> + </li> + {% endfor %} + {% else %} + <li class="d-flex gap-2 justify-content-center"> + <a href="{{ path('app_wishlists_index') }}" class="btn btn-retour"> + <i class="fas fa-arrow-left"></i> Retour à mes listes + </a> + <a href="{{ path('app_home') }}" class="btn btn-retour"> + <i class="fas fa-home"></i> Retour à l'accueil + </a> + </li> + {% endif %} + </ul> +</div> +{% endblock %} diff --git a/Server/templates/wishlists/create.html.twig b/Server/templates/wishlists/create.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..0f2ed60bb8a9420db96be53ae351fb04cad9ed08 --- /dev/null +++ b/Server/templates/wishlists/create.html.twig @@ -0,0 +1,142 @@ +{# + + @authors + - KELA Wydie (Binôme 14) + - LALE Ny (Binôme 14) +#} +{% extends 'base.html.twig' %} + +{% block title %}Créer une nouvelle liste de souhaits{% endblock %} + +{% block body %} +<div class="container mt-5"> + <div class="row"> + <div class="col-md-8 mx-auto"> + <div class="card shadow-lg border-light rounded-3"> + <div class="card-header text-white text-center" style="background-color: #00B8DE;"> + <h1 class="h3 mb-0">Créer une nouvelle liste de souhaits</h1> + </div> + <div class="card-body p-5"> + {{ form_start(form) }} + + <!-- Champs pour le nom et la date limite --> + <div class="mb-4"> + {{ form_row(form.name) }} + </div> + <div class="mb-4"> + {{ form_row(form.deadline) }} + </div> + + <!-- Section pour partager la liste via l'ajout d'un collaborateur --> + <div class="mb-4"> + <label for="collaborator_username" class="fw-bold text-dark">Partager avec</label> + <div class="input-group"> + <input type="text" id="collaborator_username" class="form-control" placeholder="Entrez le username de la personne"> + <button type="button" id="add_collaborator_btn" class="btn btn-outline-secondary">Ajouter</button> + </div> + <small class="form-text text-muted">Ajoutez le username de la personne avec laquelle vous souhaitez partager la liste.</small> + </div> + + <!-- Conteneur pour afficher les collaborateurs ajoutés --> + <div id="collaborators_list" class="mb-4"> + <ul class="list-group"></ul> + </div> + + <!-- Champ caché pour stocker les usernames (séparés par une virgule) --> + <input type="hidden" id="collaborators_hidden" name="collaborators_hidden" value=""> + + <div class="d-grid gap-3 d-md-flex justify-content-md-between mt-4"> + <a href="{{ path('app_wishlists_index') }}" class="btn btn-outline-dark px-4 py-2 shadow-sm mb-3"> + <i class="fas fa-times-circle me-2"></i> Annuler + </a> + <button type="submit" class="btn btn-primary px-4 py-2 shadow-sm mt-3"> + <i class="fas fa-save me-2"></i> Créer la liste + </button> + </div> + + {{ form_end(form) }} + </div> + </div> + </div> + </div> +</div> +{% endblock %} + +{% block javascripts %} + {{ parent() }} + <script> + document.addEventListener('DOMContentLoaded', function() { + const addBtn = document.getElementById('add_collaborator_btn'); + const collaboratorInput = document.getElementById('collaborator_username'); + const collaboratorsList = document.querySelector('#collaborators_list ul'); + const hiddenField = document.getElementById('collaborators_hidden'); + + // Met à jour le champ caché en concaténant les usernames ajoutés + function updateHiddenField() { + const usernames = []; + collaboratorsList.querySelectorAll('li').forEach(li => { + usernames.push(li.getAttribute('data-username')); + }); + hiddenField.value = usernames.join(','); + } + + addBtn.addEventListener('click', function() { + let username = collaboratorInput.value.trim(); + console.log("Avant nettoyage: " + username); + // Nettoyer : retirer les guillemets s'ils sont présents en début et fin de chaîne + if ((username.startsWith('"') && username.endsWith('"')) || + (username.startsWith("'") && username.endsWith("'"))) { + username = username.substring(1, username.length - 1); + } + console.log("Après nettoyage: " + username); + + if (!username) { + alert('Veuillez saisir un username.'); + return; + } + // Vérifier si le username est déjà ajouté + if (collaboratorsList.querySelector(`li[data-username="${username}"]`)) { + alert('Ce collaborateur est déjà ajouté.'); + return; + } + // Envoi AJAX pour vérifier l'existence du username + fetch('{{ path("app_check_username") }}', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest' + }, + body: 'username=' + encodeURIComponent(username) + }) + .then(response => { + if (!response.ok) { + throw new Error('Utilisateur non trouvé'); + } + return response.json(); + }) + .then(data => { + // Ajoute le collaborateur dans la liste affichée + const li = document.createElement('li'); + li.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-center'); + li.setAttribute('data-username', username); + li.textContent = username; + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.classList.add('btn', 'btn-sm', 'btn-danger'); + removeBtn.textContent = 'X'; + removeBtn.addEventListener('click', function() { + li.remove(); + updateHiddenField(); + }); + li.appendChild(removeBtn); + collaboratorsList.appendChild(li); + updateHiddenField(); + collaboratorInput.value = ''; + }) + .catch(error => { + alert('Le username "' + username + '" n\'existe pas.'); + }); + }); + }); + </script> +{% endblock %} diff --git a/Server/templates/wishlists/edit.html.twig b/Server/templates/wishlists/edit.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..d0d87b4e15afa9353967488f4c05e4239beabb6f --- /dev/null +++ b/Server/templates/wishlists/edit.html.twig @@ -0,0 +1,155 @@ +{# + + @authors + - KELA Wydie (Binôme 14) + - LALE Ny (Binôme 14) +#} +{% extends 'base.html.twig' %} + +{% block title %}Modifier la liste de souhaits{% endblock %} + +{% block body %} +<div class="container mt-5"> + <div class="row"> + <div class="col-md-8 mx-auto"> + <div class="card shadow-lg border-light rounded-3"> + <div class="card-header text-white text-center" style="background-color: #00B8DE;"> + <h1 class="h3 mb-0">Modifier la liste de souhaits</h1> + </div> + <div class="card-body p-5"> + {{ form_start(form) }} + + <!-- Champs pour le nom et la date limite --> + <div class="mb-4"> + {{ form_row(form.name) }} + </div> + <div class="mb-4"> + {{ form_row(form.deadline) }} + </div> + + <!-- Section pour partager avec (ajout dynamique de collaborateurs) --> + <div class="mb-4"> + <label for="collaborator_username" class="fw-bold text-dark">Partager avec</label> + <div class="input-group"> + <input type="text" id="collaborator_username" class="form-control" placeholder="Entrez le username de la personne"> + <button type="button" id="add_collaborator_btn" class="btn btn-outline-secondary">Ajouter</button> + </div> + <small class="form-text text-muted"> + Ajoutez le username de la personne avec laquelle vous souhaitez partager la liste. + </small> + </div> + + <!-- Zone d'affichage des "boxes" de collaborateurs --> + <div id="collaborators_list" class="mb-4"> + <ul class="list-group"></ul> + </div> + + <!-- Champ caché pour stocker les usernames (séparés par une virgule) --> + <input type="hidden" id="collaborators_hidden" name="collaborators_hidden" value=""> + + <div class="d-grid gap-3 d-md-flex justify-content-md-between mt-4"> + <a href="{{ path('app_wishlists_index') }}" class="btn btn-outline-dark px-4 py-2 shadow-sm mb-3"> + <i class="fas fa-times-circle me-2"></i> Annuler + </a> + <button type="submit" class="btn btn-primary px-4 py-2 shadow-sm mt-3"> + <i class="fas fa-save me-2"></i> Enregistrer les modifications + </button> + </div> + + {{ form_end(form) }} + </div> + </div> + </div> + </div> +</div> +{% endblock %} + +{% block javascripts %} + {{ parent() }} + <script> + document.addEventListener('DOMContentLoaded', function() { + const addBtn = document.getElementById('add_collaborator_btn'); + const collaboratorInput = document.getElementById('collaborator_username'); + const collaboratorsList = document.querySelector('#collaborators_list ul'); + const hiddenField = document.getElementById('collaborators_hidden'); + + // Update hidden field from the list boxes + function updateHiddenField() { + const usernames = []; + collaboratorsList.querySelectorAll('li').forEach(li => { + usernames.push(li.getAttribute('data-username')); + }); + hiddenField.value = usernames.join(','); + } + + // Function to add a collaborator box + function addCollaboratorBox(username) { + const li = document.createElement('li'); + li.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-center'); + li.setAttribute('data-username', username); + li.textContent = username; + + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.classList.add('btn', 'btn-sm', 'btn-danger'); + removeBtn.textContent = 'X'; + removeBtn.addEventListener('click', function() { + li.remove(); + updateHiddenField(); + }); + li.appendChild(removeBtn); + + collaboratorsList.appendChild(li); + updateHiddenField(); + } + + // Pre-fill existing collaborators (for debugging, log the value) + const existingCollaborators = {{ wishlist.collaborators|map(c => c.username)|json_encode|raw }}; + console.log("Existing collaborators:", existingCollaborators); + existingCollaborators.forEach(username => { + addCollaboratorBox(username); + }); + + // Add new collaborator via AJAX when clicking "Ajouter" + addBtn.addEventListener('click', function() { + let username = collaboratorInput.value.trim(); + console.log("Avant nettoyage:", username); + // Remove surrounding quotes if present + username = username.replace(/^['"]+|['"]+$/g, ''); + console.log("Après nettoyage:", username); + + if (!username) { + alert('Veuillez saisir un username.'); + return; + } + // Check if already added + if (collaboratorsList.querySelector(`li[data-username="${username}"]`)) { + alert('Ce collaborateur est déjà ajouté.'); + return; + } + // AJAX call to verify username exists + fetch('{{ path("app_check_username") }}', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest' + }, + body: 'username=' + encodeURIComponent(username) + }) + .then(response => { + if (!response.ok) { + throw new Error('Utilisateur non trouvé'); + } + return response.json(); + }) + .then(data => { + addCollaboratorBox(username); + collaboratorInput.value = ''; + }) + .catch(error => { + alert('Le username "' + username + '" n\'existe pas.'); + }); + }); + }); + </script> +{% endblock %} diff --git a/Server/templates/wishlists/index.html.twig b/Server/templates/wishlists/index.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..836ebd5aa76ea0d94aad41768f6350ea221c273b --- /dev/null +++ b/Server/templates/wishlists/index.html.twig @@ -0,0 +1,424 @@ +{# + + @authors + - KELA Wydie (Binôme 14) + - LALE Ny (Binôme 14) +#} +{# templates/wishlist/index.html.twig #} +{% extends 'base.html.twig' %} + +{% block stylesheets %} + {{ parent() }} + <style> + /* Style pour centrer la popup */ + .share-popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + } + .share-popup-content { + background-color: white; + padding: 20px; + border-radius: 8px; + width: 90%; + max-width: 500px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + } + /* Style pour alignement horizontal des wishlists */ + .wishlist-container { + display: flex; + flex-wrap: wrap; + gap: 15px; + justify-content: flex-start; + } + .wishlist-card { + flex: 0 0 auto; + width: 340px; + margin-bottom: 20px; + } + .hidden { + display: none !important; + } + </style> +{% endblock %} + +{% block title %}Mes listes de souhaits{% endblock %} + +{% block body %} +<div class="container"> + + <div class="d-flex justify-content-between align-items-center mb-4"> + <div> + <h4>Mes listes de souhaits</h4> + </div> + <div> + <a href="{{ path('app_wishlists_create') }}" class="btn btn-creer">Créer une nouvelle liste</a> + </div> + </div> + + {# Zone d'image avec augmentation de l'espace sous l'image #} + <div class="profile-header mb-4"> + <img src="{{ asset('images/gifts.jpg') }}" alt="cover pic" class="header-image"> + <div class="profile-pic-container"> + <img src="{{ asset('images/blank_prof_pic.jpg') }}" alt="Profile Picture" class="profile-pic"> + </div> + </div> + + <div class="row-md-4 mb-4"> + {% if is_granted('ROLE_USER') %} + <h3>{{ app.user.username }}</h3> + {% endif %} + </div> + + <div class="container" style="margin-top: 100px;"> {# Augmentation de l'espace entre l'image et la zone de boutons #} + {% if wishlists is empty %} + <div class="alert alert-info">Vous n'avez pas encore créé de liste de souhaits.</div> + {% else %} + <div class="wishlist-container"> + {% for wishlist in wishlists %} + <div class="wishlist-card"> + <div class="card h-100 {% if wishlist.isExpired() %}card-expired{% endif %}"> + <div class="card-body d-flex flex-column h-100"> + {# Titre de la wishlist #} + <h5 class="card-title mb-3"> + {{ wishlist.name }} + {% if wishlist.isExpired() %}<span class="badge bg-secondary">Expirée</span>{% endif %} + </h5> + + {% if wishlist.collaborators|length > 0 %} + <div class="mt-2 mb-3"> + <h6 class="text-muted">Partagée avec:</h6> + <div class="d-flex flex-wrap"> + {% for collaborator in wishlist.collaborators %} + <span class="badge bg-info me-1 mb-1">{{ collaborator.username }}</span> + {% endfor %} + </div> + </div> + + {% endif %} + + {# Zone contenant la date et les boutons #} + <div class="mt-auto py-3"> + <p class="card-text"> + <strong>Date limite:</strong> + <span class="{% if wishlist.isExpired() %}text-danger{% endif %}"> + {{ wishlist.deadline ? wishlist.deadline|date('d/m/Y') : 'Non définie' }} + </span> + </p> + </div> + <!-- Wishlist Actions --> + <div class="btn-group d-flex justify-content-between"> + <a href="{{ path('app_wishlist_show', {'id': wishlist.id}) }}" class="btn btn-primary flex-fill me-1 text-center"> + <i class="fas fa-eye"></i> + </a> + + {% if not wishlist.isExpired() %} + <a href="{{ path('app_wishlists_edit', {'id': wishlist.id}) }}" class="btn btn-warning flex-fill me-1 text-center"> + <i class="fas fa-pencil-alt"></i> + </a> + {% else %} + <button class="btn btn-warning flex-fill me-1 text-center opacity-50" disabled> + <i class="fas fa-pencil-alt"></i> + </button> + {% endif %} + + <form method="post" action="{{ path('app_wishlist_delete_wishlist', {'id': wishlist.id}) }}" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cette liste?');" style="display: inline;" class="flex-fill me-1"> + <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ wishlist.id) }}"> + <button type="submit" class="btn btn-supprimer w-100 text-center"> + <i class="fas fa-trash"></i> + </button> + </form> + + {% if not wishlist.isExpired() %} + <button class="btn btn-creer flex-fill text-center share-wishlist-btn" data-id="{{ wishlist.id }}"> + <i class="fas fa-share-alt"></i> + </button> + {% else %} + <button class="btn btn-creer flex-fill text-center opacity-50" disabled> + <i class="fas fa-share-alt"></i> + </button> + {% endif %} + </div> + </div> + </div> + </div> + {% endfor %} + </div> + {% endif %} + + {# Section pour les listes partagées avec moi #} + <h4>Listes partagées avec moi</h4> + {% if wishlistsIamCollaborator is empty %} + <div class="alert alert-info">Aucune liste de souhaits partagée avec vous.</div> + {% else %} + <div class="row"> + {% for wishlist in wishlistsIamCollaborator %} + <div class="col-md-4 mb-4"> + <div class="card h-100"> + <div class="card-body d-flex flex-column"> + <!-- Titre et date limite --> + <div class="d-flex justify-content-between align-items-center mb-2"> + <h5 class="card-title mb-0">{{ wishlist.name }}</h5> + <p class="card-text mb-0"> + Date limite: {{ wishlist.deadline ? wishlist.deadline|date('d/m/Y') : 'Non définie' }} + </p> + </div> + <div class="mb-3"> + <p class="mb-1"><strong>Partagée par:</strong> {{ wishlist.owner.username }}</p> + </div> + <!-- Actions --> + <div class="btn-group d-flex justify-content-between"> + <a href="{{ path('app_wishlist_show', {'id': wishlist.id}) }}" class="btn btn-primary flex-fill me-1 text-center"> + <i class="fas fa-eye"></i> + </a> + <a href="{{ path('app_wishlists_edit', {'id': wishlist.id}) }}" class="btn btn-warning flex-fill me-1 text-center"> + <i class="fas fa-pencil-alt"></i> + </a> + </div> + </div> + </div> + </div> + {% endfor %} + </div> + {% endif %} + + {# Section pour afficher Mes invitations #} + <h4>Mes invitations</h4> + {% if invitations is empty %} + <div class="alert alert-info">Vous n'avez pas d'invitations en attente.</div> + {% else %} + <div class="row"> + {% for invitation in invitations %} + <div class="col-md-4 mb-4"> + <div class="card h-100"> + <div class="card-body d-flex flex-column"> + <h5 class="card-title">{{ invitation.wishlist.name }}</h5> + <p class="card-text"> + <strong>Expéditeur :</strong> {{ invitation.sender.username }} + </p> + <p class="card-text"> + <strong>Statut :</strong> + {% if invitation.accepted is same as(null) %} + En attente + {% elseif invitation.accepted == true %} + Acceptée + {% elseif invitation.accepted == false %} + Refusée + {% endif %} + </p> + <a href="{{ path('invitation_show', {'id': invitation.id}) }}" class="btn btn-primary mb-2">Voir l'invitation</a> + {% if invitation.accepted is same as(null) %} + <div class="action-buttons" style="display: flex; justify-content: center; gap: 10px; margin-top: 10px;"> + <form action="{{ path('invitation_accept', {'id': invitation.id}) }}" method="post" style="margin: 0;"> + <input type="hidden" name="_token" value="{{ csrf_token('accept' ~ invitation.id) }}"> + <button type="submit" class="btn btn-success">Accepter l'invitation</button> + </form> + <form action="{{ path('invitation_reject', {'id': invitation.id}) }}" method="post" style="margin: 0;"> + <input type="hidden" name="_token" value="{{ csrf_token('reject' ~ invitation.id) }}"> + <button type="submit" class="btn btn-danger">Refuser l'invitation</button> + </form> + </div> + {% endif %} + + </div> + </div> + </div> + {% endfor %} + </div> + {% endif %} + + </div> + + {# Popups de partage pour les wishlists #} + {% for wishlist in wishlists %} + <div id="share-popup-{{ wishlist.id }}" class="share-popup hidden"> + <div class="share-popup-content"> + <h2>Share Your Wishlist</h2> + <p>Use the link below to share your wishlist with friends and family.</p> + <input type="text" id="share-link-{{ wishlist.id }}" value="{{ url('app_wishlist_public', {'token': wishlist.publicToken}) }}" readonly class="form-control mb-2"> + <button class="btn btn-info copy-link-btn mb-2" data-id="{{ wishlist.id }}">Copy Link</button> + <button class="btn btn-secondary close-popup-btn mb-3" data-id="{{ wishlist.id }}">Close</button> + {# <div class="add-friends-container"> + <h3>Share with Users</h3> + <div class="input-with-button d-flex mb-2"> + <input + type="text" + id="friend-username-{{ wishlist.id }}" + placeholder="Search username" + class="friend-input form-control me-2" + autocomplete="off" + > + <button + type="button" + class="btn btn-primary add-friend-btn" + data-id="{{ wishlist.id }}" + > + <i class="fas fa-plus"></i> + </button> + </div> #} + <!-- Suggestions d'utilisateurs --> + <div id="user-suggestions-{{ wishlist.id }}" class="user-suggestions mt-2"></div> + </div> + </div> + </div> + {% endfor %} +</div> +{% endblock %} + +{% block javascripts %} +{{ parent() }} +<script> + document.addEventListener('DOMContentLoaded', function () { + const csrfTokenElement = document.querySelector('meta[name="csrf-token"]'); + const csrfToken = csrfTokenElement ? csrfTokenElement.getAttribute('content') : null; + + // Gestion des boutons de partage + const shareButtons = document.querySelectorAll('.share-wishlist-btn'); + shareButtons.forEach(button => { + button.addEventListener('click', function (event) { + event.stopPropagation(); + const popups = document.querySelectorAll('.share-popup'); + popups.forEach(popup => { + popup.classList.add('hidden'); + }); + const wishlistId = this.getAttribute('data-id'); + const popup = document.getElementById('share-popup-' + wishlistId); + popup.classList.remove('hidden'); + }); + }); + + // Boutons de fermeture des popups + const closeButtons = document.querySelectorAll('.close-popup-btn'); + closeButtons.forEach(button => { + button.addEventListener('click', function () { + const wishlistId = this.getAttribute('data-id'); + const popup = document.getElementById('share-popup-' + wishlistId); + popup.classList.add('hidden'); + }); + }); + + // Recherche d'utilisateurs pour le partage + const friendInputs = document.querySelectorAll('.friend-input'); + friendInputs.forEach(input => { + let searchTimeout; + input.addEventListener('input', function () { + const searchValue = this.value.trim(); + const wishlistId = this.id.replace('friend-username-', ''); + const suggestionsContainer = document.getElementById(`user-suggestions-${wishlistId}`); + clearTimeout(searchTimeout); + if (searchValue.length < 2) { + suggestionsContainer.innerHTML = ''; + return; + } + searchTimeout = setTimeout(() => { + fetch(`/api/users/search?query=${encodeURIComponent(searchValue)}`) + .then(response => response.json()) + .then(users => { + suggestionsContainer.innerHTML = ''; + if (users.length === 0) { + suggestionsContainer.innerHTML = '<p class="text-muted">Aucun utilisateur trouvé</p>'; + return; + } + const suggestionsList = document.createElement('div'); + suggestionsList.className = 'list-group'; + users.forEach(user => { + const suggestionItem = document.createElement('a'); + suggestionItem.href = '#'; + suggestionItem.className = 'list-group-item list-group-item-action'; + suggestionItem.innerHTML = ` + <div class="d-flex w-100 justify-content-between"> + <h5 class="mb-1">${user.username}</h5> + <small>${user.email}</small> + </div> + `; + suggestionItem.addEventListener('click', function (e) { + e.preventDefault(); + input.value = user.username; + suggestionsContainer.innerHTML = ''; + }); + suggestionsList.appendChild(suggestionItem); + }); + suggestionsContainer.appendChild(suggestionsList); + }) + .catch(error => { + console.error('Erreur lors de la recherche d\'utilisateurs:', error); + suggestionsContainer.innerHTML = '<p class="text-danger">Erreur de recherche</p>'; + }); + }, 300); + }); + }); + + // Bouton d'ajout d'ami + const addFriendButtons = document.querySelectorAll('.add-friend-btn'); + addFriendButtons.forEach(button => { + button.addEventListener('click', function (event) { + event.preventDefault(); + const wishlistId = this.getAttribute('data-id'); + const inputField = document.getElementById(`friend-username-${wishlistId}`); + const username = inputField.value.trim(); + if (!username) { + alert('Veuillez entrer un nom d\'utilisateur.'); + return; + } + fetch('/api/wishlist/add-collaborator', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + wishlistId: wishlistId, + username: username + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert(`Collaborateur ajouté : ${data.username}`); + inputField.value = ''; + } else { + alert(data.message || 'Erreur lors de l\'ajout du collaborateur.'); + } + }) + .catch(error => { + console.error('Erreur lors de la requête :', error); + alert('Une erreur est survenue.'); + }); + }); + }); + + // Bouton de copie du lien + const copyButtons = document.querySelectorAll('.copy-link-btn'); + copyButtons.forEach(button => { + button.addEventListener('click', function() { + const wishlistId = this.getAttribute('data-id'); + const linkInput = document.getElementById(`share-link-${wishlistId}`); + linkInput.select(); + document.execCommand('copy'); + this.textContent = 'Copié !'; + setTimeout(() => { + this.textContent = 'Copy Link'; + }, 2000); + }); + }); + + // Fermeture des popups en cliquant à l'extérieur + document.addEventListener('click', function (event) { + const openPopups = document.querySelectorAll('.share-popup:not(.hidden)'); + openPopups.forEach(popup => { + if (!popup.contains(event.target) && + !event.target.closest('.share-wishlist-btn')) { + popup.classList.add('hidden'); + } + }); + }); + }); +</script> +{% endblock %} \ No newline at end of file diff --git a/Server/templates/wishlists/show.html.twig b/Server/templates/wishlists/show.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..4aee07827894917668fbebff02898bd396c5c27a --- /dev/null +++ b/Server/templates/wishlists/show.html.twig @@ -0,0 +1,127 @@ + +{# + + @authors + - KELA Wydie (Binôme 14) + - LALE Ny (Binôme 14) +#} +{% extends 'base.html.twig' %} + +{% block title %} + {{ wishlist.name }} - Ma liste de souhaits +{% endblock %} + +{% block body %} +<div class="container my-4"> + <div class="row mb-4"> + <div class="col-md-8"> + <h1>{{ wishlist.name }}</h1> + {% if wishlist.description %} + <p class="lead">{{ wishlist.description }}</p> + {% endif %} + </div> + <div class="col-md-4 text-end"> + <div class="btn-group"> + <a href="{{ path('app_wishlists_index') }}" class="btn btn-outline-secondary"> + <i class="fas fa-arrow-left"></i> Retour aux listes + </a> + {% if wishlist.owner == app.user %} + <a href="{{ path('app_wishlist_edit', {'id': wishlist.id}) }}" class="btn btn-primary"> + <i class="fas fa-edit"></i> Modifier + </a> + <a href="{{ path('app_wishlist_get_url', {'id': wishlist.id}) }}" class="btn btn-info"> + <i class="fas fa-share-alt"></i> Partager + </a> + {% endif %} + </div> + </div> + </div> + + <div class="card shadow-sm mb-4"> + <div class="card-header d-flex justify-content-between align-items-center"> + <h2 class="h5 mb-0">Articles ({{ items|length }})</h2> + <a href="{{ path('app_wishlist_insert_item', {'id': wishlist.id}) }}" class="btn btn-sm btn-success"> + <i class="fas fa-plus"></i> Ajouter un article + </a> + </div> + + {% if items|length > 0 %} + <div class="table-responsive"> + <table class="table table-hover mb-0"> + <thead class="table-light"> + <tr> + <th>Nom</th> + <th>Prix</th> + <th>Priorité</th> + <th>Lien</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + {% for item in items %} + <tr> + <td> + {% if item.image %} + <img src="{{ item.image }}" alt="{{ item.name }}" class="img-thumbnail me-2" style="max-width: 50px;"> + {% endif %} + {{ item.name }} + </td> + <td>{% if item.price %}{{ item.price|number_format(2, ',', ' ') }} €{% else %}Prix non spécifié{% endif %}</td> + <td> + {% if item.priority == 'high' %} + <span class="badge bg-danger">Haute</span> + {% elseif item.priority == 'medium' %} + <span class="badge bg-warning">Moyenne</span> + {% else %} + <span class="badge bg-info">Basse</span> + {% endif %} + </td> + <td> + {% if item.url %} + <a href="{{ path('app_wishlist_goto_official_website', {'id': wishlist.id, 'itemId': item.id}) }}" + target="_blank" class="btn btn-sm btn-outline-primary"> + <i class="fas fa-external-link-alt"></i> Voir + </a> + {% else %} + <span class="text-muted">Aucun lien</span> + {% endif %} + </td> + <td> + <div class="btn-group btn-group-sm"> + <a href="{{ path('app_wishlist_modify_item', {'id': wishlist.id, 'itemId': item.id}) }}" + class="btn btn-outline-secondary"> + <i class="fas fa-edit"></i> + </a> + <a href="{{ path('app_wishlist_delete_item', {'id': wishlist.id, 'itemId': item.id}) }}" + class="btn btn-outline-danger" + onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet article ?')"> + <i class="fas fa-trash"></i> + </a> + </div> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% else %} + <div class="card-body text-center py-5"> + <p class="text-muted mb-3">Cette liste ne contient aucun article pour le moment.</p> + <a href="{{ path('app_wishlist_insert_item', {'id': wishlist.id}) }}" class="btn btn-primary"> + <i class="fas fa-plus"></i> Ajouter mon premier article + </a> + </div> + {% endif %} + </div> + + {% if wishlist.owner == app.user %} + <div class="d-flex justify-content-end mt-4"> + <a href="{{ path('app_wishlist_delete_wishlist', {'id': wishlist.id}) }}" + class="btn btn-danger" + onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette liste de souhaits et tous ses articles ?')"> + <i class="fas fa-trash-alt"></i> Supprimer cette liste + </a> + </div> + {% endif %} +</div> +{% endblock %} diff --git a/Server/test.php b/Server/test.php new file mode 100644 index 0000000000000000000000000000000000000000..3ede80bae44bcd5485d09056f449a46bdec1f5c6 --- /dev/null +++ b/Server/test.php @@ -0,0 +1,10 @@ +echo '<?php +try { + $pdo = new PDO("sqlite::memory:"); + echo "SQLite connection successful!\n"; + + $drivers = PDO::getAvailableDrivers(); + echo "Available PDO drivers: " . implode(", ", $drivers) . "\n"; +} catch (PDOException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} \ No newline at end of file