diff --git a/assets/app.js b/assets/app.js index 88672ac..0194a77 100644 --- a/assets/app.js +++ b/assets/app.js @@ -14,9 +14,11 @@ import './javascript/theme.js'; import './javascript/emoji-footprint.js'; import './javascript/modes.js'; import './javascript/htmx.js'; +import emojiButtonListener from './javascript/emoji-button.js'; import 'bootstrap'; import { initRadioState } from './javascript/radioState.js'; document.addEventListener('DOMContentLoaded', () => { initRadioState(); + emojiButtonListener(); }); \ No newline at end of file diff --git a/assets/javascript/emoji-button.js b/assets/javascript/emoji-button.js new file mode 100644 index 0000000..8fc3825 --- /dev/null +++ b/assets/javascript/emoji-button.js @@ -0,0 +1,14 @@ +const emojiButtonListener = function () { + const buttons = document.querySelectorAll('.emoji-buttons .btn.btn-primary'); + + buttons.forEach(button => { + button.addEventListener('click', function() { + const emojiField = document.querySelector('#food_vendor_emojis'); + if (emojiField) { + emojiField.value += this.textContent; + } + }); + }); +} + +export default emojiButtonListener; \ No newline at end of file diff --git a/assets/styles/modes.css b/assets/styles/modes.css index 383815b..39dd41e 100644 --- a/assets/styles/modes.css +++ b/assets/styles/modes.css @@ -86,6 +86,9 @@ /* 🎭 BONKERS MODE CLASSES 🎭 */ .bonkers-mode { + background: linear-gradient(270deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan), var(--bs-yellow), var(--bs-green), var(--bs-orange), var(--bs-red), var(--bs-pink)); + background-size: 1600% 1600%; + animation: rainbowGradient 10s ease infinite; transition: all 0.3s ease-in-out; } diff --git a/composer.json b/composer.json index 6a39e12..186955e 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "symfony/flex": "^2.7.1", "symfony/form": "7.3.*", "symfony/framework-bundle": "7.3.*", + "symfony/monolog-bundle": "^3.10", "symfony/property-access": "7.3.*", "symfony/property-info": "7.3.*", "symfony/runtime": "7.3.*", diff --git a/composer.lock b/composer.lock index a54695b..46aa9a3 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": "f7f3513731749141a755b5e676088c7d", + "content-hash": "923bae46e3b7f783b6c25f8f68f97991", "packages": [ { "name": "api-platform/core", @@ -1422,6 +1422,109 @@ }, "time": "2025-01-24T11:45:48+00:00" }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, { "name": "nelmio/cors-bundle", "version": "2.5.0", @@ -4020,6 +4123,165 @@ ], "time": "2025-05-29T07:47:32+00:00" }, + { + "name": "symfony/monolog-bridge", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "1b188c8abbbef25b111da878797514b7a8d33990" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/1b188c8abbbef25b111da878797514b7a8d33990", + "reference": "1b188c8abbbef25b111da878797514b7a8d33990", + "shasum": "" + }, + "require": { + "monolog/monolog": "^3", + "php": ">=8.2", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/mailer": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-21T12:17:46+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "php": ">=7.2.5", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T17:08:13+00:00" + }, { "name": "symfony/options-resolver", "version": "v7.3.0", @@ -10519,7 +10781,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -10527,7 +10789,7 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.4" }, diff --git a/config/bundles.php b/config/bundles.php index f212861..2c6ab01 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -8,6 +8,7 @@ use Liip\TestFixturesBundle\LiipTestFixturesBundle; use Nelmio\CorsBundle\NelmioCorsBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\MakerBundle\MakerBundle; +use Symfony\Bundle\MonologBundle\MonologBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; @@ -53,4 +54,7 @@ return [ TwigExtraBundle::class => [ 'all' => true, ], + MonologBundle::class => [ + 'all' => true, + ], ]; diff --git a/config/packages/monolog.php b/config/packages/monolog.php new file mode 100644 index 0000000..a4075ba --- /dev/null +++ b/config/packages/monolog.php @@ -0,0 +1,95 @@ +extension('monolog', [ + 'channels' => [ + 'deprecation', + ], + ]); + if ($containerConfigurator->env() === 'dev') { + $containerConfigurator->extension('monolog', [ + 'handlers' => [ + 'main' => [ + 'type' => 'stream', + 'path' => '%kernel.logs_dir%/%kernel.environment%.log', + 'level' => 'debug', + 'channels' => [ + '!event', + ], + ], + 'console' => [ + 'type' => 'console', + 'process_psr_3_messages' => false, + 'channels' => [ + '!event', + '!doctrine', + '!console', + ], + ], + ], + ]); + } + if ($containerConfigurator->env() === 'test') { + $containerConfigurator->extension('monolog', [ + 'handlers' => [ + 'main' => [ + 'type' => 'fingers_crossed', + 'action_level' => 'error', + 'handler' => 'nested', + 'excluded_http_codes' => [ + 404, + 405, + ], + 'channels' => [ + '!event', + ], + ], + 'nested' => [ + 'type' => 'stream', + 'path' => '%kernel.logs_dir%/%kernel.environment%.log', + 'level' => 'debug', + ], + ], + ]); + } + if ($containerConfigurator->env() === 'prod') { + $containerConfigurator->extension('monolog', [ + 'handlers' => [ + 'main' => [ + 'type' => 'fingers_crossed', + 'action_level' => 'error', + 'handler' => 'nested', + 'excluded_http_codes' => [ + 404, + 405, + ], + 'buffer_size' => 50, + ], + 'nested' => [ + 'type' => 'stream', + 'path' => 'php://stderr', + 'level' => 'debug', + 'formatter' => 'monolog.formatter.json', + ], + 'console' => [ + 'type' => 'console', + 'process_psr_3_messages' => false, + 'channels' => [ + '!event', + '!doctrine', + ], + ], + 'deprecation' => [ + 'type' => 'stream', + 'channels' => [ + 'deprecation', + ], + 'path' => 'php://stderr', + 'formatter' => 'monolog.formatter.json', + ], + ], + ]); + } +}; diff --git a/deploy/prepare-deploy.sh b/deploy/prepare-deploy.sh index 40af730..b57c20c 100755 --- a/deploy/prepare-deploy.sh +++ b/deploy/prepare-deploy.sh @@ -8,7 +8,7 @@ fi mkdir $TARGETDIR cd $TARGETDIR || return -pathsToCopy="public bin config migrations src templates composer.json composer.lock symfony.lock .env" +pathsToCopy="assets public bin config migrations src templates composer.json composer.lock symfony.lock .env importmap.php" for path in $pathsToCopy do diff --git a/deploy/update.sh b/deploy/update.sh index 4d7548a..59ea373 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -5,4 +5,7 @@ systemctl --user start pod-futtern sleep 2 podman exec -it futtern-php /var/www/html/bin/console cache:clear podman exec -it futtern-php /var/www/html/bin/console cache:warmup +podman exec -it futtern-php /var/www/html/bin/console asset-map:compile + + echo 'yes' | podman exec -it futtern-php /var/www/html/bin/console doctrine:migrations:migrate diff --git a/importmap.php b/importmap.php index 36a6905..3f89e51 100644 --- a/importmap.php +++ b/importmap.php @@ -27,6 +27,6 @@ return [ 'type' => 'css', ], 'htmx.org' => [ - 'version' => '1.9.12', + 'version' => '2.0.5', ], ]; diff --git a/migrations/Version20250621131822.php b/migrations/Version20250621131822.php new file mode 100644 index 0000000..735b90d --- /dev/null +++ b/migrations/Version20250621131822.php @@ -0,0 +1,47 @@ +addSql(<<<'SQL' + ALTER TABLE food_vendor ADD COLUMN emojis VARCHAR(30) DEFAULT NULL + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__food_vendor AS SELECT name, phone, menu_link, id FROM food_vendor + SQL); + $this->addSql(<<<'SQL' + DROP TABLE food_vendor + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE food_vendor (name VARCHAR(50) NOT NULL, phone VARCHAR(50) DEFAULT '', menu_link VARCHAR(255) DEFAULT NULL, id BLOB NOT NULL, PRIMARY KEY(id)) + SQL); + $this->addSql(<<<'SQL' + INSERT INTO food_vendor (name, phone, menu_link, id) SELECT name, phone, menu_link, id FROM __temp__food_vendor + SQL); + $this->addSql(<<<'SQL' + DROP TABLE __temp__food_vendor + SQL); + } +} diff --git a/src/Entity/FoodVendor.php b/src/Entity/FoodVendor.php index a0b3708..83bef71 100644 --- a/src/Entity/FoodVendor.php +++ b/src/Entity/FoodVendor.php @@ -7,23 +7,27 @@ use App\Repository\FoodVendorRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use InvalidArgumentException; use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator; use Symfony\Bridge\Doctrine\Types\UlidType; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Uid\Ulid; +use Symfony\Component\Validator\Constraints\Length; + +use function mb_strlen; #[ORM\Entity(repositoryClass: FoodVendorRepository::class)] #[ApiResource] class FoodVendor { #[ORM\Column(length: 50)] - #[Groups(['food_order:latest'])] + #[Groups(['food_order:latest', 'food_vendor:read'])] private string|null $name = null; #[ORM\Column(length: 50, nullable: true, options: [ 'default' => '', ])] - #[Groups(['food_order:latest'])] + #[Groups(['food_order:latest', 'food_vendor:read'])] private string|null $phone = null; /** @@ -42,6 +46,14 @@ class FoodVendor #[Groups(['food_order:latest'])] private string|null $menuLink = null; + /** + * String of emojis (max 30 characters) + */ + #[ORM\Column(length: 30, nullable: true)] + #[Groups(['food_order:latest', 'food_vendor:read'])] + #[Length(max: 10)] + private string|null $emojis = null; + public function __construct( #[ORM\Id] #[ORM\GeneratedValue(strategy: 'CUSTOM')] @@ -155,4 +167,19 @@ class FoodVendor $this->phone = $phone; return $this; } + + public function getEmojis(): string|null + { + return $this->emojis; + } + + public function setEmojis(string|null $emojis): static + { + if ($emojis !== null && mb_strlen($emojis) > 30) { + throw new InvalidArgumentException('A maximum of 30 characters is allowed for emojis'); + } + + $this->emojis = $emojis; + return $this; + } } diff --git a/src/Form/FoodVendorType.php b/src/Form/FoodVendorType.php index 7ff61da..466d9e2 100644 --- a/src/Form/FoodVendorType.php +++ b/src/Form/FoodVendorType.php @@ -17,6 +17,7 @@ final class FoodVendorType extends AbstractType ->add('name') ->add('menuLink') ->add('phone') + ->add('emojis') ; } diff --git a/symfony.lock b/symfony.lock index cfab487..79a5298 100644 --- a/symfony.lock +++ b/symfony.lock @@ -196,6 +196,18 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/monolog-bundle": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "aff23899c4440dd995907613c1dd709b6f59503f" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, "symfony/property-info": { "version": "7.3", "recipe": { diff --git a/templates/food_vendor/edit.html.twig b/templates/food_vendor/edit.html.twig index 83e4bd3..51daaf7 100644 --- a/templates/food_vendor/edit.html.twig +++ b/templates/food_vendor/edit.html.twig @@ -9,5 +9,121 @@ {{ include('food_vendor/_form.html.twig', {'button_label': 'Update'}) }} +
+ back to list {% endblock %} diff --git a/tests/Unit/Entity/FoodVendorTest.php b/tests/Unit/Entity/FoodVendorTest.php index 88ab094..ee00ece 100644 --- a/tests/Unit/Entity/FoodVendorTest.php +++ b/tests/Unit/Entity/FoodVendorTest.php @@ -5,6 +5,7 @@ namespace App\Tests\Unit\Entity; use App\Entity\FoodOrder; use App\Entity\FoodVendor; use App\Entity\MenuItem; +use InvalidArgumentException; use Symfony\Component\Uid\Ulid; use function describe; @@ -20,6 +21,17 @@ describe(FoodVendor::class, function (): void { $vendor->setPhone('1234567890'); $this->assertEquals('1234567890', $vendor->getPhone()); + // Test emojis field + $this->assertNull($vendor->getEmojis()); + $emojis = '😀😂🎉👍❤️'; + $vendor->setEmojis($emojis); + $this->assertEquals($emojis, $vendor->getEmojis()); + + // Test emojis validation + $tooManyEmojis = '😀😂🎉👍❤️🚀🎈🎁🎊🎋🎍🎎🎏🎐🎑🎒🎓🎔🎕🎖🎗🎘🎙🎚🎛🎜🎝🎞🎟🎠🎡🎢'; + $this->expectException(InvalidArgumentException::class); + $vendor->setEmojis($tooManyEmojis); + $this->assertCount(0, $vendor->getFoodOrders()); $order1 = new FoodOrder; $vendor->addFoodOrder($order1);