add tailiwindi

This commit is contained in:
lubiana 2025-05-24 23:02:31 +02:00
parent 314063f15c
commit f38c05d97c
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
27 changed files with 1139 additions and 494 deletions

5
.gitignore vendored
View file

@ -17,3 +17,8 @@
###< phpunit/phpunit ###
.DS_Store
###> symfony/asset-mapper ###
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###

156
.junie/guidelines.md Normal file
View file

@ -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
<?php declare(strict_types=1);
test('example test', function (): void {
expect(true)->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`

3
.symfony.local.yaml Normal file
View file

@ -0,0 +1,3 @@
workers:
tailwind:
cmd: ['symfony', 'console', 'tailwind:build', '--watch']

9
assets/app.js Normal file
View file

@ -0,0 +1,9 @@
/*
* 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';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');

7
assets/styles/app.css Normal file
View file

@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background-color: skyblue;
}

View file

@ -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"

686
composer.lock generated
View file

@ -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",

View file

@ -1,52 +1,16 @@
<?php declare(strict_types=1);
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle;
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
use Liip\TestFixturesBundle\LiipTestFixturesBundle;
use Nelmio\CorsBundle\NelmioCorsBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\MakerBundle\MakerBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
<?php
return [
FrameworkBundle::class => [
'all' => true,
],
MakerBundle::class => [
'dev' => true,
],
DoctrineBundle::class => [
'all' => true,
],
DoctrineMigrationsBundle::class => [
'all' => true,
],
TwigBundle::class => [
'all' => true,
],
DoctrineFixturesBundle::class => [
'dev' => true,
'test' => true,
],
WebProfilerBundle::class => [
'dev' => true,
'test' => true,
],
SecurityBundle::class => [
'all' => true,
],
NelmioCorsBundle::class => [
'all' => true,
],
ApiPlatformBundle::class => [
'all' => true,
],
LiipTestFixturesBundle::class => [
'dev' => true,
'test' => true,
],
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
Liip\TestFixturesBundle\LiipTestFixturesBundle::class => ['dev' => true, 'test' => true],
Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle::class => ['all' => true],
];

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('framework', [
'asset_mapper' => [
'paths' => [
'assets/',
],
'missing_import_mode' => 'strict',
],
]);
if ($containerConfigurator->env() === 'prod') {
$containerConfigurator->extension('framework', [
'asset_mapper' => [
'missing_import_mode' => 'warn',
],
]);
}
};

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('symfonycasts_tailwind', [
'binary_version' => 'v3.4.17',
]);
};

19
importmap.php Normal file
View file

@ -0,0 +1,19 @@
<?php
/**
* Returns the importmap for this application.
*
* - "path" is a path inside the asset mapper system. Use the
* "debug:asset-map" command to see the full list of paths.
*
* - "entrypoint" (JavaScript only) set to true for any module that will
* be used as an "entrypoint" (and passed to the importmap() Twig function).
*
* The "importmap:require" command can be used to add new entries to this file.
*/
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
];

View file

@ -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"
]
}
}

11
tailwind.config.js Normal file
View file

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./assets/**/*.js",
"./templates/**/*.html.twig",
],
theme: {
extend: {},
},
plugins: [],
}

View file

@ -1,4 +1,24 @@
{{ form_start(form) }}
{{ form_widget(form) }}
<button class="btn">{{ button_label|default('Save') }}</button>
{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }}
{% for field in form %}
{% if field.vars.name != '_token' %}
<div class="space-y-2">
{{ form_label(field, null, {'label_attr': {'class': 'block text-sm font-medium leading-6 text-gray-900'}}) }}
<div>
{{ 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'}}) }}
</div>
{% if field.vars.errors|length > 0 %}
<div class="mt-1 text-sm text-red-600">
{{ form_errors(field) }}
</div>
{% endif %}
{% if field.vars.help is defined and field.vars.help %}
<p class="mt-1 text-sm text-gray-500">{{ field.vars.help }}</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
<div class="mt-6 flex items-center justify-end gap-x-6">
<a href="{{ app.request.headers.get('referer', '/') }}" class="text-sm font-semibold leading-6 text-gray-900">Cancel</a>
<button type="submit" class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">{{ button_label|default('Save') }}</button>
</div>
{{ form_end(form) }}

View file

@ -1,46 +1,90 @@
<!DOCTYPE html>
<html>
<html class="h-full bg-gray-100">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0000ff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#222222" media="(prefers-color-scheme: dark)">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" type="image/svg+xml"
href="{{ favicon }}" />
{% set currentDate = "now"|date("d") %}
{% if currentDate % 4 == 0 %}
<link rel="stylesheet" href="/static/css/new.min.css">
{% elseif currentDate % 4 == 1 %}
<link rel="stylesheet" href="/static/css/simple.min.css">
{% elseif currentDate % 4 == 2 %}
<link rel="stylesheet" href="/static/css/water.min.css">
{% else %}
<link rel="stylesheet" href="/static/css/fieber.css">
{% endif %}
<style>
label{
display: block;
}
</style>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Welcome!{% endblock %} - Futtern</title>
<link rel="icon" type="image/svg+xml" href="{{ favicon }}" />
<script src="/static/js/htmx.min.js"></script>
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
{% endblock %}
{% block javascripts %}
{{ importmap() }}
{% endblock %}
</head>
<body>
<header>
<p>Hello {{ app.request.cookies.get('username', 'nobody') }} - <a href="{{ path('username') }}">change name</a></p>
<nav>
<a href="{{ path('app_food_order_index') }}">Orders</a> /
<a href="{{ path('app_food_vendor_index') }}">Vendors</a> /
<a
href="https://git.hannover.ccc.de/lubiana/futtern/issues/new"
target="_blank"
>Create Issue</a> /
<a href="/api">API</a>
<body class="h-full">
<div class="min-h-full">
<nav class="bg-indigo-600">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0">
<span class="text-white text-xl font-bold">Futtern</span>
</div>
<div class="hidden md:block">
<div class="ml-10 flex items-baseline space-x-4">
<a href="{{ path('app_food_order_index') }}" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md px-3 py-2 text-sm font-medium">Orders</a>
<a href="{{ path('app_food_vendor_index') }}" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md px-3 py-2 text-sm font-medium">Vendors</a>
<a href="https://git.hannover.ccc.de/lubiana/futtern/issues/new" target="_blank" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md px-3 py-2 text-sm font-medium">Create Issue</a>
<a href="/api" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md px-3 py-2 text-sm font-medium">API</a>
</div>
</div>
</div>
<div class="hidden md:block">
<div class="ml-4 flex items-center md:ml-6">
<div class="text-white">
Hello {{ app.request.cookies.get('username', 'nobody') }} -
<a href="{{ path('username') }}" class="text-indigo-200 hover:text-white">change name</a>
</div>
</div>
</div>
<div class="-mr-2 flex md:hidden">
<!-- Mobile menu button -->
<button type="button" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-2 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false">
<span class="absolute -inset-0.5"></span>
<span class="sr-only">Open main menu</span>
<!-- Menu open: "hidden", Menu closed: "block" -->
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile menu, show/hide based on menu state. -->
<div class="md:hidden" id="mobile-menu">
<div class="space-y-1 px-2 pb-3 pt-2 sm:px-3">
<a href="{{ path('app_food_order_index') }}" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">Orders</a>
<a href="{{ path('app_food_vendor_index') }}" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">Vendors</a>
<a href="https://git.hannover.ccc.de/lubiana/futtern/issues/new" target="_blank" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">Create Issue</a>
<a href="/api" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">API</a>
</div>
<div class="border-t border-indigo-700 pb-3 pt-4">
<div class="flex items-center px-5">
<div class="text-base font-medium text-white">
Hello {{ app.request.cookies.get('username', 'nobody') }} -
<a href="{{ path('username') }}" class="text-indigo-200 hover:text-white">change name</a>
</div>
</div>
</div>
</div>
</nav>
</header>
<main>
{% block body %}{% endblock %}
</main>
<header class="bg-white shadow">
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold tracking-tight text-gray-900">{% block header %}{% endblock %}</h1>
</div>
</header>
<main>
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
{% block body %}{% endblock %}
</div>
</div>
</main>
</div>
</body>
</html>

View file

@ -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 %}
<h1>FoodOrder index</h1>
<div>
<button
<div class="sm:flex sm:items-center sm:justify-between mb-6">
<div class="min-w-0 flex-1">
<p class="text-sm text-gray-500">
Manage your food orders and create new ones.
</p>
</div>
<div class="mt-4 flex sm:ml-4 sm:mt-0">
<button
hx-get="{{ path('app_food_order_new') }}"
hx-trigger="click"
hx-target="closest div"
>Create new</button>
hx-target="#new-order-form"
class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>Create new order</button>
</div>
</div>
<hr>
<table class="table">
<thead>
<tr>
<th>CreatedBy</th>
<th>Vendor</th>
<th>CreatedAt</th>
<th>ClosedAt</th>
<th>actions</th>
</tr>
</thead>
<tbody>
{% for food_order in food_orders %}
{{ include('food_order/table_row.html.twig') }}
{% endfor %}
{% if food_orders|length < 10 %}
<tr>
<td colspan="5">
check the <a href="{{ path('app_food_order_archive') }}">archive</a>
for older orders
</td>
</tr>
<div id="new-order-form" class="mb-8"></div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Created By</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Vendor</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Created At</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Closed At</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{% for food_order in food_orders %}
{{ include('food_order/table_row.html.twig') }}
{% endfor %}
{% if food_orders|length < 10 %}
<tr>
<td colspan="5" class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-500 sm:pl-6">
Check the <a href="{{ path('app_food_order_archive') }}" class="text-indigo-600 hover:text-indigo-900">archive</a>
for older orders
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-center gap-x-6">
{% if prev_page > 0 %}
<a href="{{ path('app_food_order_archive', {'page': prev_page}) }}" class="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Previous Page</a>
{% endif %}
</tbody>
</table>
{% if prev_page > 0 %}
<a href="{{ path('app_food_order_archive', {'page': prev_page}) }}">previous page</a> |
{% endif %}
{% if next_page > current_page %}
<a href="{{ path('app_food_order_archive', {'page': next_page}) }}">next page</a>
{% endif %}
{% if next_page > current_page %}
<a href="{{ path('app_food_order_archive', {'page': next_page}) }}" class="rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Next Page</a>
{% endif %}
</div>
{% endblock %}

View file

@ -1,2 +1,11 @@
{{ include('_form.html.twig') }}
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">Create New Order</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500">
<p>Fill out the form below to create a new food order.</p>
</div>
<div class="mt-5">
{{ include('_form.html.twig') }}
</div>
</div>
</div>

View file

@ -1,71 +1,101 @@
{% extends 'base.html.twig' %}
{% block title %}FoodOrder{% endblock %}
{% block title %}Order Details{% endblock %}
{% block header %}Order Details{% endblock %}
{% block body %}
<h1>FoodOrder</h1>
<div class="bg-white shadow sm:rounded-lg mb-8">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">Order Information</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Details about the food order from {{ food_order.foodVendor.name }}.</p>
</div>
<div class="border-t border-gray-200">
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Vendor</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">{{ food_order.foodVendor.name }}</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Vendor Phone</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">{{ food_order.foodVendor.phone }}</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Created By</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">{{ food_order.createdBy }}</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Created At</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Closed At</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : 'Not closed yet' }}</dd>
</div>
</dl>
</div>
</div>
<table class="table">
<tbody>
<tr>
<th>Vendor</th>
<td>{{ food_order.foodVendor.name }}</td>
</tr>
<tr>
<th>Vendorphone</th>
<td>{{ food_order.foodVendor.phone }}</td>
</tr>
<tr>
<th>Created By</th>
<td>{{ food_order.createdBy }}</td>
</tr>
<tr>
<th>CreatedAt</th>
<td>{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
</tr>
<tr>
<th>ClosedAt</th>
<td>{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
</tr>
</tbody>
</table>
<a class="button" href="{{ path('app_food_order_index') }}">back to list</a>
{% if(food_order.isClosed) %}
<a class="button" href="{{ path('app_food_order_open', {'id': food_order.id}) }}">reopen</a>
{% else %}
<a class="button" href="{{ path('app_food_order_close', {'id': food_order.id}) }}">close</a>
{% endif %}
<h2>Items</h2>
<table class="table">
<thead>
<tr>
<th>Index</th>
<th>username</th>
<th>name</th>
<th>extras</th>
<th>actions</th>
</tr>
</thead>
<tbody>
{% for item in food_order.orderItemsSortedByName %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ item.createdBy }}</td>
<td>{{ item.name }}</td>
<td>{{ item.extras }}</td>
<td>
{% if(food_order.isClosed) %}
{% else %}
<a href="{{ path('app_order_item_edit', {'id': item.id}) }}">edit</a>
<a href="{{ path('app_order_item_copy', {'id': item.id}) }}">copy</a>
<a href="{{ path('app_order_item_delete', {'id': item.id}) }}">remove</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<a class="button" href="{{ path('app_order_item_new', {'foodOrder': food_order.id}) }}">New Item</a>
<div class="flex space-x-4 mb-8">
<a href="{{ path('app_food_order_index') }}" class="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Back to list
</a>
{% if(food_order.isClosed) %}
<a href="{{ path('app_food_order_open', {'id': food_order.id}) }}" class="inline-flex items-center rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600">
Reopen Order
</a>
{% else %}
<a href="{{ path('app_food_order_close', {'id': food_order.id}) }}" class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">
Close Order
</a>
{% endif %}
</div>
<div class="bg-white shadow sm:rounded-lg mb-8">
<div class="px-4 py-5 sm:px-6 flex justify-between items-center">
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900">Order Items</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Items included in this order.</p>
</div>
<div>
<a href="{{ path('app_order_item_new', {'foodOrder': food_order.id}) }}" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
Add New Item
</a>
</div>
</div>
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">#</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Username</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Item Name</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Extras</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{% for item in food_order.orderItemsSortedByName %}
<tr class="hover:bg-gray-50">
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">{{ loop.index }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ item.createdBy }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ item.name }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ item.extras }}</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
{% if(food_order.isClosed) %}
<span class="text-gray-400">Order closed</span>
{% else %}
<a href="{{ path('app_order_item_edit', {'id': item.id}) }}" class="text-indigo-600 hover:text-indigo-900 mr-2">Edit</a>
<a href="{{ path('app_order_item_copy', {'id': item.id}) }}" class="text-indigo-600 hover:text-indigo-900 mr-2">Copy</a>
<a href="{{ path('app_order_item_delete', {'id': item.id}) }}" class="text-red-600 hover:text-red-900">Remove</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View file

@ -1,9 +1,9 @@
<tr>
<td>{{ food_order.createdBy }}</td>
<td>{{ food_order.foodVendor.name }}</td>
<td>{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
<td>{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
<td>
<a href="{{ path('app_food_order_show', {'id': food_order.id}) }}">show</a>
<tr class="hover:bg-gray-50">
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">{{ food_order.createdBy }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ food_order.foodVendor.name }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<a href="{{ path('app_food_order_show', {'id': food_order.id}) }}" class="text-indigo-600 hover:text-indigo-900">View<span class="sr-only">, order from {{ food_order.createdBy }}</span></a>
</td>
</tr>
</tr>

View file

@ -1,4 +1,24 @@
{{ form_start(form) }}
{{ form_widget(form) }}
<button class="btn">{{ button_label|default('Save') }}</button>
{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }}
{% for field in form %}
{% if field.vars.name != '_token' %}
<div class="space-y-2">
{{ form_label(field, null, {'label_attr': {'class': 'block text-sm font-medium leading-6 text-gray-900'}}) }}
<div>
{{ 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'}}) }}
</div>
{% if field.vars.errors|length > 0 %}
<div class="mt-1 text-sm text-red-600">
{{ form_errors(field) }}
</div>
{% endif %}
{% if field.vars.help is defined and field.vars.help %}
<p class="mt-1 text-sm text-gray-500">{{ field.vars.help }}</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
<div class="mt-6 flex items-center justify-end gap-x-6">
<a href="{{ app.request.headers.get('referer', '/') }}" class="text-sm font-semibold leading-6 text-gray-900">Cancel</a>
<button type="submit" class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">{{ button_label|default('Save') }}</button>
</div>
{{ form_end(form) }}

View file

@ -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 %}
<h1>FoodVendor index</h1>
<div class="sm:flex sm:items-center sm:justify-between mb-6">
<div class="min-w-0 flex-1">
<p class="text-sm text-gray-500">
Manage your food vendors and create new ones.
</p>
</div>
<div class="mt-4 flex sm:ml-4 sm:mt-0">
<a href="{{ path('app_food_vendor_new') }}" class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
Create new vendor
</a>
</div>
</div>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>actions</th>
</tr>
</thead>
<tbody>
{% for food_vendor in food_vendors %}
<tr>
<td>{{ food_vendor.name }}</td>
<td>
<a href="{{ path('app_food_vendor_show', {'id': food_vendor.id}) }}">show</a>
<a href="{{ path('app_food_vendor_edit', {'id': food_vendor.id}) }}">edit</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="3">no records found</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ path('app_food_vendor_new') }}">Create new</a>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Name</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{% for food_vendor in food_vendors %}
<tr class="hover:bg-gray-50">
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">{{ food_vendor.name }}</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<a href="{{ path('app_food_vendor_show', {'id': food_vendor.id}) }}" class="text-indigo-600 hover:text-indigo-900 mr-2">View</a>
<a href="{{ path('app_food_vendor_edit', {'id': food_vendor.id}) }}" class="text-indigo-600 hover:text-indigo-900">Edit</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="2" class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-500 sm:pl-6">No vendors found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -2,8 +2,15 @@
{% block title %}Tell me your name{% endblock %}
{% block header %}Tell me your name{% endblock %}
{% block body %}
<h1>Tell me your name</h1>
<p>By submitting the form, you agree that your username will be stored as a cookie.</p>
{{ include('_form.html.twig') }}
{% endblock %}
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="max-w-xl text-sm text-gray-500 mb-6">
<p>By submitting the form, you agree that your username will be stored as a cookie.</p>
</div>
{{ include('_form.html.twig') }}
</div>
</div>
{% endblock %}

View file

@ -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,20 @@ 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 +127,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 +142,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 +153,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 +163,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 +181,7 @@ describe(FoodOrderController::class, function (): void {
[1, 0, 2],
[2, 1, 3],
[3, 2, 4],
[4, 3, 0, 5],
[4, 3, 0, 10],
]
);

View file

@ -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 {

View file

@ -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();

View file

@ -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);

View file

@ -0,0 +1,16 @@
<?php declare(strict_types=1);
test('example test', function (): void {
// This is a simple example test
expect(true)->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);
});