diff --git a/.cursor/rules/react.mdc b/.cursor/rules/react.mdc index abdb334..76a3ac1 100644 --- a/.cursor/rules/react.mdc +++ b/.cursor/rules/react.mdc @@ -6,4 +6,9 @@ alwaysApply: true # React components 1. You will always provide a maintainable folder structure when adding or updating react components. -2. You will reuse common components \ No newline at end of file +2. You will reuse common components + +# Folders +- components: general reusable components +- pages: specific components related to the domain and the application +- lib: plain typescript for building the domain logic, services etc. you will never put react code in here \ No newline at end of file diff --git a/.cursor/rules/symfony.mdc b/.cursor/rules/symfony.mdc new file mode 100644 index 0000000..eb65e23 --- /dev/null +++ b/.cursor/rules/symfony.mdc @@ -0,0 +1,8 @@ +--- +description: +globs: +alwaysApply: true +--- +# Updating / Creating Entities +When updating or creating entities, always update and the related DTO classes. +DTO Classes are a representation of a JSON Schema \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..be8fa1d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "phpstan.binPath": "backend/vendor/bin/phpstan", + "phpstan.configFile": "backend/phpstan.dist.neon", + "phpstan.checkValidity": true, + "phpstan.showTypeOnHover": false, + "phpstan.showProgress": true +} \ No newline at end of file diff --git a/backend/.env b/backend/.env index 7c428e5..2df5f9b 100644 --- a/backend/.env +++ b/backend/.env @@ -18,3 +18,13 @@ APP_ENV=dev APP_SECRET=71bf50bfb778d456b3a376ff60d5dcd8 ###< symfony/framework-bundle ### + +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" +# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" +DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres?serverVersion=16&charset=utf8" +###< doctrine/doctrine-bundle ### diff --git a/backend/.env.dev b/backend/.env.dev deleted file mode 100644 index e69de29..0000000 diff --git a/backend/.gitignore b/backend/.gitignore index a67f91e..fb8e1e9 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -8,3 +8,7 @@ /var/ /vendor/ ###< symfony/framework-bundle ### + +###> phpstan/phpstan ### +phpstan.neon +###< phpstan/phpstan ### diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..051f6d1 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,38 @@ +# Domain-Driven Design Structure + +This project follows Domain-Driven Design (DDD) principles with the following structure: + +## Application Layer +Contains application-specific logic and serves as the entry point for external requests. + +- **Controller**: HTTP controllers that handle web requests +- **DTO**: Data Transfer Objects for API request/response + +## Domain Layer +Contains the core business logic and domain models. + +- **Model**: Domain entities representing the core business concepts + +## Infrastructure Layer +Provides technical capabilities that support the higher layers. + +- **Repository**: Data access logic for persisting and retrieving domain objects +- **DataFixtures**: Test data fixtures for development and testing + +## Shared +Contains cross-cutting concerns and utilities used across all layers. + +## Folder Structure + +``` +src/ +├── Application/ +│ ├── Controller/ +│ └── DTO/ +├── Domain/ +│ └── Model/ +├── Infrastructure/ +│ ├── Repository/ +│ └── DataFixtures/ +└── Shared/ +``` \ No newline at end of file diff --git a/backend/composer.json b/backend/composer.json index 6b85ccd..ffd0e25 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -7,14 +7,25 @@ "php": ">=8.1", "ext-ctype": "*", "ext-iconv": "*", + "doctrine/annotations": "^2.0", + "doctrine/doctrine-bundle": "^2.14", + "doctrine/doctrine-migrations-bundle": "^3.4", + "doctrine/orm": "^3.3", "nelmio/api-doc-bundle": "^5.0", + "phpdocumentor/reflection-docblock": "^5.6", + "phpstan/phpdoc-parser": "^2.1", "symfony/asset": "6.4.*", "symfony/console": "6.4.*", "symfony/dotenv": "6.4.*", "symfony/flex": "^2", "symfony/framework-bundle": "6.4.*", + "symfony/property-access": "6.4.*", + "symfony/property-info": "6.4.*", "symfony/runtime": "6.4.*", + "symfony/serializer": "6.4.*", "symfony/twig-bundle": "6.4.*", + "symfony/uid": "6.4.*", + "symfony/validator": "6.4.*", "symfony/yaml": "6.4.*" }, "config": { @@ -54,7 +65,9 @@ ], "post-update-cmd": [ "@auto-scripts" - ] + ], + "phpstan": "phpstan analyse --ansi", + "rebuild-db": "php bin/console doctrine:schema:drop --force && php bin/console doctrine:schema:update --force && php bin/console doctrine:fixtures:load --no-interaction" }, "conflict": { "symfony/symfony": "*" @@ -64,5 +77,11 @@ "allow-contrib": false, "require": "6.4.*" } + }, + "require-dev": { + "doctrine/doctrine-fixtures-bundle": "^4.1", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-symfony": "^2.0", + "symfony/maker-bundle": "^1.62" } } diff --git a/backend/composer.lock b/backend/composer.lock index b610c27..9b62630 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,8 +4,276 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4039935fb5570ed6d17240c7dec929ec", + "content-hash": "58ca9f6d53632372fae9dee2c6c72aa7", "packages": [ + { + "name": "doctrine/annotations", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2 || ^3", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/2.0.2" + }, + "time": "2024-09-05T10:17:24+00:00" + }, + { + "name": "doctrine/collections", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/2eb07e5953eed811ce1b309a7478a3b236f2273d", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "ext-json": "*", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.3.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2025-03-22T10:17:19+00:00" + }, + { + "name": "doctrine/dbal", + "version": "4.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/33d2d7fe1269b2301640c44cf2896ea607b30e3e", + "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^0.5.3|^1", + "php": "^8.1", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "10.5.39", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^6.3.8|^7.0", + "symfony/console": "^5.4|^6.3|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-03-07T18:29:05+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.5", @@ -54,6 +322,886 @@ }, "time": "2025-04-07T20:06:18+00:00" }, + { + "name": "doctrine/doctrine-bundle", + "version": "2.14.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "ca6a7350b421baf7fbdefbf9f4993292ed18effb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/ca6a7350b421baf7fbdefbf9f4993292ed18effb", + "reference": "ca6a7350b421baf7fbdefbf9f4993292ed18effb", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^3.7.0 || ^4.0", + "doctrine/persistence": "^3.1 || ^4", + "doctrine/sql-formatter": "^1.0.1", + "php": "^8.1", + "symfony/cache": "^6.4 || ^7.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/service-contracts": "^2.5 || ^3" + }, + "conflict": { + "doctrine/annotations": ">=3.0", + "doctrine/cache": "< 1.11", + "doctrine/orm": "<2.17 || >=4.0", + "symfony/var-exporter": "< 6.4.1 || 7.0.0", + "twig/twig": "<2.13 || >=3.0 <3.0.4" + }, + "require-dev": { + "doctrine/annotations": "^1 || ^2", + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^12", + "doctrine/deprecations": "^1.0", + "doctrine/orm": "^2.17 || ^3.0", + "friendsofphp/proxy-manager-lts": "^1.0", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9.6.22", + "psr/log": "^1.1.4 || ^2.0 || ^3.0", + "symfony/doctrine-messenger": "^6.4 || ^7.0", + "symfony/messenger": "^6.4 || ^7.0", + "symfony/phpunit-bridge": "^7.2", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/stopwatch": "^6.4 || ^7.0", + "symfony/string": "^6.4 || ^7.0", + "symfony/twig-bridge": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "symfony/var-exporter": "^6.4.1 || ^7.0.1", + "symfony/web-profiler-bundle": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0", + "twig/twig": "^2.13 || ^3.0.4" + }, + "suggest": { + "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", + "ext-pdo": "*", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org/" + } + ], + "description": "Symfony DoctrineBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineBundle/issues", + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.14.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle", + "type": "tidelift" + } + ], + "time": "2025-03-22T17:28:21+00:00" + }, + { + "name": "doctrine/doctrine-migrations-bundle", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "e858ce0f5c12b266dce7dce24834448355155da7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/e858ce0f5c12b266dce7dce24834448355155da7", + "reference": "e858ce0f5c12b266dce7dce24834448355155da7", + "shasum": "" + }, + "require": { + "doctrine/doctrine-bundle": "^2.4", + "doctrine/migrations": "^3.2", + "php": "^7.2 || ^8.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/coding-standard": "^12", + "doctrine/orm": "^2.6 || ^3", + "doctrine/persistence": "^2.0 || ^3", + "phpstan/phpstan": "^1.4 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpstan/phpstan-symfony": "^1.3 || ^2", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/phpunit-bridge": "^6.3 || ^7", + "symfony/var-exporter": "^5.4 || ^6 || ^7" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\MigrationsBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineMigrationsBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "dbal", + "migrations", + "schema" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle", + "type": "tidelift" + } + ], + "time": "2025-01-27T22:48:22+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "doctrine/migrations", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "325b61e41d032f5f7d7e2d11cbefff656eadc9ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/325b61e41d032f5f7d7e2d11cbefff656eadc9ab", + "reference": "325b61e41d032f5f7d7e2d11cbefff656eadc9ab", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/dbal": "^3.6 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2.0", + "php": "^8.1", + "psr/log": "^1.1.3 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.2 || ^7.0" + }, + "conflict": { + "doctrine/orm": "<2.12 || >=4" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "doctrine/orm": "^2.13 || ^3", + "doctrine/persistence": "^2 || ^3 || ^4", + "doctrine/sql-formatter": "^1.0", + "ext-pdo_sqlite": "*", + "fig/log-test": "^1", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.4", + "phpstan/phpstan-symfony": "^1.3", + "phpunit/phpunit": "^10.3", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", + "symfony/yaml": "Allows the use of yaml for migration configuration files." + }, + "bin": [ + "bin/doctrine-migrations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Migrations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" + } + ], + "description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.", + "homepage": "https://www.doctrine-project.org/projects/migrations.html", + "keywords": [ + "database", + "dbal", + "migrations" + ], + "support": { + "issues": "https://github.com/doctrine/migrations/issues", + "source": "https://github.com/doctrine/migrations/tree/3.9.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fmigrations", + "type": "tidelift" + } + ], + "time": "2025-03-26T06:48:45+00:00" + }, + { + "name": "doctrine/orm", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "c9557c588b3a70ed93caff069d0aa75737f25609" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/c9557c588b3a70ed93caff069d0aa75737f25609", + "reference": "c9557c588b3a70ed93caff069d0aa75737f25609", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/collections": "^2.2", + "doctrine/dbal": "^3.8.2 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^3", + "doctrine/persistence": "^3.3.1 || ^4", + "ext-ctype": "*", + "php": "^8.1", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.3.9 || ^7.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0", + "phpbench/phpbench": "^1.0", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "2.0.3", + "phpstan/phpstan-deprecation-rules": "^2", + "phpunit/phpunit": "^10.4.0", + "psr/log": "^1 || ^2 || ^3", + "squizlabs/php_codesniffer": "3.7.2", + "symfony/cache": "^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/3.3.2" + }, + "time": "2025-02-04T19:43:15+00:00" + }, + { + "name": "doctrine/persistence", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "45004aca79189474f113cbe3a53847c2115a55fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/45004aca79189474f113cbe3a53847c2115a55fa", + "reference": "45004aca79189474f113cbe3a53847c2115a55fa", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1 || ^2", + "php": "^8.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "doctrine/common": "<2.10" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "1.12.7", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^9.6", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src/Persistence" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/4.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2024-11-01T21:49:07+00:00" + }, + { + "name": "doctrine/sql-formatter", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/sql-formatter.git", + "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "ergebnis/phpunit-slow-test-detector": "^2.14", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5" + }, + "bin": [ + "bin/sql-formatter" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\SqlFormatter\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Dorn", + "email": "jeremy@jeremydorn.com", + "homepage": "https://jeremydorn.com/" + } + ], + "description": "a PHP SQL highlighting library", + "homepage": "https://github.com/doctrine/sql-formatter/", + "keywords": [ + "highlight", + "sql" + ], + "support": { + "issues": "https://github.com/doctrine/sql-formatter/issues", + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.2" + }, + "time": "2025-01-24T11:45:48+00:00" + }, { "name": "nelmio/api-doc-bundle", "version": "v5.0.1", @@ -1215,6 +2363,114 @@ ], "time": "2024-09-25T14:20:29+00:00" }, + { + "name": "symfony/doctrine-bridge", + "version": "v6.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "7205dbc642bac2ecbf108fadbf9a04aa08290a2a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/7205dbc642bac2ecbf108fadbf9a04aa08290a2a", + "reference": "7205dbc642bac2ecbf108fadbf9a04aa08290a2a", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1.2|^2", + "doctrine/persistence": "^2.5|^3.1|^4", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "doctrine/lexer": "<1.1", + "doctrine/orm": "<2.15", + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<6.2", + "symfony/form": "<5.4.38|>=6,<6.4.6|>=7,<7.0.6", + "symfony/http-foundation": "<6.3", + "symfony/http-kernel": "<6.2", + "symfony/lock": "<6.3", + "symfony/messenger": "<5.4", + "symfony/property-info": "<5.4", + "symfony/security-bundle": "<5.4", + "symfony/security-core": "<6.4", + "symfony/validator": "<6.4" + }, + "require-dev": { + "doctrine/collections": "^1.0|^2.0", + "doctrine/data-fixtures": "^1.1|^2", + "doctrine/dbal": "^2.13.1|^3|^4", + "doctrine/orm": "^2.15|^3", + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.2|^7.0", + "symfony/doctrine-messenger": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4.38|^6.4.6|^7.0.6", + "symfony/http-kernel": "^6.3|^7.0", + "symfony/lock": "^6.3|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/proxy-manager-bridge": "^6.4", + "symfony/security-core": "^6.4|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Doctrine with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.20" + }, + "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-28T20:55:44+00:00" + }, { "name": "symfony/dotenv", "version": "v6.4.16", @@ -2440,6 +3696,238 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T12:04:04+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/property-access", + "version": "v6.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "80e0378f2f058b60d87dedc3c760caec882e992c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/80e0378f2f058b60d87dedc3c760caec882e992c", + "reference": "80e0378f2f058b60d87dedc3c760caec882e992c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/property-info": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "symfony/cache": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v6.4.18" + }, + "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-16T14:42:05+00:00" + }, { "name": "symfony/property-info", "version": "v6.4.18", @@ -2688,6 +4176,104 @@ ], "time": "2024-11-05T16:39:55+00:00" }, + { + "name": "symfony/serializer", + "version": "v6.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "a221b2f6066af304d760cff7a26f201b4fab4aef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/a221b2f6066af304d760cff7a26f201b4fab4aef", + "reference": "a221b2f6066af304d760cff7a26f201b4fab4aef", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<5.4", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4.24|>=6,<6.2.11", + "symfony/uid": "<5.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.26|^6.3|^7.0", + "symfony/property-info": "^5.4.24|^6.2.11|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "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": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v6.4.19" + }, + "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-24T08:42:36+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.5.1", @@ -2771,6 +4357,68 @@ ], "time": "2024-09-25T14:20:29+00:00" }, + { + "name": "symfony/stopwatch", + "version": "v6.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", + "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v6.4.19" + }, + "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-21T10:06:30+00:00" + }, { "name": "symfony/string", "version": "v6.4.15", @@ -3128,6 +4776,177 @@ ], "time": "2024-09-25T14:18:03+00:00" }, + { + "name": "symfony/uid", + "version": "v6.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "18eb207f0436a993fffbdd811b5b8fa35fa5e007" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/18eb207f0436a993fffbdd811b5b8fa35fa5e007", + "reference": "18eb207f0436a993fffbdd811b5b8fa35fa5e007", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:18:03+00:00" + }, + { + "name": "symfony/validator", + "version": "v6.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "9314555aceb8d8ce8abda81e1e47e439258d9309" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/9314555aceb8d8ce8abda81e1e47e439258d9309", + "reference": "9314555aceb8d8ce8abda81e1e47e439258d9309", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.13", + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<5.4", + "symfony/expression-language": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/intl": "<5.4", + "symfony/property-info": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13|^2", + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v6.4.20" + }, + "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-14T14:22:58+00:00" + }, { "name": "symfony/var-dumper", "version": "v6.4.18", @@ -3586,7 +5405,459 @@ "time": "2025-04-18T00:35:12+00:00" } ], - "packages-dev": [], + "packages-dev": [ + { + "name": "doctrine/data-fixtures", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "f7f1e12d6bceb58c204b3e77210a103c1c57601e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/f7f1e12d6bceb58c204b3e77210a103c1c57601e", + "reference": "f7f1e12d6bceb58c204b3e77210a103c1c57601e", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^3.1 || ^4.0", + "php": "^8.1", + "psr/log": "^1.1 || ^2 || ^3" + }, + "conflict": { + "doctrine/dbal": "<3.5 || >=5", + "doctrine/orm": "<2.14 || >=4", + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "doctrine/dbal": "^3.5 || ^4", + "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", + "doctrine/orm": "^2.14 || ^3", + "ext-sqlite3": "*", + "fig/log-test": "^1", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5.3", + "symfony/cache": "^6.4 || ^7", + "symfony/var-exporter": "^6.4 || ^7" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database" + ], + "support": { + "issues": "https://github.com/doctrine/data-fixtures/issues", + "source": "https://github.com/doctrine/data-fixtures/tree/2.0.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures", + "type": "tidelift" + } + ], + "time": "2025-01-21T13:21:31+00:00" + }, + { + "name": "doctrine/doctrine-fixtures-bundle", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", + "reference": "a06db6b81ff20a2980bf92063d80c013bb8b4b7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/a06db6b81ff20a2980bf92063d80c013bb8b4b7c", + "reference": "a06db6b81ff20a2980bf92063d80c013bb8b4b7c", + "shasum": "" + }, + "require": { + "doctrine/data-fixtures": "^2.0", + "doctrine/doctrine-bundle": "^2.2", + "doctrine/orm": "^2.14.0 || ^3.0", + "doctrine/persistence": "^2.4 || ^3.0 || ^4.0", + "php": "^8.1", + "psr/log": "^2 || ^3", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/doctrine-bridge": "^6.4.16 || ^7.1.9", + "symfony/http-kernel": "^6.4 || ^7.0" + }, + "conflict": { + "doctrine/dbal": "< 3" + }, + "require-dev": { + "doctrine/coding-standard": "13.0.0", + "phpstan/phpstan": "2.1.11", + "phpunit/phpunit": "^10.5.38 || 11.4.14" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\FixturesBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineFixturesBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "Fixture", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/4.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-fixtures-bundle", + "type": "tidelift" + } + ], + "time": "2025-03-26T10:56:26+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.12", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "96dde49e967c0c22812bcfa7bda4ff82c09f3b0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/96dde49e967c0c22812bcfa7bda4ff82c09f3b0c", + "reference": "96dde49e967c0c22812bcfa7bda4ff82c09f3b0c", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-04-16T13:19:18+00:00" + }, + { + "name": "phpstan/phpstan-symfony", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-symfony.git", + "reference": "648087fb4dd865a09b1828a3b0396eb447665f2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/648087fb4dd865a09b1828a3b0396eb447665f2e", + "reference": "648087fb4dd865a09b1828a3b0396eb447665f2e", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.2" + }, + "conflict": { + "symfony/framework-bundle": "<3.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "psr/container": "1.0 || 1.1.1", + "symfony/config": "^5.4 || ^6.1", + "symfony/console": "^5.4 || ^6.1", + "symfony/dependency-injection": "^5.4 || ^6.1", + "symfony/form": "^5.4 || ^6.1", + "symfony/framework-bundle": "^5.4 || ^6.1", + "symfony/http-foundation": "^5.4 || ^6.1", + "symfony/messenger": "^5.4", + "symfony/polyfill-php80": "^1.24", + "symfony/serializer": "^5.4", + "symfony/service-contracts": "^2.2.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukáš Unger", + "email": "looky.msc@gmail.com", + "homepage": "https://lookyman.net" + } + ], + "description": "Symfony Framework extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-symfony/issues", + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.4" + }, + "time": "2025-03-28T12:02:03+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "v1.62.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "468ff2708200c95ebc0d85d3174b6c6711b8a590" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/468ff2708200c95ebc0d85d3174b6c6711b8a590", + "reference": "468ff2708200c95ebc0d85d3174b6c6711b8a590", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^4.18|^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.62.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-15T00:21:40+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "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/v6.4.20" + }, + "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-10T17:11:00+00:00" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": [], diff --git a/backend/config/bundles.php b/backend/config/bundles.php index 8962353..c6fb3fd 100644 --- a/backend/config/bundles.php +++ b/backend/config/bundles.php @@ -4,4 +4,8 @@ return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], ]; diff --git a/backend/config/packages/doctrine.yaml b/backend/config/packages/doctrine.yaml new file mode 100644 index 0000000..c5e0d10 --- /dev/null +++ b/backend/config/packages/doctrine.yaml @@ -0,0 +1,51 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + #server_version: '16' + + profiling_collect_backtrace: '%kernel.debug%' + use_savepoints: true + driver: 'pdo_pgsql' + orm: + auto_generate_proxy_classes: true + enable_lazy_ghost_objects: true + report_fields_where_declared: true + validate_xml_mapping: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + App: + type: attribute + is_bundle: false + dir: '%kernel.project_dir%/src/Domain/Model' + prefix: 'App\Domain\Model' + alias: App + +when@test: + doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/backend/config/packages/doctrine_migrations.yaml b/backend/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000..29231d9 --- /dev/null +++ b/backend/config/packages/doctrine_migrations.yaml @@ -0,0 +1,6 @@ +doctrine_migrations: + migrations_paths: + # namespace is arbitrary but should be different from App\Migrations + # as migrations classes should NOT be autoloaded + 'DoctrineMigrations': '%kernel.project_dir%/migrations' + enable_profiler: false diff --git a/backend/config/packages/nelmio_api_doc.yaml b/backend/config/packages/nelmio_api_doc.yaml index c1344b2..ad8fadf 100644 --- a/backend/config/packages/nelmio_api_doc.yaml +++ b/backend/config/packages/nelmio_api_doc.yaml @@ -4,6 +4,10 @@ nelmio_api_doc: title: Calendi API description: API Documentation for Calendi version: 1.0.0 + components: + schemas: + directory: "%kernel.project_dir%/public/schema" + type: json areas: path_patterns: - ^/api(?!/doc$) diff --git a/backend/config/packages/uid.yaml b/backend/config/packages/uid.yaml new file mode 100644 index 0000000..0152094 --- /dev/null +++ b/backend/config/packages/uid.yaml @@ -0,0 +1,4 @@ +framework: + uid: + default_uuid_version: 7 + time_based_uuid_version: 7 diff --git a/backend/config/packages/validator.yaml b/backend/config/packages/validator.yaml new file mode 100644 index 0000000..0201281 --- /dev/null +++ b/backend/config/packages/validator.yaml @@ -0,0 +1,13 @@ +framework: + validation: + email_validation_mode: html5 + + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] + +when@test: + framework: + validation: + not_compromised_password: false diff --git a/backend/config/routes.yaml b/backend/config/routes.yaml index 41ef814..316c9ce 100644 --- a/backend/config/routes.yaml +++ b/backend/config/routes.yaml @@ -1,5 +1,5 @@ controllers: resource: - path: ../src/Controller/ - namespace: App\Controller + path: ../src/Application/Controller/ + namespace: App\Application\Controller type: attribute diff --git a/backend/phpstan.dist.neon b/backend/phpstan.dist.neon new file mode 100644 index 0000000..6b7f2be --- /dev/null +++ b/backend/phpstan.dist.neon @@ -0,0 +1,10 @@ +parameters: + level: 10 + paths: + - src + symfony: + containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml + +includes: + - vendor/phpstan/phpstan-symfony/extension.neon + - vendor/phpstan/phpstan-symfony/rules.neon \ No newline at end of file diff --git a/backend/src/Application/Controller/CalendarController.php b/backend/src/Application/Controller/CalendarController.php new file mode 100644 index 0000000..05de992 --- /dev/null +++ b/backend/src/Application/Controller/CalendarController.php @@ -0,0 +1,53 @@ +query->get('start'); + $end = $request->query->get('end'); + + if (!$start || !$end) { + // Default to current month if not specified + $now = new \DateTimeImmutable(); + $start = $now->modify('first day of this month')->setTime(0, 0); + $end = $now->modify('last day of this month')->setTime(23, 59, 59); + } else { + $start = new \DateTimeImmutable($start); + $end = new \DateTimeImmutable($end); + } + + $events = $this->eventRepository->findByDateRange($start, $end); + + return $this->json([ + 'events' => $events, + ]); + } +} \ No newline at end of file diff --git a/backend/src/Application/Controller/EventController.php b/backend/src/Application/Controller/EventController.php new file mode 100644 index 0000000..c95745e --- /dev/null +++ b/backend/src/Application/Controller/EventController.php @@ -0,0 +1,243 @@ +eventRepository->findAll(); + $eventDTOs = array_map(static fn (Event $event) => EventDTO::fromEntity($event), $events); + + return $this->json($eventDTOs); + } + + #[Route('/{id}', name: 'get', methods: ['GET'])] + #[OA\Tag(name: 'Events')] + #[OA\Parameter( + name: 'id', + description: 'Event ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + )] + #[OA\Response( + response: 200, + description: 'Returns event details', + content: new OA\JsonContent(ref: new Model(type: EventDTO::class)) + )] + #[OA\Response( + response: 404, + description: 'Event not found' + )] + public function get(string $id): JsonResponse + { + $event = $this->eventRepository->find($id); + + if (!$event instanceof Event) { + return $this->json(['error' => 'Event not found'], Response::HTTP_NOT_FOUND); + } + + return $this->json(EventDTO::fromEntity($event)); + } + + #[Route('', name: 'create', methods: ['POST'])] + #[OA\Tag(name: 'Events')] + #[OA\RequestBody( + required: true, + content: new OA\JsonContent(ref: new Model(type: EventDTO::class)) + )] + #[OA\Response( + response: 201, + description: 'Event created', + content: new OA\JsonContent(ref: new Model(type: EventDTO::class)) + )] + #[OA\Response( + response: 400, + description: 'Invalid input' + )] + public function create(Request $request): JsonResponse + { + /** @var array|null $data */ + $data = json_decode($request->getContent(), true); + + if (!is_array($data)) { + return $this->json(['error' => 'Invalid JSON'], Response::HTTP_BAD_REQUEST); + } + + $event = new Event(); + $event->setTitle((string)$data['title']); + + if (array_key_exists('description', $data)) { + $event->setDescription($data['description'] !== null ? (string)$data['description'] : null); + } + + try { + $event->setStart(new \DateTimeImmutable($data['start'] ?? 'now')); + $event->setEnd(new \DateTimeImmutable($data['end'] ?? 'now')); + } catch (\Exception) { + return $this->json(['error' => 'Invalid date format'], Response::HTTP_BAD_REQUEST); + } + + $event->setAllDay(isset($data['allDay']) ? (bool)$data['allDay'] : false); + + $errors = $this->validator->validate($event); + if (count($errors) > 0) { + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[$error->getPropertyPath()] = $error->getMessage(); + } + + return $this->json(['errors' => $errorMessages], Response::HTTP_BAD_REQUEST); + } + + $this->entityManager->persist($event); + $this->entityManager->flush(); + + return $this->json(EventDTO::fromEntity($event), Response::HTTP_CREATED); + } + + #[Route('/{id}', name: 'update', methods: ['PUT'])] + #[OA\Tag(name: 'Events')] + #[OA\Parameter( + name: 'id', + description: 'Event ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + )] + #[OA\RequestBody( + required: true, + content: new OA\JsonContent(ref: new Model(type: EventDTO::class)) + )] + #[OA\Response( + response: 200, + description: 'Event updated', + content: new OA\JsonContent(ref: new Model(type: EventDTO::class)) + )] + #[OA\Response( + response: 400, + description: 'Invalid input' + )] + #[OA\Response( + response: 404, + description: 'Event not found' + )] + public function update(string $id, Request $request): JsonResponse + { + $event = $this->eventRepository->find($id); + + if (!$event instanceof Event) { + return $this->json(['error' => 'Event not found'], Response::HTTP_NOT_FOUND); + } + + /** @var array|null $data */ + $data = json_decode($request->getContent(), true); + + if (!is_array($data)) { + return $this->json(['error' => 'Invalid JSON'], Response::HTTP_BAD_REQUEST); + } + + if (isset($data['title'])) { + $event->setTitle((string)$data['title']); + } + + if (array_key_exists('description', $data)) { + $event->setDescription($data['description'] !== null ? (string)$data['description'] : null); + } + + if (isset($data['start'])) { + try { + $event->setStart(new \DateTimeImmutable((string)$data['start'])); + } catch (\Exception) { + return $this->json(['error' => 'Invalid start date format'], Response::HTTP_BAD_REQUEST); + } + } + + if (isset($data['end'])) { + try { + $event->setEnd(new \DateTimeImmutable((string)$data['end'])); + } catch (\Exception) { + return $this->json(['error' => 'Invalid end date format'], Response::HTTP_BAD_REQUEST); + } + } + + if (isset($data['allDay'])) { + $event->setAllDay((bool)$data['allDay']); + } + + $errors = $this->validator->validate($event); + if (count($errors) > 0) { + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[$error->getPropertyPath()] = $error->getMessage(); + } + + return $this->json(['errors' => $errorMessages], Response::HTTP_BAD_REQUEST); + } + + $this->entityManager->flush(); + + return $this->json(EventDTO::fromEntity($event)); + } + + #[Route('/{id}', name: 'delete', methods: ['DELETE'])] + #[OA\Tag(name: 'Events')] + #[OA\Parameter( + name: 'id', + description: 'Event ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + )] + #[OA\Response( + response: 204, + description: 'Event deleted' + )] + #[OA\Response( + response: 404, + description: 'Event not found' + )] + public function delete(string $id): JsonResponse + { + $event = $this->eventRepository->find($id); + + if (!$event instanceof Event) { + return $this->json(['error' => 'Event not found'], Response::HTTP_NOT_FOUND); + } + + $this->entityManager->remove($event); + $this->entityManager->flush(); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } +} \ No newline at end of file diff --git a/backend/src/Controller/PingController.php b/backend/src/Application/Controller/PingController.php similarity index 96% rename from backend/src/Controller/PingController.php rename to backend/src/Application/Controller/PingController.php index 1e16b92..6ace4d5 100644 --- a/backend/src/Controller/PingController.php +++ b/backend/src/Application/Controller/PingController.php @@ -1,6 +1,6 @@ userRepository->findOneBy([]); + + if (!$user) { + throw new NotFoundHttpException('No user found'); + } + + return $this->json(UserDTO::fromEntity($user)); + } +} \ No newline at end of file diff --git a/backend/src/Application/DTO/CalendarDTO.php b/backend/src/Application/DTO/CalendarDTO.php new file mode 100644 index 0000000..6be13bc --- /dev/null +++ b/backend/src/Application/DTO/CalendarDTO.php @@ -0,0 +1,28 @@ + $events + */ + public function __construct( + #[OA\Property(type: 'array', items: new OA\Items(ref: "#/components/schemas/EventDTO"))] + public array $events = [] + ) { + } + + public function addEvent(EventDTO $event): self + { + $events = $this->events; + $events[] = $event; + + return new self($events); + } +} \ No newline at end of file diff --git a/backend/src/Application/DTO/EventDTO.php b/backend/src/Application/DTO/EventDTO.php new file mode 100644 index 0000000..ba6bc08 --- /dev/null +++ b/backend/src/Application/DTO/EventDTO.php @@ -0,0 +1,40 @@ +getId(), + $event->getTitle(), + $event->getDescription() ?? '', + $event->getFrom()->format('Y-m-d H:i:s'), + $event->getTo()->format('Y-m-d H:i:s'), + $event->isAllDay() + ); + } +} \ No newline at end of file diff --git a/backend/src/Application/DTO/UserDTO.php b/backend/src/Application/DTO/UserDTO.php new file mode 100644 index 0000000..9458a05 --- /dev/null +++ b/backend/src/Application/DTO/UserDTO.php @@ -0,0 +1,37 @@ +getId(), + $user->getEmail(), + $user->getFirstName(), + $user->getLastName(), + $user->getFullName() + ); + } +} \ No newline at end of file diff --git a/backend/src/Domain/Model/Event.php b/backend/src/Domain/Model/Event.php new file mode 100644 index 0000000..85f3de5 --- /dev/null +++ b/backend/src/Domain/Model/Event.php @@ -0,0 +1,131 @@ +id = Uuid::v4()->toRfc4122(); + } + + public function getId(): string + { + return $this->id; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): self + { + $this->description = $description; + + return $this; + } + + public function getStart(): \DateTimeImmutable + { + return $this->from; + } + + public function setStart(\DateTimeImmutable $start): self + { + $this->from = $start; + + return $this; + } + + public function getFrom(): \DateTimeImmutable + { + return $this->from; + } + + public function setFrom(\DateTimeImmutable $from): self + { + $this->from = $from; + + return $this; + } + + public function getEnd(): \DateTimeImmutable + { + return $this->to; + } + + public function setEnd(\DateTimeImmutable $end): self + { + $this->to = $end; + + return $this; + } + + public function getTo(): \DateTimeImmutable + { + return $this->to; + } + + public function setTo(\DateTimeImmutable $to): self + { + $this->to = $to; + + return $this; + } + + public function isAllDay(): bool + { + return $this->allDay; + } + + public function setAllDay(bool $allDay): self + { + $this->allDay = $allDay; + + return $this; + } +} \ No newline at end of file diff --git a/backend/src/Domain/Model/User.php b/backend/src/Domain/Model/User.php new file mode 100644 index 0000000..e422e43 --- /dev/null +++ b/backend/src/Domain/Model/User.php @@ -0,0 +1,81 @@ +id = Uuid::v4()->toRfc4122(); + } + + public function getId(): string + { + return $this->id; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } + + public function getFirstName(): string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): self + { + $this->firstName = $firstName; + + return $this; + } + + public function getLastName(): string + { + return $this->lastName; + } + + public function setLastName(string $lastName): self + { + $this->lastName = $lastName; + + return $this; + } + + public function getFullName(): string + { + return "$this->firstName $this->lastName"; + } +} \ No newline at end of file diff --git a/backend/src/Infrastructure/DataFixtures/AppFixtures.php b/backend/src/Infrastructure/DataFixtures/AppFixtures.php new file mode 100644 index 0000000..20e577f --- /dev/null +++ b/backend/src/Infrastructure/DataFixtures/AppFixtures.php @@ -0,0 +1,26 @@ +persist($product); + + $manager->flush(); + } + + public function getDependencies(): array + { + return [ + UserFixtures::class, + EventFixtures::class, + ]; + } +} diff --git a/backend/src/Infrastructure/DataFixtures/EventFixtures.php b/backend/src/Infrastructure/DataFixtures/EventFixtures.php new file mode 100644 index 0000000..cc9bc4c --- /dev/null +++ b/backend/src/Infrastructure/DataFixtures/EventFixtures.php @@ -0,0 +1,266 @@ +createDoctorAppointment($manager); + $this->createDentistAppointment($manager); + $this->createYogaClass($manager); + $this->createGymSession($manager); + + // Social events + $this->createDinnerWithFriends($manager); + $this->createMovieNight($manager); + $this->createBirthdayParty($manager); + + // Travel and trips + $this->createWeekendGetaway($manager); + $this->createVacation($manager); + + // Hobbies and interests + $this->createCookingClass($manager); + $this->createBookClub($manager); + $this->createHiking($manager); + + // Recurring events + $this->createWeeklyGroceryShopping($manager); + $this->createMonthlyCleaning($manager); + + // Special occasions + $this->createAnniversary($manager); + $this->createConcert($manager); + + $manager->flush(); + } + + private function createDoctorAppointment(ObjectManager $manager): void + { + $event = new Event(); + $event->setTitle('Doctor Appointment') + ->setDescription('Annual checkup with Dr. Smith') + ->setFrom(new DateTimeImmutable('next monday 09:30')) + ->setTo(new DateTimeImmutable('next monday 10:30')) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createDentistAppointment(ObjectManager $manager): void + { + $event = new Event(); + $event->setTitle('Dentist Appointment') + ->setDescription('Teeth cleaning with Dr. Johnson') + ->setFrom(new DateTimeImmutable('next friday 14:00')) + ->setTo(new DateTimeImmutable('next friday 15:00')) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createYogaClass(ObjectManager $manager): void + { + $tomorrow = new DateTimeImmutable('tomorrow'); + + $event = new Event(); + $event->setTitle('Yoga Class') + ->setDescription('Vinyasa flow at Peaceful Mind Studio') + ->setFrom($tomorrow->setTime(18, 0)) + ->setTo($tomorrow->setTime(19, 0)) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createGymSession(ObjectManager $manager): void + { + $dayAfterTomorrow = new DateTimeImmutable('today +2 days'); + + $event = new Event(); + $event->setTitle('Gym Workout') + ->setDescription('Strength training at Fitness Center') + ->setFrom($dayAfterTomorrow->setTime(7, 0)) + ->setTo($dayAfterTomorrow->setTime(8, 30)) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createDinnerWithFriends(ObjectManager $manager): void + { + $thisWeekend = new DateTimeImmutable('next saturday'); + + $event = new Event(); + $event->setTitle('Dinner with Friends') + ->setDescription('Dinner at Italiano Restaurant with Alex and Jamie') + ->setFrom($thisWeekend->setTime(19, 0)) + ->setTo($thisWeekend->setTime(22, 0)) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createMovieNight(ObjectManager $manager): void + { + $nextFriday = new DateTimeImmutable('next friday'); + + $event = new Event(); + $event->setTitle('Movie Night') + ->setDescription('Watching new Marvel movie at Cinema City') + ->setFrom($nextFriday->setTime(20, 0)) + ->setTo($nextFriday->setTime(23, 0)) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createBirthdayParty(ObjectManager $manager): void + { + $twoWeeksLater = new DateTimeImmutable('today +14 days'); + + $event = new Event(); + $event->setTitle('Sarah\'s Birthday Party') + ->setDescription('Birthday celebration at Rooftop Bar') + ->setFrom($twoWeeksLater->setTime(18, 0)) + ->setTo($twoWeeksLater->setTime(23, 0)) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createWeekendGetaway(ObjectManager $manager): void + { + $nextWeekend = new DateTimeImmutable('next saturday'); + $nextWeekendEnd = new DateTimeImmutable('next sunday'); + + $event = new Event(); + $event->setTitle('Weekend Getaway') + ->setDescription('Short trip to the mountains') + ->setFrom($nextWeekend) + ->setTo($nextWeekendEnd) + ->setAllDay(true); + + $manager->persist($event); + } + + private function createVacation(ObjectManager $manager): void + { + $vacationStart = new DateTimeImmutable('next month first day'); + $vacationEnd = new DateTimeImmutable('next month first day +6 days'); + + $event = new Event(); + $event->setTitle('Summer Vacation') + ->setDescription('Beach vacation in Hawaii') + ->setFrom($vacationStart) + ->setTo($vacationEnd) + ->setAllDay(true); + + $manager->persist($event); + } + + private function createCookingClass(ObjectManager $manager): void + { + $nextSaturday = new DateTimeImmutable('next saturday'); + + $event = new Event(); + $event->setTitle('Italian Cooking Class') + ->setDescription('Learn to make pasta from scratch at Culinary Center') + ->setFrom($nextSaturday->setTime(14, 0)) + ->setTo($nextSaturday->setTime(17, 0)) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createBookClub(ObjectManager $manager): void + { + $nextThursday = new DateTimeImmutable('next thursday'); + + $event = new Event(); + $event->setTitle('Book Club Meeting') + ->setDescription('Discussing "The Midnight Library" at Local Cafe') + ->setFrom($nextThursday->setTime(19, 0)) + ->setTo($nextThursday->setTime(21, 0)) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createHiking(ObjectManager $manager): void + { + $inTwoWeeks = new DateTimeImmutable('today +14 days'); + + $event = new Event(); + $event->setTitle('Hiking Trip') + ->setDescription('Explore Blue Mountain Trail') + ->setFrom($inTwoWeeks->setTime(8, 0)) + ->setTo($inTwoWeeks->setTime(16, 0)) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createWeeklyGroceryShopping(ObjectManager $manager): void + { + $nextSunday = new DateTimeImmutable('next sunday'); + + $event = new Event(); + $event->setTitle('Grocery Shopping') + ->setDescription('Weekly grocery run at Farmer\'s Market') + ->setFrom($nextSunday->setTime(10, 0)) + ->setTo($nextSunday->setTime(11, 30)) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createMonthlyCleaning(ObjectManager $manager): void + { + $firstOfNextMonth = new DateTimeImmutable('first day of next month'); + + $event = new Event(); + $event->setTitle('Monthly Deep Cleaning') + ->setDescription('House deep cleaning day') + ->setFrom($firstOfNextMonth) + ->setTo($firstOfNextMonth) + ->setAllDay(true); + + $manager->persist($event); + } + + private function createAnniversary(ObjectManager $manager): void + { + $twoMonthsFromNow = new DateTimeImmutable('today +2 months'); + + $event = new Event(); + $event->setTitle('Relationship Anniversary') + ->setDescription('Dinner reservation at Sunset Restaurant') + ->setFrom($twoMonthsFromNow->setTime(19, 0)) + ->setTo($twoMonthsFromNow->setTime(22, 0)) + ->setAllDay(false); + + $manager->persist($event); + } + + private function createConcert(ObjectManager $manager): void + { + $nextMonth = new DateTimeImmutable('next month'); + + $event = new Event(); + $event->setTitle('Live Concert') + ->setDescription('Favorite band performing at Central Arena') + ->setFrom($nextMonth->setTime(20, 0)) + ->setTo($nextMonth->setTime(23, 0)) + ->setAllDay(false); + + $manager->persist($event); + } +} \ No newline at end of file diff --git a/backend/src/Infrastructure/DataFixtures/UserFixtures.php b/backend/src/Infrastructure/DataFixtures/UserFixtures.php new file mode 100644 index 0000000..e6ec4cd --- /dev/null +++ b/backend/src/Infrastructure/DataFixtures/UserFixtures.php @@ -0,0 +1,25 @@ +setEmail('user@example.com') + ->setFirstName('John') + ->setLastName('Doe'); + + $manager->persist($user); + $manager->flush(); + + $this->addReference(self::DEFAULT_USER_REFERENCE, $user); + } +} \ No newline at end of file diff --git a/backend/src/Infrastructure/Repository/EventRepository.php b/backend/src/Infrastructure/Repository/EventRepository.php new file mode 100644 index 0000000..67357ba --- /dev/null +++ b/backend/src/Infrastructure/Repository/EventRepository.php @@ -0,0 +1,37 @@ + + */ +class EventRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Event::class); + } + + /** + * @return array + */ + public function findByDateRange(\DateTimeInterface $start, \DateTimeInterface $end): array + { + /** @var array $result */ + $result = $this->createQueryBuilder('e') + ->andWhere('e.from >= :start AND e.from <= :end') + ->orWhere('e.to >= :start AND e.to <= :end') + ->orWhere('e.from <= :start AND e.to >= :end') + ->setParameter('start', $start) + ->setParameter('end', $end) + ->orderBy('e.from', 'ASC') + ->getQuery() + ->getResult(); + + return $result; + } +} \ No newline at end of file diff --git a/backend/src/Infrastructure/Repository/UserRepository.php b/backend/src/Infrastructure/Repository/UserRepository.php new file mode 100644 index 0000000..4b33bb5 --- /dev/null +++ b/backend/src/Infrastructure/Repository/UserRepository.php @@ -0,0 +1,30 @@ + + */ +class UserRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + public function save(User $user): void + { + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } + + public function remove(User $user): void + { + $this->getEntityManager()->remove($user); + $this->getEntityManager()->flush(); + } +} \ No newline at end of file diff --git a/backend/symfony.lock b/backend/symfony.lock index 6975890..ae277e7 100644 --- a/backend/symfony.lock +++ b/backend/symfony.lock @@ -1,4 +1,52 @@ { + "doctrine/annotations": { + "version": "2.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.10", + "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" + } + }, + "doctrine/doctrine-bundle": { + "version": "2.14", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.10", + "ref": "c170ded8fc587d6bd670550c43dafcf093762245" + }, + "files": [ + "config/packages/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-fixtures-bundle": { + "version": "4.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "1f5514cfa15b947298df4d771e694e578d4c204d" + }, + "files": [ + "src/DataFixtures/AppFixtures.php" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "3.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.1", + "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" + }, + "files": [ + "config/packages/doctrine_migrations.yaml", + "migrations/.gitignore" + ] + }, "nelmio/api-doc-bundle": { "version": "5.0", "recipe": { @@ -8,6 +56,18 @@ "ref": "c8e0c38e1a280ab9e37587a8fa32b251d5bc1c94" } }, + "phpstan/phpstan": { + "version": "2.1", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767" + }, + "files": [ + "phpstan.dist.neon" + ] + }, "symfony/console": { "version": "6.4", "recipe": { @@ -52,6 +112,15 @@ "src/Kernel.php" ] }, + "symfony/maker-bundle": { + "version": "1.62", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, "symfony/routing": { "version": "6.4", "recipe": { @@ -77,5 +146,29 @@ "config/packages/twig.yaml", "templates/base.html.twig" ] + }, + "symfony/uid": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.2", + "ref": "d294ad4add3e15d7eb1bae0221588ca89b38e558" + }, + "files": [ + "config/packages/uid.yaml" + ] + }, + "symfony/validator": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c" + }, + "files": [ + "config/packages/validator.yaml" + ] } } diff --git a/docker/Dockerfile b/docker/Dockerfile index bb8b36d..e517535 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,7 +29,8 @@ RUN apt-get update && apt-get install -y \ php8.4-bcmath \ php8.4-intl \ php8.4-gd \ - php8.4-fpm + php8.4-fpm \ + php8.4-pgsql # Install Composer RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer diff --git a/frontend/src/App.css b/frontend/src/App.css index 7cd2d48..76a7d83 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,5 +1,8 @@ .App { - text-align: center; + width: 100%; + max-width: 100%; + overflow-x: hidden; + box-sizing: border-box; } .App-logo { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 668476c..b427820 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,14 +1,14 @@ -import React from 'react'; import './App.css'; import { TabView } from './components/navigation/TabView'; import Home from './pages/home/Home'; +import Profile from './pages/profile/Profile'; import { faCalendar, faUser } from '@fortawesome/free-solid-svg-icons'; import { faHome } from '@fortawesome/free-solid-svg-icons'; const tabs = [ { id: 'home', label: 'Home', component: Home, icon: faHome }, { id: 'calendar', label: 'Calendar', component: Home, icon: faCalendar }, - { id: 'profile', label: 'Profile', component: Home, icon: faUser } + { id: 'profile', label: 'Profile', component: Profile, icon: faUser } ]; function App() { diff --git a/frontend/src/components/navigation/TabView.css b/frontend/src/components/navigation/TabView.css index 22ee57d..5cfbee1 100644 --- a/frontend/src/components/navigation/TabView.css +++ b/frontend/src/components/navigation/TabView.css @@ -3,10 +3,13 @@ flex-direction: column; height: 100vh; width: 100%; + max-width: 100%; position: fixed; top: 0; left: 0; background-color: #f8fafc; + overflow-x: hidden; + box-sizing: border-box; } .tab-bar { @@ -60,13 +63,16 @@ .tab-content { flex: 1; - overflow: auto; - padding: 20px; + overflow-y: auto; + overflow-x: hidden; display: flex; flex-direction: column; - justify-content: center; - align-items: center; + justify-content: start; + align-items: start; background-color: #ffffff; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); margin-bottom: -20px; + width: 100%; + max-width: 100%; + box-sizing: border-box; } \ No newline at end of file diff --git a/frontend/src/components/ui/Calendar/Calendar.module.css b/frontend/src/components/ui/Calendar/Calendar.module.css new file mode 100644 index 0000000..a429765 --- /dev/null +++ b/frontend/src/components/ui/Calendar/Calendar.module.css @@ -0,0 +1,200 @@ +.calendar { + background-color: #fff; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + overflow: hidden; + width: 100%; + max-width: 900px; + margin: 0 auto; +} + +.calendarContainer { + display: flex; + flex-direction: column; + + @media (min-width: 768px) { + flex-direction: row; + } +} + +/* Header styles */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid #f0f0f0; + background-color: #fff; +} + +.headerTitle { + font-size: 20px; + font-weight: 600; + margin: 0; + color: #333; +} + +.navButton { + background-color: transparent; + border: none; + border-radius: 50%; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #666; + transition: background-color 0.2s, color 0.2s; +} + +.navButton:hover { + background-color: #f5f5f5; + color: #333; +} + +.navButton:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3); +} + +/* Calendar grid styles */ +.calendarGrid { + display: grid; + grid-template-columns: repeat(7, 1fr); + padding: 0 1rem 1rem; + flex: 2; +} + +.weekDay { + padding: 12px 0; + text-align: center; + font-size: 13px; + font-weight: 600; + color: #777; + border-bottom: 1px solid #f0f0f0; +} + +.day, .emptyDay { + aspect-ratio: 1/1; + padding: 4px; + position: relative; + cursor: pointer; + transition: background-color 0.2s; +} + +.day:hover { + background-color: #f5f5f5; +} + +.dayNumber { + position: absolute; + top: 4px; + left: 6px; + font-size: 14px; + font-weight: 500; + color: #444; + height: 24px; + width: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.today .dayNumber { + background-color: #4285f4; + color: white; +} + +.selectedDay { + background-color: rgba(66, 133, 244, 0.08); +} + +.selectedDay .dayNumber { + font-weight: 700; +} + +.eventIndicators { + position: absolute; + bottom: 8px; + left: 0; + right: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.eventIndicator { + width: 80%; + height: 4px; + border-radius: 2px; +} + +.moreEvents { + font-size: 10px; + color: #777; + margin-top: 2px; +} + +/* Events list styles */ +.eventsList { + padding: 16px; + border-left: 1px solid #f0f0f0; + flex: 1; + max-height: 460px; + overflow-y: auto; + + @media (max-width: 767px) { + border-left: none; + border-top: 1px solid #f0f0f0; + } +} + +.eventsListTitle { + font-size: 16px; + font-weight: 600; + margin: 0 0 16px; + color: #333; +} + +.eventsListEmpty { + padding: 20px; + text-align: center; + color: #777; + font-style: italic; +} + +.eventsContainer { + display: flex; + flex-direction: column; + gap: 8px; +} + +.event { + padding: 12px; + border-radius: 8px; + background-color: #f8f9fa; + border-left: 4px solid #4285f4; + cursor: pointer; + transition: transform 0.1s, box-shadow 0.2s; +} + +.event:hover { + transform: translateY(-2px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.eventTime { + font-size: 12px; + font-weight: 500; + color: #666; + margin-bottom: 4px; +} + +.eventTitle { + font-size: 14px; + font-weight: 500; + color: #333; +} \ No newline at end of file diff --git a/frontend/src/components/ui/Calendar/CalendarDemo.tsx b/frontend/src/components/ui/Calendar/CalendarDemo.tsx new file mode 100644 index 0000000..82d6cb5 --- /dev/null +++ b/frontend/src/components/ui/Calendar/CalendarDemo.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import { Calendar, CalendarEvent } from './index'; +import { useApi } from '../../../lib/api/useApi'; +import { getEvents } from '../../../lib/api/endpoints'; + +export const CalendarDemo = () => { + const [selectedEvent, setSelectedEvent] = useState(null); + const [fetchEvents, { data: events, loading, error }] = useApi(getEvents, []); + + useEffect(() => { + fetchEvents(); + + }, [fetchEvents]); + + if (loading) { + return
Loading...
; + } + + if (error) { + return
Error: {error}
; + } + + const calendarEvents: CalendarEvent[] = events ? events.map(event => ({ + id: event.id, + title: event.title, + date: new Date(event.start), + color: event.allDay ? '#4285F4' : '#34A853' + })) : []; + + console.log(calendarEvents); + + const handleDateClick = (date: Date) => { + console.log('Date clicked:', date); + }; + + const handleEventClick = (event: CalendarEvent) => { + setSelectedEvent(event); + }; + + return ( +
+ +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/ui/Calendar/CalendarEventsList.tsx b/frontend/src/components/ui/Calendar/CalendarEventsList.tsx new file mode 100644 index 0000000..3ab9de7 --- /dev/null +++ b/frontend/src/components/ui/Calendar/CalendarEventsList.tsx @@ -0,0 +1,58 @@ +import { CalendarEvent } from './index'; +import styles from './Calendar.module.css'; + +export type CalendarEventsListProps = { + events: CalendarEvent[]; + date: Date; + onEventClick?: (event: CalendarEvent) => void; +}; + +export const CalendarEventsList = ({ + events, + date, + onEventClick +}: CalendarEventsListProps) => { + if (events.length === 0) { + return ( +
+

No events scheduled for this day

+
+ ); + } + + const handleEventClick = (event: CalendarEvent) => { + if (onEventClick) { + onEventClick(event); + } + }; + + const formatTime = (date: Date) => { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + }; + + return ( +
+

+ Events for {date.toLocaleDateString([], { month: 'long', day: 'numeric' })} +

+ +
+ {events.map(event => ( +
handleEventClick(event)} + style={{ borderLeftColor: event.color || '#4285f4' }} + > +
+ {formatTime(event.date)} +
+
+ {event.title} +
+
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/ui/Calendar/CalendarGrid.tsx b/frontend/src/components/ui/Calendar/CalendarGrid.tsx new file mode 100644 index 0000000..77f9aac --- /dev/null +++ b/frontend/src/components/ui/Calendar/CalendarGrid.tsx @@ -0,0 +1,97 @@ +import { formatDate } from '../../../lib/date'; +import { CalendarEvent } from './index'; +import styles from './Calendar.module.css'; + +export type CalendarGridProps = { + daysInMonth: number[]; + firstDay: number; + currentDate: Date; + selectedDate: Date | null; + events: CalendarEvent[]; + onDateClick: (day: number) => void; +}; + +export const CalendarGrid = ({ + daysInMonth, + firstDay, + currentDate, + selectedDate, + events, + onDateClick +}: CalendarGridProps) => { + const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const today = new Date(); + const currentMonth = currentDate.getMonth(); + const currentYear = currentDate.getFullYear(); + + const isCurrentMonth = + today.getMonth() === currentMonth && + today.getFullYear() === currentYear; + + const todayDate = today.getDate(); + + const getEventsByDay = (day: number) => { + const date = new Date(currentYear, currentMonth, day); + const formattedDate = formatDate(date); + + return events.filter(event => formatDate(event.date) === formattedDate); + }; + + const renderDay = (day: number) => { + const date = new Date(currentYear, currentMonth, day); + const isSelected = selectedDate && formatDate(date) === formatDate(selectedDate); + const isToday = isCurrentMonth && day === todayDate; + const dayEvents = getEventsByDay(day); + + return ( +
onDateClick(day)} + > + {day} + + {dayEvents.length > 0 && ( +
+ {dayEvents.slice(0, 3).map((event, index) => ( +
+ ))} + + {dayEvents.length > 3 && ( +
+ +{dayEvents.length - 3} +
+ )} +
+ )} +
+ ); + }; + + const renderEmptyDay = (index: number) => ( +
+ ); + + return ( +
+ {weekDays.map(day => ( +
+ {day} +
+ ))} + + {Array(firstDay).fill(null).map((_, index) => renderEmptyDay(index))} + + {daysInMonth.map(day => renderDay(day))} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/ui/Calendar/CalendarHeader.tsx b/frontend/src/components/ui/Calendar/CalendarHeader.tsx new file mode 100644 index 0000000..1cf15ce --- /dev/null +++ b/frontend/src/components/ui/Calendar/CalendarHeader.tsx @@ -0,0 +1,47 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import styles from './Calendar.module.css'; + +export type CalendarHeaderProps = { + currentDate: Date; + onPrevMonth: () => void; + onNextMonth: () => void; +}; + +export const CalendarHeader = ({ + currentDate, + onPrevMonth, + onNextMonth +}: CalendarHeaderProps) => { + const monthNames = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + + const month = monthNames[currentDate.getMonth()]; + const year = currentDate.getFullYear(); + + return ( +
+ + +

+ {month} {year} +

+ + +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/ui/Calendar/index.tsx b/frontend/src/components/ui/Calendar/index.tsx new file mode 100644 index 0000000..9b97c77 --- /dev/null +++ b/frontend/src/components/ui/Calendar/index.tsx @@ -0,0 +1,99 @@ +import { useState, useEffect } from 'react'; +import { CalendarHeader } from './CalendarHeader'; +import { CalendarGrid } from './CalendarGrid'; +import { CalendarEventsList } from './CalendarEventsList'; +import { getDaysInMonth, getFirstDayOfMonth, formatDate } from '../../../lib/date'; +import styles from './Calendar.module.css'; + +export type CalendarEvent = { + id: string; + title: string; + date: Date; + color?: string; +}; + +export type CalendarProps = { + events?: CalendarEvent[]; + onDateClick?: (date: Date) => void; + onEventClick?: (event: CalendarEvent) => void; + initialDate?: Date; +}; + +export const Calendar = ({ + events = [], + onDateClick, + onEventClick, + initialDate = new Date() +}: CalendarProps) => { + const [currentDate, setCurrentDate] = useState(initialDate); + const [selectedDate, setSelectedDate] = useState(null); + const [daysInMonth, setDaysInMonth] = useState([]); + const [firstDay, setFirstDay] = useState(0); + + useEffect(() => { + const days = getDaysInMonth(currentDate.getFullYear(), currentDate.getMonth()); + const firstDayOfMonth = getFirstDayOfMonth(currentDate.getFullYear(), currentDate.getMonth()); + + setDaysInMonth(Array.from({ length: days }, (_, i) => i + 1)); + setFirstDay(firstDayOfMonth); + }, [currentDate]); + + const handlePrevMonth = () => { + setCurrentDate(prevDate => { + const newDate = new Date(prevDate); + newDate.setMonth(newDate.getMonth() - 1); + return newDate; + }); + }; + + const handleNextMonth = () => { + setCurrentDate(prevDate => { + const newDate = new Date(prevDate); + newDate.setMonth(newDate.getMonth() + 1); + return newDate; + }); + }; + + const handleDateClick = (day: number) => { + const newDate = new Date(currentDate); + newDate.setDate(day); + setSelectedDate(newDate); + + if (onDateClick) { + onDateClick(newDate); + } + }; + + const filteredEvents = selectedDate + ? events.filter(event => formatDate(event.date) === formatDate(selectedDate)) + : []; + + return ( +
+ + +
+ + + {selectedDate && ( + + )} +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index ec2585e..4ee0b61 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,13 @@ +* { + box-sizing: border-box; +} + +html, body { + width: 100%; + max-width: 100%; + overflow-x: hidden; +} + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 068d67d..f4ce9aa 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -4,13 +4,16 @@ import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import './components/ui/FontAwesomeIcons'; +import { UserProvider } from './lib/context'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( - + + + ); diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts new file mode 100644 index 0000000..de2e8af --- /dev/null +++ b/frontend/src/lib/api/client.ts @@ -0,0 +1,101 @@ +export type ApiResponse = { + data: T; + success: boolean; + error?: string; +}; + +export type RequestOptions = { + params?: Record; + headers?: Record; +}; + +const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:9010'; + +export class ApiError extends Error { + status: number; + + constructor(message: string, status: number) { + super(message); + this.status = status; + this.name = 'ApiError'; + } +} + +export async function get(endpoint: string, options?: RequestOptions): Promise { + const url = buildUrl(endpoint, options?.params); + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + }); + + return handleResponse(response); +} + +export async function post(endpoint: string, data: unknown, options?: RequestOptions): Promise { + const url = buildUrl(endpoint, options?.params); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...options?.headers + }, + body: JSON.stringify(data) + }); + + return handleResponse(response); +} + +export async function put(endpoint: string, data: unknown, options?: RequestOptions): Promise { + const url = buildUrl(endpoint, options?.params); + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...options?.headers + }, + body: JSON.stringify(data) + }); + + return handleResponse(response); +} + +export async function del(endpoint: string, options?: RequestOptions): Promise { + const url = buildUrl(endpoint, options?.params); + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + }); + + return handleResponse(response); +} + +function buildUrl(endpoint: string, params?: Record): string { + const url = new URL(`${API_BASE_URL}${endpoint}`); + + if (params) { + Object.entries(params).forEach(([key, value]) => { + url.searchParams.append(key, String(value)); + }); + } + + return url.toString(); +} + +async function handleResponse(response: Response): Promise { + if (!response.ok) { + const errorText = await response.text(); + throw new ApiError(errorText || `Request failed with status ${response.status}`, response.status); + } + + try { + return await response.json() as T; + } catch (error) { + throw new ApiError('Failed to parse response', 500); + } +} \ No newline at end of file diff --git a/frontend/src/lib/api/endpoints.ts b/frontend/src/lib/api/endpoints.ts new file mode 100644 index 0000000..59d5a92 --- /dev/null +++ b/frontend/src/lib/api/endpoints.ts @@ -0,0 +1,65 @@ +import { get, post, put, del } from './client'; + +// Define your data types here +export type User = { + id: number; + name: string; + email: string; +}; + +export type CreateUserRequest = { + name: string; + email: string; + password: string; +}; + +// User endpoints +export const getUser = () => get('/api/user'); + +// Event types +export type Event = { + id: string; + title: string; + description: string; + start: string; + end: string; + allDay: boolean; +}; + +export type CreateEventRequest = { + title: string; + description?: string; + start: string; + end: string; + allDay?: boolean; +}; + +// Event endpoints +export const getEvents = () => get('/api/events'); +export const getEvent = (id: string) => get(`/api/events/${id}`); +export const createEvent = (data: CreateEventRequest) => post('/api/events', data); +export const updateEvent = (id: string, data: Partial) => put(`/api/events/${id}`, data); +export const deleteEvent = (id: string) => del(`/api/events/${id}`); + +// Calendar types +export type Calendar = { + events: Event[]; +}; + +// Calendar endpoints +export const getCalendar = (start?: string, end?: string) => { + const params = new URLSearchParams(); + if (start) params.append('start', start); + if (end) params.append('end', end); + + const queryString = params.toString(); + return get(`/api/calendar${queryString ? `?${queryString}` : ''}`); +}; + +// Health check endpoint +export type PingResponse = { + status: string; + timestamp: number; +}; + +export const ping = () => get('/api/ping'); \ No newline at end of file diff --git a/frontend/src/lib/api/index.ts b/frontend/src/lib/api/index.ts new file mode 100644 index 0000000..20bfd85 --- /dev/null +++ b/frontend/src/lib/api/index.ts @@ -0,0 +1,3 @@ +export * from './client'; +export * from './endpoints'; +export * from './useApi'; \ No newline at end of file diff --git a/frontend/src/lib/api/useApi.ts b/frontend/src/lib/api/useApi.ts new file mode 100644 index 0000000..4b0f6bf --- /dev/null +++ b/frontend/src/lib/api/useApi.ts @@ -0,0 +1,67 @@ +import { useState, useCallback } from 'react'; +import { ApiError } from './client'; + +export type ApiState = { + data: T | null; + loading: boolean; + error: string | null; +}; + +export type ApiHook = [ + (...args: P) => Promise, + ApiState +]; + +export function useApi( + apiFunction: (...args: P) => Promise, + initialData: T | null = null +): ApiHook { + const [state, setState] = useState>({ + data: initialData, + loading: false, + error: null, + }); + + const execute = useCallback( + async (...args: P) => { + setState((prev) => ({ ...prev, loading: true, error: null })); + + try { + const data = await apiFunction(...args); + setState({ data, loading: false, error: null }); + } catch (err) { + const error = err instanceof ApiError + ? err.message + : 'An unexpected error occurred'; + + console.error(err); + + setState((prev) => ({ ...prev, loading: false, error })); + } + }, + [apiFunction] + ); + + return [execute, state]; +} + +// Example usage: +// +// function UserList() { +// const [fetchUsers, { data: users, loading, error }] = useApi(getUsers, []); +// +// useEffect(() => { +// fetchUsers(); +// }, [fetchUsers]); +// +// if (loading) return
Loading...
; +// if (error) return
Error: {error}
; +// +// return ( +//
    +// {users?.map(user => ( +//
  • {user.name}
  • +// ))} +//
+// ); +// } \ No newline at end of file diff --git a/frontend/src/lib/calendar/CalendarService.ts b/frontend/src/lib/calendar/CalendarService.ts new file mode 100644 index 0000000..56e23c2 --- /dev/null +++ b/frontend/src/lib/calendar/CalendarService.ts @@ -0,0 +1,140 @@ +import { CalendarEvent, RecurrenceRule } from '../models/CalendarEvent'; + +export class CalendarService { + static getEventsForMonth(events: CalendarEvent[], year: number, month: number): CalendarEvent[] { + const startOfMonth = new Date(year, month, 1); + const endOfMonth = new Date(year, month + 1, 0); + + return events.filter(event => + (event.start <= endOfMonth && event.end >= startOfMonth) || + this.isRecurringInRange(event, startOfMonth, endOfMonth) + ); + } + + static getEventsForDay(events: CalendarEvent[], date: Date): CalendarEvent[] { + const dayStart = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + const dayEnd = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59); + + return events.filter(event => + (event.start <= dayEnd && event.end >= dayStart) || + this.isRecurringInRange(event, dayStart, dayEnd) + ); + } + + static getEventsForWeek(events: CalendarEvent[], date: Date): CalendarEvent[] { + const dayOfWeek = date.getDay(); + const weekStart = new Date(date); + weekStart.setDate(date.getDate() - dayOfWeek); + weekStart.setHours(0, 0, 0, 0); + + const weekEnd = new Date(weekStart); + weekEnd.setDate(weekStart.getDate() + 6); + weekEnd.setHours(23, 59, 59, 999); + + return events.filter(event => + (event.start <= weekEnd && event.end >= weekStart) || + this.isRecurringInRange(event, weekStart, weekEnd) + ); + } + + static isRecurringInRange(event: CalendarEvent, rangeStart: Date, rangeEnd: Date): boolean { + if (!event.recurrence) { + return false; + } + + if (event.recurrence.until && event.recurrence.until < rangeStart) { + return false; + } + + // Simplified recurrence check - a complete implementation would be more complex + switch (event.recurrence.frequency) { + case 'daily': + return this.checkDailyRecurrence(event, rangeStart, rangeEnd); + case 'weekly': + return this.checkWeeklyRecurrence(event, rangeStart, rangeEnd); + case 'monthly': + return this.checkMonthlyRecurrence(event, rangeStart, rangeEnd); + case 'yearly': + return this.checkYearlyRecurrence(event, rangeStart, rangeEnd); + default: + return false; + } + } + + private static checkDailyRecurrence(event: CalendarEvent, rangeStart: Date, rangeEnd: Date): boolean { + const interval = event.recurrence?.interval || 1; + const eventDuration = event.end.getTime() - event.start.getTime(); + + // Check if any occurrence falls within range + let currentDate = new Date(event.start); + const rangeEndTime = rangeEnd.getTime(); + + while (currentDate.getTime() <= rangeEndTime) { + if (currentDate.getTime() + eventDuration >= rangeStart.getTime()) { + return true; + } + + currentDate.setDate(currentDate.getDate() + interval); + } + + return false; + } + + private static checkWeeklyRecurrence(event: CalendarEvent, rangeStart: Date, rangeEnd: Date): boolean { + const interval = event.recurrence?.interval || 1; + const eventDuration = event.end.getTime() - event.start.getTime(); + + // Check if any occurrence falls within range + let currentDate = new Date(event.start); + const rangeEndTime = rangeEnd.getTime(); + + while (currentDate.getTime() <= rangeEndTime) { + if (currentDate.getTime() + eventDuration >= rangeStart.getTime()) { + return true; + } + + currentDate.setDate(currentDate.getDate() + (interval * 7)); + } + + return false; + } + + private static checkMonthlyRecurrence(event: CalendarEvent, rangeStart: Date, rangeEnd: Date): boolean { + const interval = event.recurrence?.interval || 1; + const eventDay = event.start.getDate(); + + // Simple check - a complete implementation would handle more complex patterns + for (let year = rangeStart.getFullYear(); year <= rangeEnd.getFullYear(); year++) { + const startMonth = year === rangeStart.getFullYear() ? rangeStart.getMonth() : 0; + const endMonth = year === rangeEnd.getFullYear() ? rangeEnd.getMonth() : 11; + + for (let month = startMonth; month <= endMonth; month++) { + if ((month - event.start.getMonth()) % interval === 0) { + const checkDate = new Date(year, month, eventDay); + if (checkDate >= rangeStart && checkDate <= rangeEnd) { + return true; + } + } + } + } + + return false; + } + + private static checkYearlyRecurrence(event: CalendarEvent, rangeStart: Date, rangeEnd: Date): boolean { + const interval = event.recurrence?.interval || 1; + const eventMonth = event.start.getMonth(); + const eventDay = event.start.getDate(); + + for (let year = rangeStart.getFullYear(); year <= rangeEnd.getFullYear(); year++) { + if ((year - event.start.getFullYear()) % interval === 0) { + const checkDate = new Date(year, eventMonth, eventDay); + if (checkDate >= rangeStart && checkDate <= rangeEnd) { + return true; + } + } + } + + return false; + } +} \ No newline at end of file diff --git a/frontend/src/lib/context/UserContext.tsx b/frontend/src/lib/context/UserContext.tsx new file mode 100644 index 0000000..36463e9 --- /dev/null +++ b/frontend/src/lib/context/UserContext.tsx @@ -0,0 +1,57 @@ +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { User, getUser } from '../api/endpoints'; + +type UserContextType = { + user: User | null; + loading: boolean; + error: Error | null; + refetch: () => Promise; +}; + +const UserContext = createContext(undefined); + +export const UserProvider = ({ children }: { children: ReactNode }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchUser = async () => { + try { + setLoading(true); + setError(null); + const userData = await getUser(); + setUser(userData); + } catch (err) { + setError(err instanceof Error ? err : new Error('Failed to fetch user')); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchUser(); + }, []); + + return ( + + {children} + + ); +}; + +export const useUser = (): UserContextType => { + const context = useContext(UserContext); + + if (context === undefined) { + throw new Error('useUser must be used within a UserProvider'); + } + + return context; +}; \ No newline at end of file diff --git a/frontend/src/lib/context/index.ts b/frontend/src/lib/context/index.ts new file mode 100644 index 0000000..9bd9eeb --- /dev/null +++ b/frontend/src/lib/context/index.ts @@ -0,0 +1 @@ +export * from './UserContext'; \ No newline at end of file diff --git a/frontend/src/lib/date.ts b/frontend/src/lib/date.ts new file mode 100644 index 0000000..e077093 --- /dev/null +++ b/frontend/src/lib/date.ts @@ -0,0 +1,69 @@ +/** + * Get the number of days in a month + */ +export const getDaysInMonth = (year: number, month: number): number => { + return new Date(year, month + 1, 0).getDate(); +}; + +/** + * Get the day of the week for the first day of a month (0 = Sunday, 6 = Saturday) + */ +export const getFirstDayOfMonth = (year: number, month: number): number => { + return new Date(year, month, 1).getDay(); +}; + +/** + * Format a date as YYYY-MM-DD for comparison + */ +export const formatDate = (date: Date): string => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +}; + +/** + * Get an array of dates for the current week containing the given date + */ +export const getDatesForWeek = (date: Date): Date[] => { + const day = date.getDay(); + const diff = date.getDate() - day; + + return Array(7) + .fill(null) + .map((_, index) => { + const newDate = new Date(date); + newDate.setDate(diff + index); + return newDate; + }); +}; + +/** + * Get the list of dates for a given month + */ +export const getDatesForMonth = (year: number, month: number): Date[] => { + const daysInMonth = getDaysInMonth(year, month); + + return Array(daysInMonth) + .fill(null) + .map((_, index) => new Date(year, month, index + 1)); +}; + +/** + * Check if two dates are the same day + */ +export const isSameDay = (date1: Date, date2: Date): boolean => { + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ); +}; + +/** + * Check if a date is today + */ +export const isToday = (date: Date): boolean => { + return isSameDay(date, new Date()); +}; \ No newline at end of file diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts new file mode 100644 index 0000000..287d7da --- /dev/null +++ b/frontend/src/lib/index.ts @@ -0,0 +1,11 @@ +// Models +export * from './models/CalendarEvent'; + +// Calendar utilities +export * from './calendar/CalendarService'; + +// Date utilities +export * from './utils/DateUtils'; + +// API client +export * as api from './api'; \ No newline at end of file diff --git a/frontend/src/lib/models/CalendarEvent.ts b/frontend/src/lib/models/CalendarEvent.ts new file mode 100644 index 0000000..e428344 --- /dev/null +++ b/frontend/src/lib/models/CalendarEvent.ts @@ -0,0 +1,37 @@ +export interface CalendarEvent { + id: string; + title: string; + description?: string; + start: Date; + end: Date; + allDay?: boolean; + color?: string; + location?: string; + organizer?: string; + attendees?: Attendee[]; + recurrence?: RecurrenceRule; +} + +export interface Attendee { + id: string; + name: string; + email: string; + status: 'accepted' | 'declined' | 'tentative' | 'pending'; +} + +export interface RecurrenceRule { + frequency: 'daily' | 'weekly' | 'monthly' | 'yearly'; + interval?: number; + count?: number; + until?: Date; + byDay?: string[]; + byMonth?: number[]; + byMonthDay?: number[]; + exceptions?: Date[]; +} + +export enum EventStatus { + CONFIRMED = 'confirmed', + TENTATIVE = 'tentative', + CANCELLED = 'cancelled' +} \ No newline at end of file diff --git a/frontend/src/lib/utils/DateUtils.ts b/frontend/src/lib/utils/DateUtils.ts new file mode 100644 index 0000000..1cc8b61 --- /dev/null +++ b/frontend/src/lib/utils/DateUtils.ts @@ -0,0 +1,89 @@ +export class DateUtils { + static getWeekNumber(date: Date): number { + const firstDayOfYear = new Date(date.getFullYear(), 0, 1); + const pastDaysOfYear = (date.getTime() - firstDayOfYear.getTime()) / 86400000; + return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7); + } + + static getMonthName(month: number): string { + const months = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + return months[month]; + } + + static getShortMonthName(month: number): string { + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + return months[month]; + } + + static getDayName(day: number): string { + const days = [ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', + 'Thursday', 'Friday', 'Saturday' + ]; + return days[day]; + } + + static getShortDayName(day: number): string { + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return days[day]; + } + + static areDatesEqual(date1: Date, date2: Date): boolean { + return date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate(); + } + + static areDateTimesEqual(date1: Date, date2: Date): boolean { + return date1.getTime() === date2.getTime(); + } + + static formatDate(date: Date, format: string = 'YYYY-MM-DD'): string { + const year = date.getFullYear().toString(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const seconds = date.getSeconds().toString().padStart(2, '0'); + + return format + .replace('YYYY', year) + .replace('MM', month) + .replace('DD', day) + .replace('HH', hours) + .replace('mm', minutes) + .replace('ss', seconds); + } + + static getDaysInMonth(year: number, month: number): number { + return new Date(year, month + 1, 0).getDate(); + } + + static getFirstDayOfMonth(year: number, month: number): number { + return new Date(year, month, 1).getDay(); + } + + static addDays(date: Date, days: number): Date { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; + } + + static addMonths(date: Date, months: number): Date { + const result = new Date(date); + result.setMonth(result.getMonth() + months); + return result; + } + + static addYears(date: Date, years: number): Date { + const result = new Date(date); + result.setFullYear(result.getFullYear() + years); + return result; + } +} \ No newline at end of file diff --git a/frontend/src/pages/home/Home.css b/frontend/src/pages/home/Home.css new file mode 100644 index 0000000..a9d2595 --- /dev/null +++ b/frontend/src/pages/home/Home.css @@ -0,0 +1,162 @@ +.home-container { + display: flex; + flex-direction: column; + padding: 1.25rem; + background-color: #ffffff; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + max-width: 1200px; + margin: 0 auto; + width: 100%; + box-sizing: border-box; +} + +.greeting { + font-size: 2rem; + font-weight: 700; + margin-bottom: 1.875rem; + color: #000000; +} + +.events-section { + margin-bottom: 2.5rem; +} + +.section-title { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; + color: #000000; +} + +.events-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.tomorrow-events { + display: flex; + flex-direction: row; + gap: 1rem; + padding-bottom: 0.5rem; + width: 100%; + flex-wrap: wrap; +} + +.tomorrow-events .event-item { + min-width: 200px; + width: 250px; + flex-shrink: 0; + max-width: 100%; +} + +.week-events { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; + width: 100%; +} + +.event-item { + background-color: #ec6a5e; + border-radius: 1rem; + padding: 1.125rem 1.25rem; + color: #ffffff; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + box-shadow: 0 2px 8px rgba(236, 106, 94, 0.3); + display: flex; + flex-direction: column; + min-height: 80px; +} + +.event-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(236, 106, 94, 0.4); +} + +.event-title { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 0.5rem; + word-break: break-word; +} + +.event-time { + font-size: 0.875rem; + opacity: 0.9; +} + +.no-events { + color: #888; + font-style: italic; + padding: 1rem 0; +} + +.loading, .error { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + font-size: 1.125rem; + color: #555; +} + +.error { + color: #ec6a5e; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .greeting { + font-size: 1.75rem; + margin-bottom: 1.5rem; + } + + .section-title { + font-size: 1.25rem; + } + + .events-section { + margin-bottom: 2rem; + width: 100%; + max-width: 100%; + } + + .tomorrow-events { + flex-direction: column; + } + + .tomorrow-events .event-item { + width: 100%; + min-width: auto; + } +} + +@media (max-width: 480px) { + .home-container { + padding: 1rem; + } + + .greeting { + font-size: 1.5rem; + margin-bottom: 1.25rem; + } + + .tomorrow-events .event-item { + width: 100%; + } + + .week-events { + grid-template-columns: 1fr; + } +} + +/* Ensure scrolling works properly within the TabView */ +@media (max-height: 700px) { + .home-container { + padding-bottom: 6.25rem; + } +} \ No newline at end of file diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index 46806b8..e2d023b 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -1,53 +1,126 @@ -import React from 'react'; -import { FontAwesomeIcon } from '../../components/ui/FontAwesomeIcons'; +import React, { useEffect, useState } from 'react'; +import { getEvents, Event } from '../../lib/api/endpoints'; +import './Home.css'; const Home: React.FC = () => { + const [todayEvents, setTodayEvents] = useState([]); + const [tomorrowEvents, setTomorrowEvents] = useState([]); + const [weekEvents, setWeekEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [userName, setUserName] = useState('John'); + + useEffect(() => { + const fetchEvents = async () => { + try { + setLoading(true); + const response = await getEvents(); + + // Get today, tomorrow, and this week's date ranges + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + + const weekStart = new Date(today); + const weekEnd = new Date(today); + weekEnd.setDate(weekEnd.getDate() + 7); + + // Filter events for each time period + const todayEvts = response.filter(event => { + const eventDate = new Date(event.start); + eventDate.setHours(0, 0, 0, 0); + return eventDate.getTime() === today.getTime(); + }); + + const tomorrowEvts = response.filter(event => { + const eventDate = new Date(event.start); + eventDate.setHours(0, 0, 0, 0); + return eventDate.getTime() === tomorrow.getTime(); + }); + + const weekEvts = response.filter(event => { + const eventDate = new Date(event.start); + eventDate.setHours(0, 0, 0, 0); + return eventDate > today && eventDate <= weekEnd && + eventDate.getTime() !== today.getTime() && + eventDate.getTime() !== tomorrow.getTime(); + }); + + setTodayEvents(todayEvts); + setTomorrowEvents(tomorrowEvts); + setWeekEvents(weekEvts); + setLoading(false); + } catch (err) { + setError('Failed to load events'); + setLoading(false); + } + }; + + fetchEvents(); + }, []); + + const EventItem = ({ event }: { event: Event }) => ( +
+
{event.title}
+
+ {new Date(event.start).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + {!event.allDay && event.end && ` - ${new Date(event.end).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`} + {event.allDay && ' (All day)'} +
+
+ ); + + if (loading) { + return
Loading events...
; + } + + if (error) { + return
{error}
; + } + return (
-
-

Welcome to Our Application

-

Your centralized dashboard for all features

-
+

Hallo {userName}!

-
-
-
- -
-

Create New

-

Start a new project or task

-
- -
-
- -
-

Schedule

-

Manage your upcoming events

-
- -
-
- -
-

Edit

-

Modify your existing content

+
+

Heute

+
+ {todayEvents.length > 0 ? ( + todayEvents.map(event => ( + + )) + ) : ( +
Keine Termine für heute
+ )}
-
-

Quick Links

- +
+

Morgen

+
+ {tomorrowEvents.length > 0 ? ( + tomorrowEvents.map(event => ( + + )) + ) : ( +
Keine Termine für morgen
+ )} +
+
+ +
+

Diese Woche

+
+ {weekEvents.length > 0 ? ( + weekEvents.map(event => ( + + )) + ) : ( +
Keine Termine für diese Woche
+ )} +
); diff --git a/frontend/src/pages/profile/Profile.tsx b/frontend/src/pages/profile/Profile.tsx new file mode 100644 index 0000000..8f94f9f --- /dev/null +++ b/frontend/src/pages/profile/Profile.tsx @@ -0,0 +1,41 @@ +import { useUser } from '../../lib/context'; + +const Profile = () => { + const { user, loading, error, refetch } = useUser(); + + if (loading) { + return
Loading user data...
; + } + + if (error) { + return ( +
+

Error loading user data: {error.message}

+ +
+ ); + } + + if (!user) { + return ( +
+

No user data available

+ +
+ ); + } + + return ( +
+

Profile

+
+ Name: {user.name} +
+
+ Email: {user.email} +
+
+ ); +}; + +export default Profile; \ No newline at end of file