diff --git a/.gitignore b/.gitignore index 80c163f..c166fee 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,8 @@ ###< phpunit/phpunit ### .DS_Store + +###> symfony/asset-mapper ### +/public/assets/ +/assets/vendor/ +###< symfony/asset-mapper ### diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 0000000..9e6e07c --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,156 @@ +# Development Guidelines + +This document provides guidelines and instructions for developing and maintaining the Futtern project. + +## Build/Configuration Instructions + +### Environment Setup + +1. **PHP Requirements**: The project requires PHP 8.4 or higher with the following extensions: + - ctype + - iconv + +2. **Composer**: Install dependencies using Composer: + ```bash + composer install + ``` + +3. **Environment Configuration**: Copy `.env` to `.env.local` and adjust the settings as needed for your local environment. + +### Development Server + +Start the Symfony development server: +```bash +symfony server:start +``` + +### Building Assets + +The project uses Tailwind CSS via the symfonycasts/tailwind-bundle: +```bash +# Install importmap assets +symfony console importmap:install +``` + +## Deployment + +The project includes several deployment scripts: + +1. **Prepare for Deployment**: + ```bash + ./deploy/prepare-deploy.sh + ``` + This script creates a clean copy of the application with only production dependencies. + +2. **Local Deployment**: + ```bash + ./deploy/local-deploy.sh + ``` + This script deploys the application to a remote server, backing up the database and restarting the service. + +3. **Update After Deployment**: + ```bash + ./deploy/update.sh + ``` + This script is run on the remote server to clear cache, warm up cache, and run database migrations. + +## Testing Information + +### Testing Framework + +The project uses Pest PHP, a testing framework built on top of PHPUnit, for testing. Tests are organized into: +- **Feature Tests**: For testing controllers and API endpoints +- **Unit Tests**: For testing individual components + +### Running Tests + +Run all tests: +```bash +composer test +# or +./vendor/bin/pest +``` + +Run specific tests: +```bash +./vendor/bin/pest tests/Unit/ExampleTest.php +``` + +Run tests in parallel: +```bash +./vendor/bin/pest --parallel +``` + +### Creating Tests + +1. **Unit Tests**: Create files in the `tests/Unit` directory. +2. **Feature Tests**: + - Controller tests: Create files in `tests/Feature/Controller` + - API tests: Create files in `tests/Feature/Api` + +### Example Test + +Here's a simple example of a Pest PHP test: + +```php +toBeTrue(); + expect(1 + 1)->toBe(2); + expect('hello world')->toContain('world'); +}); + +test('array operations', function (): void { + $array = [1, 2, 3]; + + expect($array)->toHaveCount(3); + expect($array)->toContain(2); + expect($array[0])->toBe(1); +}); +``` + +### Test Base Classes + +- `DbWebTest`: Base class for controller tests +- `DbApiTestCase`: Base class for API tests + +## Code Quality and Style + +### Code Style + +The project uses Easy Coding Standard (ECS) for code style checking: +```bash +composer lint +# or +./vendor/bin/ecs --fix +``` + +The code style is defined in `ecs.php` and includes: +- Custom rules from `Lubiana\CodeQuality\LubiSetList::ECS` +- All classes should be declared as final + +### Code Refactoring + +The project uses Rector for automated code refactoring: +```bash +./vendor/bin/rector +``` + +The refactoring rules are defined in `rector.php` and include: +- Custom rules from `Lubiana\CodeQuality\LubiSetList::RECTOR` +- StaticClosureRector is skipped in the tests directory + +### Development Workflow + +1. Make changes to the code +2. Run tests to ensure functionality: `composer test` +3. Fix code style issues: `composer lint` +4. Commit and push changes +5. Deploy using the deployment scripts + +## Debugging + +- Use the Symfony Web Profiler in development mode +- Check logs in `var/log/` +- For API debugging, use the API Platform documentation at `/api` \ No newline at end of file diff --git a/.symfony.local.yaml b/.symfony.local.yaml new file mode 100644 index 0000000..1198458 --- /dev/null +++ b/.symfony.local.yaml @@ -0,0 +1,3 @@ +workers: + tailwind: + cmd: ['symfony', 'console', 'tailwind:build', '--watch'] \ No newline at end of file diff --git a/assets/app.js b/assets/app.js new file mode 100644 index 0000000..05ff425 --- /dev/null +++ b/assets/app.js @@ -0,0 +1,8 @@ +/* + * Welcome to your app's main JavaScript file! + * + * This file will be included onto the page via the importmap() Twig function, + * which should already be in your base.html.twig. + */ +import './styles/app.css'; + diff --git a/assets/styles/app.css b/assets/styles/app.css new file mode 100644 index 0000000..f52a8ac --- /dev/null +++ b/assets/styles/app.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + background-color: skyblue; +} diff --git a/composer.json b/composer.json index 2360741..a69fb84 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,8 @@ "symfony/twig-bundle": "7.1.*", "symfony/uid": "7.1.*", "symfony/validator": "7.1.*", - "symfony/yaml": "7.1.*" + "symfony/yaml": "7.1.*", + "symfonycasts/tailwind-bundle": "^0.10.0" }, "require-dev": { "doctrine/doctrine-fixtures-bundle": "^4.0", @@ -85,7 +86,8 @@ "scripts": { "auto-scripts": { "cache:clear": "symfony-cmd", - "assets:install %PUBLIC_DIR%": "symfony-cmd" + "assets:install %PUBLIC_DIR%": "symfony-cmd", + "importmap:install": "symfony-cmd" }, "post-install-cmd": [ "@auto-scripts" diff --git a/composer.lock b/composer.lock index 472b98c..e06da3f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d7745d305c728e9812365f75052d1031", + "content-hash": "1c86a4bdee669d314840de214b23cf19", "packages": [ { "name": "api-platform/doctrine-common", @@ -1112,6 +1112,87 @@ }, "time": "2025-04-11T09:32:56+00:00" }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, { "name": "doctrine/collections", "version": "2.3.0", @@ -2983,6 +3064,85 @@ ], "time": "2024-10-25T15:15:23+00:00" }, + { + "name": "symfony/asset-mapper", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset-mapper.git", + "reference": "6428e4b6d8cff9c5fe6f40ddbee4c9f6bfdaa0b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset-mapper/zipball/6428e4b6d8cff9c5fe6f40ddbee4c9f6bfdaa0b8", + "reference": "6428e4b6d8cff9c5fe6f40ddbee4c9f6bfdaa0b8", + "shasum": "" + }, + "require": { + "composer/semver": "^3.0", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^7.1", + "symfony/http-client": "^6.4|^7.0" + }, + "conflict": { + "symfony/framework-bundle": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/event-dispatcher-contracts": "^3.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/web-link": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\AssetMapper\\": "" + }, + "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 directories of assets & makes them available in a public directory with versioned filenames.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset-mapper/tree/v7.2.5" + }, + "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-26T11:29:07+00:00" + }, { "name": "symfony/cache", "version": "v7.2.5", @@ -4467,6 +4627,179 @@ ], "time": "2025-01-29T07:13:42+00:00" }, + { + "name": "symfony/http-client", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "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 powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/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/http-client-contracts", + "version": "v3.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "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\\HttpClient\\": "" + }, + "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 HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.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-07T08:49:48+00:00" + }, { "name": "symfony/http-foundation", "version": "v7.2.5", @@ -5276,6 +5609,67 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/process", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", + "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.5" + }, + "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-13T12:21:46+00:00" + }, { "name": "symfony/property-access", "version": "v7.2.3", @@ -7107,6 +7501,62 @@ ], "time": "2025-01-07T12:50:05+00:00" }, + { + "name": "symfonycasts/tailwind-bundle", + "version": "v0.10.0", + "source": { + "type": "git", + "url": "https://github.com/SymfonyCasts/tailwind-bundle.git", + "reference": "380502c39bf403f772d050ff9904932040de8cf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SymfonyCasts/tailwind-bundle/zipball/380502c39bf403f772d050ff9904932040de8cf1", + "reference": "380502c39bf403f772d050ff9904932040de8cf1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/asset-mapper": "^6.3|^7.0", + "symfony/cache": "^6.3|^7.0", + "symfony/console": "^5.4|^6.3|^7.0", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/http-client": "^5.4|^6.3|^7.0", + "symfony/process": "^5.4|^6.3|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6", + "symfony/filesystem": "^6.3|^7.0", + "symfony/framework-bundle": "^6.3|^7.0", + "symfony/phpunit-bridge": "^6.3.9|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfonycasts\\TailwindBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ryan Weaver", + "homepage": "https://symfonycasts.com" + } + ], + "description": "Delightful Tailwind Support for Symfony + AssetMapper", + "keywords": [ + "asset-mapper", + "tailwind" + ], + "support": { + "issues": "https://github.com/SymfonyCasts/tailwind-bundle/issues", + "source": "https://github.com/SymfonyCasts/tailwind-bundle/tree/v0.10.0" + }, + "time": "2025-04-09T15:18:46+00:00" + }, { "name": "twig/twig", "version": "v3.20.0", @@ -10627,179 +11077,6 @@ ], "time": "2025-02-17T15:53:07+00:00" }, - { - "name": "symfony/http-client", - "version": "v7.2.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "~3.4.4|^3.5.2", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "amphp/amp": "<2.5", - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.4" - }, - "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "3.0" - }, - "require-dev": { - "amphp/http-client": "^4.2.1|^5.0", - "amphp/http-tunnel": "^1.0|^2.0", - "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4|^2.0", - "nyholm/psr7": "^1.0", - "php-http/httplug": "^1.0|^2.0", - "psr/http-client": "^1.0", - "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpClient\\": "" - }, - "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 powerful methods to fetch HTTP resources synchronously or asynchronously", - "homepage": "https://symfony.com", - "keywords": [ - "http" - ], - "support": { - "source": "https://github.com/symfony/http-client/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/http-client-contracts", - "version": "v3.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", - "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\\HttpClient\\": "" - }, - "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 HTTP clients", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.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-07T08:49:48+00:00" - }, { "name": "symfony/maker-bundle", "version": "v1.62.1", @@ -10892,67 +11169,6 @@ ], "time": "2025-01-15T00:21:40+00:00" }, - { - "name": "symfony/process", - "version": "v7.2.5", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", - "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", - "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.5" - }, - "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-13T12:21:46+00:00" - }, { "name": "symfony/web-profiler-bundle", "version": "v7.2.4", diff --git a/config/bundles.php b/config/bundles.php index 1aed8f6..6a662f2 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -11,6 +11,7 @@ use Symfony\Bundle\MakerBundle\MakerBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; +use Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle; return [ FrameworkBundle::class => [ @@ -49,4 +50,7 @@ return [ 'dev' => true, 'test' => true, ], + SymfonycastsTailwindBundle::class => [ + 'all' => true, + ], ]; diff --git a/config/packages/asset_mapper.php b/config/packages/asset_mapper.php new file mode 100644 index 0000000..3a3cc5f --- /dev/null +++ b/config/packages/asset_mapper.php @@ -0,0 +1,21 @@ +extension('framework', [ + 'asset_mapper' => [ + 'paths' => [ + 'assets/', + ], + 'missing_import_mode' => 'strict', + ], + ]); + if ($containerConfigurator->env() === 'prod') { + $containerConfigurator->extension('framework', [ + 'asset_mapper' => [ + 'missing_import_mode' => 'warn', + ], + ]); + } +}; diff --git a/config/packages/symfonycasts_tailwind.php b/config/packages/symfonycasts_tailwind.php new file mode 100644 index 0000000..6c1a097 --- /dev/null +++ b/config/packages/symfonycasts_tailwind.php @@ -0,0 +1,9 @@ +extension('symfonycasts_tailwind', [ + 'binary_version' => 'v3.4.17', + ]); +}; diff --git a/importmap.php b/importmap.php new file mode 100644 index 0000000..c5b734e --- /dev/null +++ b/importmap.php @@ -0,0 +1,19 @@ + [ + 'path' => './assets/app.js', + 'entrypoint' => true, + ], +]; diff --git a/symfony.lock b/symfony.lock index 17beb57..63de157 100644 --- a/symfony.lock +++ b/symfony.lock @@ -99,6 +99,21 @@ "ref": "1019e5c08d4821cb9b77f4891f8e9c31ff20ac6f" } }, + "symfony/asset-mapper": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a" + }, + "files": [ + "assets/app.js", + "assets/styles/app.css", + "config/packages/asset_mapper.yaml", + "importmap.php" + ] + }, "symfony/console": { "version": "7.1", "recipe": { @@ -223,5 +238,17 @@ "config/packages/web_profiler.yaml", "config/routes/web_profiler.yaml" ] + }, + "symfonycasts/tailwind-bundle": { + "version": "0.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.8", + "ref": "4ea7c9488fdce8943520daf3fdc31e93e5b59c64" + }, + "files": [ + "config/packages/symfonycasts_tailwind.yaml" + ] } } diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..531385f --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./assets/**/*.js", + "./templates/**/*.html.twig", + ], + darkMode: 'media', // Use 'media' to respect the user's system preference + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/templates/_form.html.twig b/templates/_form.html.twig index bf20b98..6fe4982 100644 --- a/templates/_form.html.twig +++ b/templates/_form.html.twig @@ -1,4 +1,24 @@ -{{ form_start(form) }} - {{ form_widget(form) }} - +{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }} + {% for field in form %} + {% if field.vars.name != '_token' %} +
+ {{ form_label(field, null, {'label_attr': {'class': 'block text-sm font-medium leading-6 text-gray-900 dark:text-gray-100'}}) }} +
+ {{ form_widget(field, {'attr': {'class': 'block w-full rounded-md border-0 py-1.5 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:ring-2 focus:ring-inset focus:ring-indigo-600 dark:focus:ring-indigo-500 sm:text-sm sm:leading-6'}}) }} +
+ {% if field.vars.errors|length > 0 %} +
+ {{ form_errors(field) }} +
+ {% endif %} + {% if field.vars.help is defined and field.vars.help %} +

{{ field.vars.help }}

+ {% endif %} +
+ {% endif %} + {% endfor %} +
+ Cancel + +
{{ form_end(form) }} diff --git a/templates/base.html.twig b/templates/base.html.twig index 1036cc3..a594641 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -1,46 +1,93 @@ - + + - {% block title %}Welcome!{% endblock %} - - {% set currentDate = "now"|date("d") %} - {% if currentDate % 4 == 0 %} - - {% elseif currentDate % 4 == 1 %} - - {% elseif currentDate % 4 == 2 %} - - {% else %} - - {% endif %} - + {% block title %}Welcome!{% endblock %} - Futtern + + {% block stylesheets %} + + {% endblock %} + {% block javascripts %} + {{ importmap() }} + {% endblock %} - -
-

Hello {{ app.request.cookies.get('username', 'nobody') }} - change name

-
-
- {% block body %}{% endblock %} -
+ +
+
+

{% block header %}{% endblock %}

+
+
+ +
+
+
+ {% block body %}{% endblock %} +
+
+
+ - diff --git a/templates/food_order/index.html.twig b/templates/food_order/index.html.twig index d974f87..93613a5 100644 --- a/templates/food_order/index.html.twig +++ b/templates/food_order/index.html.twig @@ -1,45 +1,69 @@ {% extends 'base.html.twig' %} -{% block title %}FoodOrder index{% endblock %} +{% block title %}Food Orders{% endblock %} + +{% block header %}Food Orders{% endblock %} {% block body %} -

FoodOrder index

-
- + hx-target="#new-order-form" + class="block rounded-md bg-indigo-600 dark:bg-indigo-700 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500" + >Create new order +
-
- - - - - - - - - - - - {% for food_order in food_orders %} - {{ include('food_order/table_row.html.twig') }} - {% endfor %} - {% if food_orders|length < 10 %} - - - + +
+ +
+
+
+
+
CreatedByVendorCreatedAtClosedAtactions
- check the archive - for older orders -
+ + + + + + + + + + + {% for food_order in food_orders %} + {{ include('food_order/table_row.html.twig') }} + {% endfor %} + {% if food_orders|length < 10 %} + + + + {% endif %} + +
Created ByVendorCreated AtClosed At + Actions +
+ Check the archive + for older orders +
+ + + + + +
+ {% if prev_page > 0 %} + Previous Page {% endif %} - - - {% if prev_page > 0 %} - previous page | - {% endif %} - {% if next_page > current_page %} - next page - {% endif %} + {% if next_page > current_page %} + Next Page + {% endif %} +
{% endblock %} diff --git a/templates/food_order/new.html.twig b/templates/food_order/new.html.twig index 72ed804..3447325 100644 --- a/templates/food_order/new.html.twig +++ b/templates/food_order/new.html.twig @@ -1,2 +1,11 @@ -{{ include('_form.html.twig') }} - +
+
+

Create New Order

+
+

Fill out the form below to create a new food order.

+
+
+ {{ include('_form.html.twig') }} +
+
+
diff --git a/templates/food_order/show.html.twig b/templates/food_order/show.html.twig index a7ace15..9ab1ef2 100644 --- a/templates/food_order/show.html.twig +++ b/templates/food_order/show.html.twig @@ -1,71 +1,101 @@ {% extends 'base.html.twig' %} -{% block title %}FoodOrder{% endblock %} +{% block title %}Order Details{% endblock %} + +{% block header %}Order Details{% endblock %} {% block body %} -

FoodOrder

+
+
+

Order Information

+

Details about the food order from {{ food_order.foodVendor.name }}.

+
+
+
+
+
Vendor
+
{{ food_order.foodVendor.name }}
+
+
+
Vendor Phone
+
{{ food_order.foodVendor.phone }}
+
+
+
Created By
+
{{ food_order.createdBy }}
+
+
+
Created At
+
{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}
+
+
+
Closed At
+
{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : 'Not closed yet' }}
+
+
+
+
- - - - - - - - - - - - - - - - - - - - - - - -
Vendor{{ food_order.foodVendor.name }}
Vendorphone{{ food_order.foodVendor.phone }}
Created By{{ food_order.createdBy }}
CreatedAt{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}
ClosedAt{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}
- back to list - {% if(food_order.isClosed) %} - reopen - {% else %} - close - {% endif %} - -

Items

- - - - - - - - - - - - {% for item in food_order.orderItemsSortedByName %} - - - - - - - - {% endfor %} - -
Indexusernamenameextrasactions
{{ loop.index }}{{ item.createdBy }}{{ item.name }}{{ item.extras }} - {% if(food_order.isClosed) %} - {% else %} - edit - copy - remove - {% endif %} -
- New Item +
+ + Back to list + + {% if(food_order.isClosed) %} + + Reopen Order + + {% else %} + + Close Order + + {% endif %} +
+
+
+
+

Order Items

+

Items included in this order.

+
+
+ + Add New Item + +
+
+
+ + + + + + + + + + + + {% for item in food_order.orderItemsSortedByName %} + + + + + + + + {% endfor %} + +
#UsernameItem NameExtras + Actions +
{{ loop.index }}{{ item.createdBy }}{{ item.name }}{{ item.extras }} + {% if(food_order.isClosed) %} + Order closed + {% else %} + Edit + Copy + Remove + {% endif %} +
+
+
{% endblock %} diff --git a/templates/food_order/table_row.html.twig b/templates/food_order/table_row.html.twig index 580d5fa..a3d9817 100644 --- a/templates/food_order/table_row.html.twig +++ b/templates/food_order/table_row.html.twig @@ -1,9 +1,9 @@ - - {{ food_order.createdBy }} - {{ food_order.foodVendor.name }} - {{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }} - {{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }} - - show + + {{ food_order.createdBy }} + {{ food_order.foodVendor.name }} + {{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }} + {{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }} + + View, order from {{ food_order.createdBy }} - \ No newline at end of file + diff --git a/templates/food_vendor/_form.html.twig b/templates/food_vendor/_form.html.twig index bf20b98..ed94f7a 100644 --- a/templates/food_vendor/_form.html.twig +++ b/templates/food_vendor/_form.html.twig @@ -1,4 +1,24 @@ -{{ form_start(form) }} - {{ form_widget(form) }} - +{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }} + {% for field in form %} + {% if field.vars.name != '_token' %} +
+ {{ form_label(field, null, {'label_attr': {'class': 'block text-sm font-medium leading-6 text-gray-900'}}) }} +
+ {{ form_widget(field, {'attr': {'class': 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6'}}) }} +
+ {% if field.vars.errors|length > 0 %} +
+ {{ form_errors(field) }} +
+ {% endif %} + {% if field.vars.help is defined and field.vars.help %} +

{{ field.vars.help }}

+ {% endif %} +
+ {% endif %} + {% endfor %} +
+ Cancel + +
{{ form_end(form) }} diff --git a/templates/food_vendor/index.html.twig b/templates/food_vendor/index.html.twig index 7754082..53f50c1 100644 --- a/templates/food_vendor/index.html.twig +++ b/templates/food_vendor/index.html.twig @@ -1,33 +1,54 @@ {% extends 'base.html.twig' %} -{% block title %}FoodVendor index{% endblock %} +{% block title %}Food Vendors{% endblock %} + +{% block header %}Food Vendors{% endblock %} {% block body %} -

FoodVendor index

+
+
+

+ Manage your food vendors and create new ones. +

+
+
+ + Create new vendor + +
+
- - - - - - - - - {% for food_vendor in food_vendors %} - - - - - {% else %} - - - - {% endfor %} - -
Nameactions
{{ food_vendor.name }} - show - edit -
no records found
- - Create new +
+
+
+
+ + + + + + + + + {% for food_vendor in food_vendors %} + + + + + {% else %} + + + + {% endfor %} + +
Name + Actions +
{{ food_vendor.name }} + View + Edit +
No vendors found
+
+
+
+
{% endblock %} diff --git a/templates/username.html.twig b/templates/username.html.twig index da56166..a7e85d3 100644 --- a/templates/username.html.twig +++ b/templates/username.html.twig @@ -2,8 +2,15 @@ {% block title %}Tell me your name{% endblock %} +{% block header %}Tell me your name{% endblock %} + {% block body %} -

Tell me your name

-

By submitting the form, you agree that your username will be stored as a cookie.

- {{ include('_form.html.twig') }} -{% endblock %} \ No newline at end of file +
+
+
+

By submitting the form, you agree that your username will be stored as a cookie.

+
+ {{ include('_form.html.twig') }} +
+
+{% endblock %} diff --git a/tests/Feature/Controller/FoodOrderControllerTest.php b/tests/Feature/Controller/FoodOrderControllerTest.php index 269b1bf..dd85add 100644 --- a/tests/Feature/Controller/FoodOrderControllerTest.php +++ b/tests/Feature/Controller/FoodOrderControllerTest.php @@ -44,7 +44,7 @@ describe(FoodOrderController::class, function (): void { $crawler = $this->client->request('GET', "{$this->path}list"); $this->assertResponseStatusCodeSame(200); - $this->assertPageTitleContains('FoodOrder index'); + $this->assertPageTitleContains('Food Orders'); $this->assertCount( 1, $crawler->filter('td') @@ -99,17 +99,29 @@ describe(FoodOrderController::class, function (): void { $crawler = $this->client->request('GET', "{$this->path}{$order->getId()}"); $this->assertResponseIsSuccessful(); - $tdContent = $crawler->filter( - 'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(3)' - )->text(); + + // Find all tables and get the last one (order items table) + $tables = $crawler->filter('table'); + $lastTable = $tables->eq($tables->count() - 1); + + // Get the item names from the table rows + $rows = $lastTable->filter('tbody tr'); + $tdContent = $rows->eq(0) + ->filter('td') + ->eq(2) + ->text(); $this->assertEquals('A', $tdContent); - $tdContent = $crawler->filter( - 'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(2) > td:nth-child(3)' - )->text(); + + $tdContent = $rows->eq(1) + ->filter('td') + ->eq(2) + ->text(); $this->assertEquals('B', $tdContent); - $tdContent = $crawler->filter( - 'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(3) > td:nth-child(3)' - )->text(); + + $tdContent = $rows->eq(2) + ->filter('td') + ->eq(2) + ->text(); $this->assertEquals('C', $tdContent); }); @@ -124,12 +136,12 @@ describe(FoodOrderController::class, function (): void { $this->manager->flush(); $crawler = $this->client->request('GET', "{$this->path}list"); $this->assertResponseStatusCodeSame(200); - $this->assertPageTitleContains('FoodOrder index'); + $this->assertPageTitleContains('Food Orders'); $this->assertElementContainsCount( $crawler, 'td', 1, - 'older orders' + 'for older orders' ); $this->assertElementContainsCount( $crawler, @@ -139,7 +151,7 @@ describe(FoodOrderController::class, function (): void { ); }); - test('paginatedFirstPage', function (int $page, int $prevPage, int $nextPage, int $items = 10): void { + test('paginatedFirstPage', function (int $page, int $prevPage, int $nextPage, int $items = 20): void { foreach (range(1, 35) as $i) { $order = new FoodOrder($this->generateOldUlid()); $order->setFoodVendor($this->vendor); @@ -150,7 +162,7 @@ describe(FoodOrderController::class, function (): void { $this->manager->flush(); $crawler = $this->client->request('GET', "{$this->path}list/archive/{$page}"); $this->assertResponseStatusCodeSame(200); - $this->assertPageTitleContains('FoodOrder index'); + $this->assertPageTitleContains('Food Orders'); $this->assertElementContainsCount( $crawler, 'td', @@ -160,14 +172,14 @@ describe(FoodOrderController::class, function (): void { if ($prevPage > 0) { $prevPage = $prevPage === 1 ? '' : "/{$prevPage}"; $node = $crawler->filter('a') - ->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'previous page') + ->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'Previous Page') ->first(); $target = $node->attr('href'); $this->assertTrue(str_ends_with((string) $target, $prevPage)); } if ($prevPage > 3) { $node = $crawler->filter('a') - ->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'next page') + ->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'Next Page') ->first(); $target = $node->attr('href'); $this->assertTrue(str_ends_with((string) $target, "/{$nextPage}")); @@ -178,7 +190,7 @@ describe(FoodOrderController::class, function (): void { [1, 0, 2], [2, 1, 3], [3, 2, 4], - [4, 3, 0, 5], + [4, 3, 0, 10], ] ); diff --git a/tests/Feature/Controller/FoodVendorControllerTest.php b/tests/Feature/Controller/FoodVendorControllerTest.php index d6d64cb..42e04a7 100644 --- a/tests/Feature/Controller/FoodVendorControllerTest.php +++ b/tests/Feature/Controller/FoodVendorControllerTest.php @@ -29,7 +29,7 @@ describe(FoodVendorController::class, function (): void { $this->client->request('GET', $this->path); $this->assertResponseStatusCodeSame(200); - $this->assertPageTitleContains('FoodVendor index'); + $this->assertPageTitleContains('Food Vendors'); }); test('new', function (): void { diff --git a/tests/Feature/Controller/MenuItemControllerTest.php b/tests/Feature/Controller/MenuItemControllerTest.php index fd63447..f9dcd8d 100644 --- a/tests/Feature/Controller/MenuItemControllerTest.php +++ b/tests/Feature/Controller/MenuItemControllerTest.php @@ -136,10 +136,9 @@ describe(MenuItemController::class, function (): void { $this->assertTrue($menuItem->isDeleted()); $crawler = $this->client->request('GET', '/order/item/new/' . $order->getId()); - $count = $crawler->filter('body > main:nth-child(2) > div:nth-child(5)') - ->children() + $count = $crawler->filter('form') ->count(); - $this->assertSame(2, $count); + $this->assertSame(1, $count); $this->assertResponseIsSuccessful(); diff --git a/tests/Feature/Controller/OrderItemControllerTest.php b/tests/Feature/Controller/OrderItemControllerTest.php index afbed56..4eb10f5 100644 --- a/tests/Feature/Controller/OrderItemControllerTest.php +++ b/tests/Feature/Controller/OrderItemControllerTest.php @@ -60,8 +60,7 @@ describe(OrderItemController::class, function (): void { sprintf('%snew/%s', $this->path, $this->order->getId()) ); - $children = $crawler->filter('body > main:nth-child(2) > div:nth-child(5)') - ->children(); + $children = $crawler->filter('form'); $this->assertCount(1, $children); diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php new file mode 100644 index 0000000..029be1b --- /dev/null +++ b/tests/Unit/ExampleTest.php @@ -0,0 +1,21 @@ +toBeTrue(); + expect(1 + 1) + ->toBe(2); + expect('hello world') + ->toContain('world'); +}); + +test('array operations', function (): void { + $array = [1, 2, 3]; + + expect($array) + ->toHaveCount(3); + expect($array) + ->toContain(2); + expect($array[0])->toBe(1); +});