From 3caf2e4c4924ecd3fcf554f6dd285606785af07c Mon Sep 17 00:00:00 2001 From: Pierre-Louis Date: Wed, 16 Oct 2024 14:39:59 +0200 Subject: [PATCH 1/4] feat(project-sheet): Add project sheet --- symfony/composer.json | 2 +- symfony/composer.lock | 201 +++++++++---- symfony/config/bundles.php | 1 + .../packages/stof_doctrine_extensions.yaml | 10 + symfony/fixtures/projects.yaml | 10 +- symfony/migrations/Version20241016122346.php | 42 +++ .../Project/SimilarProjectsAction.php | 21 ++ symfony/src/Entity/Actor.php | 10 +- symfony/src/Entity/Project.php | 78 ++++- symfony/src/Entity/Thematic.php | 4 +- symfony/src/Entity/Trait/BlamableEntity.php | 47 +++ symfony/src/Entity/Trait/SluggableEntity.php | 27 ++ .../src/Entity/Trait/TimestampableEntity.php | 2 +- symfony/src/Repository/ProjectRepository.php | 15 + symfony/symfony.lock | 268 ++++++++++++++++++ vue/src/App.vue | 6 +- vue/src/assets/plugins/vuetify.ts | 3 +- vue/src/assets/styles/global/app.scss | 40 +-- vue/src/assets/styles/global/utils.scss | 138 +++++++++ .../styles/global/vars/_dimensions.scss | 2 +- vue/src/assets/styles/views/SheetView.scss | 108 +++++++ vue/src/assets/translations/fr/actors.json | 3 +- vue/src/assets/translations/fr/common.json | 10 + vue/src/assets/translations/fr/projects.json | 11 + vue/src/components/banners/PageBanner.vue | 50 ++-- vue/src/components/banners/SectionBanner.vue | 45 +-- vue/src/components/content/ContactCard.vue | 47 +++ vue/src/components/content/ContentBanner.vue | 56 ---- vue/src/components/content/KPI.vue | 74 +++++ vue/src/components/global/Autocomplete.vue | 16 +- vue/src/components/global/BasicCard.vue | 2 +- vue/src/components/global/InfoCard.vue | 12 +- vue/src/components/global/LikeButton.vue | 2 +- vue/src/components/global/PrintButton.vue | 36 +++ vue/src/components/map/Map.vue | 38 +-- .../map/controls/ResetMapExtentControl.ts | 28 -- .../map/controls/ResetMapExtentControl.vue | 22 ++ .../map/controls/ToggleSidebarControl.ts | 28 -- .../map/controls/ToggleSidebarControl.vue | 30 ++ .../components/text-elements/PageTitle.vue | 15 +- .../components/text-elements/SectionTitle.vue | 15 +- vue/src/composables/useDate.ts | 4 +- vue/src/models/interfaces/Project.ts | 5 + vue/src/router/index.ts | 19 +- vue/src/services/map/MapService.ts | 27 +- vue/src/services/projects/ProjectService.ts | 12 +- vue/src/stores/projectStore.ts | 26 +- .../_layout/sheet/SheetContentBanner.vue | 70 +++++ .../views/_layout/sheet/UpdatedAtLabel.vue | 24 ++ vue/src/views/actors/ActorsView.vue | 9 +- .../views/actors/components/ActorProfile.vue | 22 +- .../actors/components/ActorRelatedContent.vue | 10 +- vue/src/views/admin/AdminView.vue | 2 +- .../admin-content/AdminActorsPanel.vue | 2 +- .../{ProjectsView.vue => ProjectListView.vue} | 0 vue/src/views/projects/ProjectSheetView.vue | 138 +++++++++ .../views/projects/components/ProjectCard.vue | 2 +- .../views/projects/components/ProjectMap.vue | 10 +- .../components/ProjectRelatedContent.vue | 42 +++ .../ShowProjectFiltersModalControl.ts | 34 --- .../ShowProjectFiltersModalControl.vue | 30 ++ 61 files changed, 1652 insertions(+), 411 deletions(-) create mode 100644 symfony/config/packages/stof_doctrine_extensions.yaml create mode 100644 symfony/migrations/Version20241016122346.php create mode 100644 symfony/src/Controller/Project/SimilarProjectsAction.php create mode 100644 symfony/src/Entity/Trait/BlamableEntity.php create mode 100644 symfony/src/Entity/Trait/SluggableEntity.php create mode 100644 symfony/symfony.lock create mode 100644 vue/src/assets/styles/global/utils.scss create mode 100644 vue/src/assets/styles/views/SheetView.scss create mode 100644 vue/src/components/content/ContactCard.vue delete mode 100644 vue/src/components/content/ContentBanner.vue create mode 100644 vue/src/components/content/KPI.vue create mode 100644 vue/src/components/global/PrintButton.vue delete mode 100644 vue/src/components/map/controls/ResetMapExtentControl.ts create mode 100644 vue/src/components/map/controls/ResetMapExtentControl.vue delete mode 100644 vue/src/components/map/controls/ToggleSidebarControl.ts create mode 100644 vue/src/components/map/controls/ToggleSidebarControl.vue create mode 100644 vue/src/views/_layout/sheet/SheetContentBanner.vue create mode 100644 vue/src/views/_layout/sheet/UpdatedAtLabel.vue rename vue/src/views/projects/{ProjectsView.vue => ProjectListView.vue} (100%) create mode 100644 vue/src/views/projects/ProjectSheetView.vue create mode 100644 vue/src/views/projects/components/ProjectRelatedContent.vue delete mode 100644 vue/src/views/projects/components/map-controls/ShowProjectFiltersModalControl.ts create mode 100644 vue/src/views/projects/components/map-controls/ShowProjectFiltersModalControl.vue diff --git a/symfony/composer.json b/symfony/composer.json index 65db706b..31c61e06 100644 --- a/symfony/composer.json +++ b/symfony/composer.json @@ -15,7 +15,7 @@ "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.2", "lexik/jwt-authentication-bundle": "^3.1", - "gedmo/doctrine-extensions": "^3.16", + "stof/doctrine-extensions-bundle": "^1.7", "hautelook/alice-bundle": "^2.14", "jsor/doctrine-postgis": "^2.3", "nelmio/cors-bundle": "^2.5", diff --git a/symfony/composer.lock b/symfony/composer.lock index cf759f32..cccb61fd 100644 --- a/symfony/composer.lock +++ b/symfony/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c8d623fda8ec11818e220f61fa4d5ffa", + "content-hash": "75cc4f9a3a5913683cef23a56e760fab", "packages": [ { "name": "api-platform/core", - "version": "v3.4.2", + "version": "v3.4.3", "source": { "type": "git", "url": "https://github.com/api-platform/core.git", - "reference": "6dfa89bf228ea1fe5a0db0f9de3054018c4ef57e" + "reference": "a11c21335297b6026aa2b12fbe11a77e888481d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/6dfa89bf228ea1fe5a0db0f9de3054018c4ef57e", - "reference": "6dfa89bf228ea1fe5a0db0f9de3054018c4ef57e", + "url": "https://api.github.com/repos/api-platform/core/zipball/a11c21335297b6026aa2b12fbe11a77e888481d7", + "reference": "a11c21335297b6026aa2b12fbe11a77e888481d7", "shasum": "" }, "require": { @@ -223,9 +223,9 @@ ], "support": { "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/v3.4.2" + "source": "https://github.com/api-platform/core/tree/v3.4.3" }, - "time": "2024-10-04T14:08:00+00:00" + "time": "2024-10-11T12:19:54+00:00" }, { "name": "behat/transliterator", @@ -457,16 +457,16 @@ }, { "name": "doctrine/common", - "version": "3.4.4", + "version": "3.4.5", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a" + "reference": "6c8fef961f67b8bc802ce3e32e3ebd1022907286" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", - "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", + "url": "https://api.github.com/repos/doctrine/common/zipball/6c8fef961f67b8bc802ce3e32e3ebd1022907286", + "reference": "6c8fef961f67b8bc802ce3e32e3ebd1022907286", "shasum": "" }, "require": { @@ -528,7 +528,7 @@ ], "support": { "issues": "https://github.com/doctrine/common/issues", - "source": "https://github.com/doctrine/common/tree/3.4.4" + "source": "https://github.com/doctrine/common/tree/3.4.5" }, "funding": [ { @@ -544,7 +544,7 @@ "type": "tidelift" } ], - "time": "2024-04-16T13:35:33+00:00" + "time": "2024-10-08T15:53:43+00:00" }, { "name": "doctrine/data-fixtures", @@ -632,16 +632,16 @@ }, { "name": "doctrine/dbal", - "version": "3.9.1", + "version": "3.9.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7" + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", - "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", "shasum": "" }, "require": { @@ -657,7 +657,7 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.12.0", + "phpstan/phpstan": "1.12.6", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", @@ -725,7 +725,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.1" + "source": "https://github.com/doctrine/dbal/tree/3.9.3" }, "funding": [ { @@ -741,7 +741,7 @@ "type": "tidelift" } ], - "time": "2024-09-01T13:49:23+00:00" + "time": "2024-10-10T17:56:43+00:00" }, { "name": "doctrine/deprecations", @@ -1333,16 +1333,16 @@ }, { "name": "doctrine/migrations", - "version": "3.8.1", + "version": "3.8.2", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "7760fbd0b7cb58bfb50415505a7bab821adf0877" + "reference": "5007eb1168691225ac305fe16856755c20860842" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/7760fbd0b7cb58bfb50415505a7bab821adf0877", - "reference": "7760fbd0b7cb58bfb50415505a7bab821adf0877", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/5007eb1168691225ac305fe16856755c20860842", + "reference": "5007eb1168691225ac305fe16856755c20860842", "shasum": "" }, "require": { @@ -1416,7 +1416,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.8.1" + "source": "https://github.com/doctrine/migrations/tree/3.8.2" }, "funding": [ { @@ -1432,20 +1432,20 @@ "type": "tidelift" } ], - "time": "2024-08-28T13:17:28+00:00" + "time": "2024-10-10T21:35:27+00:00" }, { "name": "doctrine/orm", - "version": "3.2.2", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "831a1eb7d260925528cdbb49cc1866c0357cf147" + "reference": "69958152e661aa9c14e80d1ee4962863485aa60b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/831a1eb7d260925528cdbb49cc1866c0357cf147", - "reference": "831a1eb7d260925528cdbb49cc1866c0357cf147", + "url": "https://api.github.com/repos/doctrine/orm/zipball/69958152e661aa9c14e80d1ee4962863485aa60b", + "reference": "69958152e661aa9c14e80d1ee4962863485aa60b", "shasum": "" }, "require": { @@ -1467,7 +1467,10 @@ "require-dev": { "doctrine/coding-standard": "^12.0", "phpbench/phpbench": "^1.0", - "phpstan/phpstan": "1.11.1", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "1.12.6", + "phpstan/phpstan-deprecation-rules": "^1.2", "phpunit/phpunit": "^10.4.0", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", @@ -1518,9 +1521,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/3.2.2" + "source": "https://github.com/doctrine/orm/tree/3.3.0" }, - "time": "2024-08-23T10:03:52+00:00" + "time": "2024-10-12T20:07:18+00:00" }, { "name": "doctrine/persistence", @@ -2253,38 +2256,38 @@ }, { "name": "lcobucci/jwt", - "version": "5.3.0", + "version": "5.4.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83" + "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", - "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/aac4fd512681fd5cb4b77d2105ab7ec700c72051", + "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051", "shasum": "" }, "require": { "ext-openssl": "*", "ext-sodium": "*", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "psr/clock": "^1.0" }, "require-dev": { - "infection/infection": "^0.27.0", - "lcobucci/clock": "^3.0", + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", "lcobucci/coding-standard": "^11.0", - "phpbench/phpbench": "^1.2.9", + "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.2", "phpstan/phpstan": "^1.10.7", "phpstan/phpstan-deprecation-rules": "^1.1.3", "phpstan/phpstan-phpunit": "^1.3.10", "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^10.2.6" + "phpunit/phpunit": "^11.1" }, "suggest": { - "lcobucci/clock": ">= 3.0" + "lcobucci/clock": ">= 3.2" }, "type": "library", "autoload": { @@ -2310,7 +2313,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.3.0" + "source": "https://github.com/lcobucci/jwt/tree/5.4.0" }, "funding": [ { @@ -2322,7 +2325,7 @@ "type": "patreon" } ], - "time": "2024-04-11T23:07:54+00:00" + "time": "2024-10-08T22:06:45+00:00" }, { "name": "lexik/jwt-authentication-bundle", @@ -2658,16 +2661,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.0", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a", - "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { @@ -2710,9 +2713,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2024-09-29T13:56:26+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -2891,16 +2894,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.32.0", + "version": "1.33.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4" + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4", - "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140", "shasum": "" }, "require": { @@ -2932,9 +2935,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0" }, - "time": "2024-09-26T07:23:32+00:00" + "time": "2024-10-13T11:25:22+00:00" }, { "name": "psr/cache", @@ -3923,6 +3926,86 @@ ], "time": "2024-07-29T11:22:56+00:00" }, + { + "name": "stof/doctrine-extensions-bundle", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/stof/StofDoctrineExtensionsBundle.git", + "reference": "473ae65598fa4160654c350e139e20ee75d9a91a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stof/StofDoctrineExtensionsBundle/zipball/473ae65598fa4160654c350e139e20ee75d9a91a", + "reference": "473ae65598fa4160654c350e139e20ee75d9a91a", + "shasum": "" + }, + "require": { + "gedmo/doctrine-extensions": "^3.15.0", + "php": "^7.4 || ^8.0", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpstan/phpstan-symfony": "^1.3", + "symfony/mime": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^v6.4.1 || ^7.0.1", + "symfony/security-core": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "to use the ORM extensions", + "doctrine/mongodb-odm-bundle": "to use the MongoDB ODM extensions", + "symfony/mime": "To use the Mime component integration for Uploadable" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Stof\\DoctrineExtensionsBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + } + ], + "description": "Integration of the gedmo/doctrine-extensions with Symfony", + "homepage": "https://github.com/stof/StofDoctrineExtensionsBundle", + "keywords": [ + "behaviors", + "doctrine2", + "extensions", + "gedmo", + "loggable", + "nestedset", + "sluggable", + "sortable", + "timestampable", + "translatable", + "tree" + ], + "support": { + "issues": "https://github.com/stof/StofDoctrineExtensionsBundle/issues", + "source": "https://github.com/stof/StofDoctrineExtensionsBundle/tree/v1.12.0" + }, + "time": "2024-06-10T12:27:27+00:00" + }, { "name": "symfony/asset", "version": "v7.1.1", @@ -8621,7 +8704,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -8629,6 +8712,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/symfony/config/bundles.php b/symfony/config/bundles.php index 6c83ab50..ed393bd2 100644 --- a/symfony/config/bundles.php +++ b/symfony/config/bundles.php @@ -16,4 +16,5 @@ Hautelook\AliceBundle\HautelookAliceBundle::class => ['all' => true], Sentry\SentryBundle\SentryBundle::class => ['prod' => true], Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], + Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], ]; diff --git a/symfony/config/packages/stof_doctrine_extensions.yaml b/symfony/config/packages/stof_doctrine_extensions.yaml new file mode 100644 index 00000000..31c645f2 --- /dev/null +++ b/symfony/config/packages/stof_doctrine_extensions.yaml @@ -0,0 +1,10 @@ +# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html +# See the official DoctrineExtensions documentation for more details: https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc +stof_doctrine_extensions: + default_locale: en_US + orm: + default: + tree: true + blameable: true + sluggable: true + timestampable: true \ No newline at end of file diff --git a/symfony/fixtures/projects.yaml b/symfony/fixtures/projects.yaml index 5d478c63..4f9b6593 100644 --- a/symfony/fixtures/projects.yaml +++ b/symfony/fixtures/projects.yaml @@ -5,7 +5,9 @@ App\Entity\Project: # coords: 'POINT( )' coords: 'POINT( )' status: ')>' - description: + description: + deliverables: + calendar: images: [] partners: [] interventionZone: ')>' @@ -17,8 +19,6 @@ App\Entity\Project: projectManagerPosition: projectManagerEmail: projectManagerTel: - projectManagerPhoto: + projectManagerPhoto: 'https://randomuser.me/api/portraits/men/.jpg' website: - logo: - created_at: '' - updated_at: '' \ No newline at end of file + logo: \ No newline at end of file diff --git a/symfony/migrations/Version20241016122346.php b/symfony/migrations/Version20241016122346.php new file mode 100644 index 00000000..36cbb771 --- /dev/null +++ b/symfony/migrations/Version20241016122346.php @@ -0,0 +1,42 @@ +addSql('ALTER TABLE project ADD deliverables TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE project ADD calendar TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE project ADD created_by VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE project ADD updated_by VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE project ADD slug VARCHAR(128) DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_2FB3D0EE989D9B62 ON project (slug)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP INDEX UNIQ_2FB3D0EE989D9B62'); + $this->addSql('ALTER TABLE project DROP deliverables'); + $this->addSql('ALTER TABLE project DROP calendar'); + $this->addSql('ALTER TABLE project DROP created_by'); + $this->addSql('ALTER TABLE project DROP updated_by'); + $this->addSql('ALTER TABLE project DROP slug'); + } +} diff --git a/symfony/src/Controller/Project/SimilarProjectsAction.php b/symfony/src/Controller/Project/SimilarProjectsAction.php new file mode 100644 index 00000000..aba7bc3f --- /dev/null +++ b/symfony/src/Controller/Project/SimilarProjectsAction.php @@ -0,0 +1,21 @@ +projectRepository->findTwoSimilarProjectsByThematics($project); + return $similarProjects; + } +} \ No newline at end of file diff --git a/symfony/src/Entity/Actor.php b/symfony/src/Entity/Actor.php index e33bbd6f..7805972f 100644 --- a/symfony/src/Entity/Actor.php +++ b/symfony/src/Entity/Actor.php @@ -73,11 +73,11 @@ class Actor private ?Uuid $id = null; #[ORM\Column(length: 255)] - #[Groups([self::ACTOR_READ_ITEM_COLLECTION, self::ACTOR_READ_ITEM, self::ACTOR_WRITE, Project::PROJECT_READ_ALL])] + #[Groups([self::ACTOR_READ_ITEM_COLLECTION, self::ACTOR_READ_ITEM, self::ACTOR_WRITE, Project::PROJECT_READ_ALL, Project::PROJECT_READ])] private ?string $name = null; #[ORM\Column(length: 255)] - #[Groups([self::ACTOR_READ_ITEM_COLLECTION, self::ACTOR_READ_ITEM, self::ACTOR_WRITE, Project::PROJECT_READ_ALL])] + #[Groups([self::ACTOR_READ_ITEM_COLLECTION, self::ACTOR_READ_ITEM, self::ACTOR_WRITE, Project::PROJECT_READ_ALL, Project::PROJECT_READ])] private ?string $acronym = null; #[ORM\ManyToOne(inversedBy: 'actorsCreated')] @@ -90,7 +90,7 @@ class Actor private ?bool $isValidated = false; #[ORM\Column(enumType: ActorCategory::class)] - #[Groups([self::ACTOR_READ_ITEM_COLLECTION, self::ACTOR_READ_ITEM, self::ACTOR_WRITE])] + #[Groups([self::ACTOR_READ_ITEM_COLLECTION, self::ACTOR_READ_ITEM, self::ACTOR_WRITE, Project::PROJECT_READ])] private ?ActorCategory $category = null; /** @@ -148,7 +148,7 @@ class Actor private Collection $projects; #[ORM\Column(length: 255, nullable: true)] - #[Groups([self::ACTOR_READ_ITEM_COLLECTION,self::ACTOR_READ_ITEM, self::ACTOR_WRITE])] + #[Groups([self::ACTOR_READ_ITEM_COLLECTION,self::ACTOR_READ_ITEM, self::ACTOR_WRITE, Project::PROJECT_READ])] private ?string $logo = null; /** @@ -189,7 +189,7 @@ public function getAcronym(): ?string public function setAcronym(string $acronym): static { - $this->acronym = $acronym; + $this->acronym = strtoupper($acronym); return $this; } diff --git a/symfony/src/Entity/Project.php b/symfony/src/Entity/Project.php index e0b4d90b..1f05d1d5 100644 --- a/symfony/src/Entity/Project.php +++ b/symfony/src/Entity/Project.php @@ -2,31 +2,53 @@ namespace App\Entity; +use ApiPlatform\Doctrine\Common\Filter\SearchFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\GetCollection; use App\Enum\AdministrativeScope; use App\Enum\Status; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\QueryParameter; +use App\Controller\Project\SimilarProjectsAction; +use App\Entity\Trait\SluggableEntity; use App\Repository\ProjectRepository; use App\Entity\Trait\TimestampableEntity; use Doctrine\Common\Collections\Collection; use Jsor\Doctrine\PostGIS\Types\PostGISType; use Doctrine\Common\Collections\ArrayCollection; +use Gedmo\Blameable\Traits\BlameableEntity; use Symfony\Component\Serializer\Attribute\Groups; #[ORM\Entity(repositoryClass: ProjectRepository::class)] +#[ApiFilter(filterClass: SearchFilter::class, properties: ['slug' => SearchFilterInterface::STRATEGY_EXACT])] #[ApiResource( paginationEnabled: false, operations: [ new GetCollection( + uriTemplate: '/projects/all', normalizationContext: ['groups' => [self::PROJECT_READ_ALL]] - ) + ), + new GetCollection( + normalizationContext: ['groups' => [self::PROJECT_READ]], + parameters: [ + 'slug' => new QueryParameter() + ] + ), + new GetCollection( + uriTemplate: '/projects/{id}/similar', + controller: SimilarProjectsAction::class, + normalizationContext: ['groups' => [self::PROJECT_READ_ALL]] + ), ] )] class Project { use TimestampableEntity; + use BlameableEntity; + use SluggableEntity; public const PROJECT_READ = 'project:read'; public const PROJECT_READ_ALL = 'project:read:all'; @@ -34,35 +56,37 @@ class Project #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] - #[Groups([self::PROJECT_READ_ALL])] + #[Groups([self::PROJECT_READ, self::PROJECT_READ_ALL])] private ?int $id = null; #[ORM\Column(length: 255)] - #[Groups([self::PROJECT_READ_ALL, Actor::ACTOR_READ_ITEM])] + #[Groups([self::PROJECT_READ, self::PROJECT_READ_ALL, Actor::ACTOR_READ_ITEM])] private ?string $name = null; #[ORM\Column(length: 255)] - #[Groups([self::PROJECT_READ_ALL])] + #[Groups([self::PROJECT_READ, self::PROJECT_READ_ALL])] private ?string $location = null; #[ORM\Column( type: PostGISType::GEOMETRY, options: ['geometry_type' => 'POINT'], )] - #[Groups([self::PROJECT_READ_ALL])] + #[Groups([self::PROJECT_READ, self::PROJECT_READ_ALL])] private ?string $coords = null; #[ORM\Column(enumType: Status::class)] - #[Groups([self::PROJECT_READ_ALL])] + #[Groups([self::PROJECT_READ, self::PROJECT_READ_ALL])] private ?Status $status = null; #[ORM\Column(type: Types::TEXT, nullable: true)] + #[Groups([self::PROJECT_READ])] private ?string $description = null; #[ORM\Column(type: Types::JSON, nullable: true)] private ?array $images = null; #[ORM\Column(type: Types::JSON, nullable: true)] + #[Groups([self::PROJECT_READ])] private ?array $partners = null; #[ORM\Column(enumType: AdministrativeScope::class)] @@ -73,29 +97,33 @@ class Project * @var Collection */ #[ORM\ManyToMany(targetEntity: Thematic::class, inversedBy: 'projects')] - #[Groups([self::PROJECT_READ_ALL])] + #[Groups([self::PROJECT_READ, self::PROJECT_READ_ALL])] private Collection $thematics; #[ORM\Column(length: 255, nullable: true)] + #[Groups([self::PROJECT_READ])] private ?string $projectManagerName = null; #[ORM\Column(length: 255, nullable: true)] + #[Groups([self::PROJECT_READ])] private ?string $projectManagerPosition = null; #[ORM\Column(length: 255, nullable: true)] private ?string $projectManagerEmail = null; #[ORM\Column(length: 255, nullable: true)] + #[Groups([self::PROJECT_READ])] private ?string $projectManagerTel = null; #[ORM\Column(length: 255, nullable: true)] + #[Groups([self::PROJECT_READ])] private ?string $projectManagerPhoto = null; #[ORM\Column(length: 255, nullable: true)] private ?string $website = null; #[ORM\Column(length: 255, nullable: true)] - #[Groups([self::PROJECT_READ_ALL])] + #[Groups([self::PROJECT_READ, self::PROJECT_READ_ALL])] private ?string $logo = null; /** @@ -117,9 +145,17 @@ class Project #[ORM\ManyToOne(inversedBy: 'projects')] #[ORM\JoinColumn(nullable: false)] - #[Groups([self::PROJECT_READ_ALL])] + #[Groups([self::PROJECT_READ, self::PROJECT_READ_ALL])] private ?Actor $actor = null; + #[ORM\Column(type: Types::TEXT, nullable: true)] + #[Groups([self::PROJECT_READ])] + private ?string $deliverables = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + #[Groups([self::PROJECT_READ])] + private ?string $calendar = null; + public function __construct() { $this->thematics = new ArrayCollection(); @@ -400,4 +436,28 @@ public function setActor(?Actor $actor): static return $this; } + + public function getDeliverables(): ?string + { + return $this->deliverables; + } + + public function setDeliverables(?string $deliverables): static + { + $this->deliverables = $deliverables; + + return $this; + } + + public function getCalendar(): ?string + { + return $this->calendar; + } + + public function setCalendar(?string $calendar): static + { + $this->calendar = $calendar; + + return $this; + } } diff --git a/symfony/src/Entity/Thematic.php b/symfony/src/Entity/Thematic.php index 407e607a..2f214659 100644 --- a/symfony/src/Entity/Thematic.php +++ b/symfony/src/Entity/Thematic.php @@ -34,11 +34,11 @@ class Thematic #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] - #[Groups([self::THEMATIC_READ, Project::PROJECT_READ_ALL])] + #[Groups([self::THEMATIC_READ, Project::PROJECT_READ, Project::PROJECT_READ_ALL])] private ?int $id = null; #[ORM\Column(length: 255)] - #[Groups([self::THEMATIC_READ, Actor::ACTOR_READ_ITEM, Project::PROJECT_READ_ALL])] + #[Groups([self::THEMATIC_READ, Actor::ACTOR_READ_ITEM, Project::PROJECT_READ, Project::PROJECT_READ_ALL])] private ?string $name = null; /** diff --git a/symfony/src/Entity/Trait/BlamableEntity.php b/symfony/src/Entity/Trait/BlamableEntity.php new file mode 100644 index 00000000..85dbd4b0 --- /dev/null +++ b/symfony/src/Entity/Trait/BlamableEntity.php @@ -0,0 +1,47 @@ +createdBy = $createdBy; + + return $this; + } + + public function getCreatedBy(): ?User + { + return $this->createdBy; + } + + public function setUpdatedBy(User $updatedBy): self + { + $this->updatedBy = $updatedBy; + + return $this; + } + + public function getUpdatedBy(): ?User + { + return $this->updatedBy; + } +} diff --git a/symfony/src/Entity/Trait/SluggableEntity.php b/symfony/src/Entity/Trait/SluggableEntity.php new file mode 100644 index 00000000..36b885eb --- /dev/null +++ b/symfony/src/Entity/Trait/SluggableEntity.php @@ -0,0 +1,27 @@ +slug; + } + + public function setSlug($slug): static + { + $this->slug = $slug; + return $this; + } +} diff --git a/symfony/src/Entity/Trait/TimestampableEntity.php b/symfony/src/Entity/Trait/TimestampableEntity.php index 522e318f..dc164192 100644 --- a/symfony/src/Entity/Trait/TimestampableEntity.php +++ b/symfony/src/Entity/Trait/TimestampableEntity.php @@ -16,7 +16,7 @@ trait TimestampableEntity #[Gedmo\Timestampable(on: 'update')] #[ORM\Column(type: Types::DATETIME_MUTABLE)] - #[Groups([Project::PROJECT_READ_ALL])] + #[Groups([Project::PROJECT_READ, Project::PROJECT_READ_ALL])] protected ?\DateTimeInterface $updatedAt; public function getCreatedAt(): ?\DateTimeInterface diff --git a/symfony/src/Repository/ProjectRepository.php b/symfony/src/Repository/ProjectRepository.php index ec56d93b..0e965b40 100644 --- a/symfony/src/Repository/ProjectRepository.php +++ b/symfony/src/Repository/ProjectRepository.php @@ -16,6 +16,21 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Project::class); } + + public function findTwoSimilarProjectsByThematics(Project $project): array { + return $this->createQueryBuilder('p') + ->leftJoin('p.thematics', 't') + ->addSelect('t') + ->andWhere('p.id != :id') + ->andWhere('t.id IN (:thematicIds)') + ->setParameter('thematicIds', $project->getThematics()->map(fn($thematic) => $thematic->getId())) + ->setParameter('id', $project->getId()) + ->orderBy('p.updatedAt', 'DESC') + ->setMaxResults(2) + ->getQuery() + ->getResult() + ; + } // /** // * @return Project[] Returns an array of Project objects // */ diff --git a/symfony/symfony.lock b/symfony/symfony.lock new file mode 100644 index 00000000..4bf5f4f4 --- /dev/null +++ b/symfony/symfony.lock @@ -0,0 +1,268 @@ +{ + "api-platform/core": { + "version": "3.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.3", + "ref": "74b45ac570c57eb1fbe56c984091a9ff87e18bab" + }, + "files": [ + "config/packages/api_platform.yaml", + "config/routes/api_platform.yaml", + "src/ApiResource/.gitignore" + ] + }, + "doctrine/doctrine-bundle": { + "version": "2.13", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.13", + "ref": "8d96c0b51591ffc26794d865ba3ee7d193438a83" + }, + "files": [ + "config/packages/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.1", + "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" + }, + "files": [ + "config/packages/doctrine_migrations.yaml", + "migrations/.gitignore" + ] + }, + "hautelook/alice-bundle": { + "version": "2.14", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.2", + "ref": "c84e4f2b9d7f436d7d52e8369230b393367607ec" + }, + "files": [ + "config/packages/hautelook_alice.yaml", + "fixtures/.gitignore" + ] + }, + "jsor/doctrine-postgis": { + "version": "2.3", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.7", + "ref": "211979f7917bf6edb8fc5006a17b2e84e8e84d50" + } + }, + "lexik/jwt-authentication-bundle": { + "version": "3.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7" + }, + "files": [ + "config/packages/lexik_jwt_authentication.yaml" + ] + }, + "nelmio/alice": { + "version": "3.13", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.3", + "ref": "42b52d2065dc3fde27912d502c18ca1926e35ae2" + }, + "files": [ + "config/packages/nelmio_alice.yaml" + ] + }, + "nelmio/cors-bundle": { + "version": "2.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.5", + "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" + }, + "files": [ + "config/packages/nelmio_cors.yaml" + ] + }, + "sentry/sentry-symfony": { + "version": "5.0", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "5.0", + "ref": "76afa07d23e76f678942f00af5a6a417ba0816d0" + } + }, + "stof/doctrine-extensions-bundle": { + "version": "1.12", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.2", + "ref": "e805aba9eff5372e2d149a9ff56566769e22819d" + }, + "files": [ + "config/packages/stof_doctrine_extensions.yaml" + ] + }, + "symfony/console": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461" + }, + "files": [ + "bin/console" + ] + }, + "symfony/debug-bundle": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "5aa8aa48234c8eb6dbdd7b3cd5d791485d2cec4b" + }, + "files": [ + "config/packages/debug.yaml" + ] + }, + "symfony/flex": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172" + }, + "files": [ + ".env" + ] + }, + "symfony/framework-bundle": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "6356c19b9ae08e7763e4ba2d9ae63043efc75db5" + }, + "files": [ + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/preload.php", + "config/routes/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/maker-bundle": { + "version": "1.61", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, + "symfony/routing": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "21b72649d5622d8f7da329ffb5afb232a023619d" + }, + "files": [ + "config/packages/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/security-bundle": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "2ae08430db28c8eb4476605894296c82a642028f" + }, + "files": [ + "config/packages/security.yaml", + "config/routes/security.yaml" + ] + }, + "symfony/twig-bundle": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/uid": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "0df5844274d871b37fc3816c57a768ffc60a43a5" + } + }, + "symfony/validator": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd" + }, + "files": [ + "config/packages/validator.yaml" + ] + }, + "symfony/web-profiler-bundle": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.1", + "ref": "e42b3f0177df239add25373083a564e5ead4e13a" + }, + "files": [ + "config/packages/web_profiler.yaml", + "config/routes/web_profiler.yaml" + ] + }, + "theofidry/alice-data-fixtures": { + "version": "1.7", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fe5a50faf580eb58f08ada2abe8afbd2d4941e05" + } + } +} diff --git a/vue/src/App.vue b/vue/src/App.vue index 249b238e..6f49a286 100644 --- a/vue/src/App.vue +++ b/vue/src/App.vue @@ -12,8 +12,10 @@ {{ appStore.snackBarMessage }}
- - + + + +
diff --git a/vue/src/assets/plugins/vuetify.ts b/vue/src/assets/plugins/vuetify.ts index 2da4d8e0..916f8be7 100644 --- a/vue/src/assets/plugins/vuetify.ts +++ b/vue/src/assets/plugins/vuetify.ts @@ -27,7 +27,7 @@ export const vuetify = createVuetify({ }, defaults: { VBtn: { - elevation: 0, + variant: 'flat', style: [{ textTransform: 'none', fontWeight: 'bold', @@ -45,6 +45,7 @@ export const vuetify = createVuetify({ VBreadcrumbs: { style: [{ padding: '.75rem 0', + fontSize: '.875rem', }], }, VFieldLabel: { diff --git a/vue/src/assets/styles/global/app.scss b/vue/src/assets/styles/global/app.scss index 94c27aa9..781c85b3 100644 --- a/vue/src/assets/styles/global/app.scss +++ b/vue/src/assets/styles/global/app.scss @@ -1,10 +1,10 @@ // Imported in app.vue @import '@/assets/styles/global/_variables'; @import '@/assets/styles/global/vuetifyOverrides'; +@import '@/assets/styles/global/utils'; @import '@/assets/styles/views/header'; @import '@/assets/styles/views/actors'; - #app { display: flex; flex-direction: column; @@ -16,30 +16,10 @@ --dim-container-w: 70rem; } -.container { - width: var(--dim-container-w); - margin: 0 auto; - max-width: 100%; - transition: all .15s ease-in; -} - -.text-action { - text-decoration: underline; - cursor: pointer; - - &:hover { - font-weight: 500; - } -} - a { color: #000; } -.Link--withoutUnderline { - text-decoration: none; -} - * { font-family: $font-secondary; } @@ -71,10 +51,6 @@ h4 { padding: 0; } -.font-ojuju { - font-family: $font-primary; -} - .App { &__content { @@ -84,7 +60,7 @@ h4 { position: relative; &--desktop{ - padding: 30px 0; + padding: 0 0 30px 0; background-size: 45% auto; } @@ -99,16 +75,4 @@ a:-webkit-any-link { color: rgb(var(--v-theme-main-blue)); cursor: pointer; text-decoration: none; -} - -.Card { - background: white; - box-shadow: 0px 0px 0px 1px rgb(var(--v-theme-main-blue)), 0px 2px 0px 1px rgb(var(--v-theme-main-blue)); - transition: all .1s ease-in; - border: none; - cursor: pointer; - - &:hover, &[active="true"] { - box-shadow: 0px 0px 0px 2px rgb(var(--v-theme-main-blue)), 0px 4px 0px 2px rgb(var(--v-theme-main-blue)); - } } \ No newline at end of file diff --git a/vue/src/assets/styles/global/utils.scss b/vue/src/assets/styles/global/utils.scss new file mode 100644 index 00000000..3257bf3c --- /dev/null +++ b/vue/src/assets/styles/global/utils.scss @@ -0,0 +1,138 @@ +.container { + width: var(--dim-container-w); + margin: 0 auto; + max-width: 100%; + transition: all .15s ease-in; +} + +.text-action { + text-decoration: underline; + cursor: pointer; + + &:hover { + font-weight: 500; + } +} + +:root { + .half-circle { + --dim-half-circle-more: 0rem; + --dim-half-circle: calc(1.5rem + var(--dim-half-circle-more)); + --dim-half-circle-margin: calc(1rem + var(--dim-half-circle-more)); + --dim-half-circle-offset: calc(var(--dim-half-circle) + var(--dim-half-circle-margin)); + } + .half-circle--big { + --dim-half-circle-more: .5rem; + } + + @media (max-width: $bp-xl) { + .half-circle { + --dim-half-circle: 1.25rem; + --dim-half-circle-margin: 1rem; + + &--big { + --dim-half-circle-margin: 1rem; + --dim-half-circle: 1.5rem; + } + } + } +} + +.half-circle { + align-self: flex-start; + align-items: center; + gap: 1.5rem; + display: flex; + position: relative; + margin-bottom: 1rem; + margin-top: 1.5rem; + padding-left: var(--dim-half-circle-offset); + + & ~ * { + padding-left: var(--dim-half-circle-offset); + } + + &::before { + content: ""; + display: inline-block; + margin: 0 auto; + left: 0; + width: var(--dim-half-circle); + height: calc(var(--dim-half-circle) * 2); + background-color: rgb(var(--v-theme-main-yellow)); + border-radius: 0 var(--dim-half-circle) var(--dim-half-circle) 0; + position: absolute; + } +} + +.show-sm { + display: none !important; +} + +@media (max-width: $bp-xl) { + .hide-sm { + display: none !important; + } + .show-sm { + display: flex !important; + } + .half-circle { + &--big { + + align-items: flex-start; + &::before { + top: -0.125em; + } + } + } + + .fixed-btn { + position: fixed; + bottom: 0; + right: 0; + border-radius: 50%; + $dim-btn: 2.875rem; + max-width: $dim-btn; + min-height: $dim-btn; + min-width: $dim-btn; + width: $dim-btn; + height: $dim-btn; + padding: 0; + margin: 1.5rem; + z-index: 1000; + box-shadow: 0 .25rem .25rem -.125rem rgba(0, 0, 0, .25); + + .v-icon { + font-size: 1.675rem; + } + + .v-btn__content { + display: none !important; + } + + .v-btn__prepend { + margin: 0; + } + } +} + +.Link--withoutUnderline { + text-decoration: none; +} + + +.font-ojuju { + font-family: $font-primary; +} + +.Card { + background: white; + box-shadow: 0px 0px 0px 1px rgb(var(--v-theme-main-blue)), 0px 2px 0px 1px rgb(var(--v-theme-main-blue)); + transition: all .1s ease-in; + border: none; + cursor: pointer; + + &:hover, &[active="true"] { + box-shadow: 0px 0px 0px 2px rgb(var(--v-theme-main-blue)), 0px 4px 0px 2px rgb(var(--v-theme-main-blue)); + } +} \ No newline at end of file diff --git a/vue/src/assets/styles/global/vars/_dimensions.scss b/vue/src/assets/styles/global/vars/_dimensions.scss index 47584629..78aa5a30 100644 --- a/vue/src/assets/styles/global/vars/_dimensions.scss +++ b/vue/src/assets/styles/global/vars/_dimensions.scss @@ -11,4 +11,4 @@ $dim-radius: 3px; $bp-sm: 576px; $bp-md: 768px; $bp-lg: 992px; -$bp-xl: 1200px; \ No newline at end of file +$bp-xl: 1100px; \ No newline at end of file diff --git a/vue/src/assets/styles/views/SheetView.scss b/vue/src/assets/styles/views/SheetView.scss new file mode 100644 index 00000000..910aabd9 --- /dev/null +++ b/vue/src/assets/styles/views/SheetView.scss @@ -0,0 +1,108 @@ +.SheetView { + display: flex; + width: 100%; + flex-flow: column wrap; + margin-top: 1.5rem; + + display: grid; + grid-template-columns: 1fr 22.5rem; + grid-template-rows: auto auto; + grid-column-gap: 2.5rem; + + .SheetView__logoCtn { + width: 100%; + height: 275px; + background: white; + display: flex; + justify-content: center; + align-items: center; + border-radius: .25rem; + border: solid 1px rgb(var(--v-theme-main-blue)); + + .SheetView__logo { + object-fit: contain; + } + } + + .SheetView__contentCtn { + display: flex; + flex-flow: column wrap; + gap: .5rem; + width: 100%; + } + + .SheetView__updatedAtCtn { + display: flex; + align-items: center; + flex-flow: row nowrap; + color: rgb(var(--v-theme-main-blue)); + font-size: $font-size-xs; + align-self: flex-end; + } + + .SheetView__infoCard { + background: rgb(var(--v-theme-light-yellow)); + padding: 1.5rem; + display: flex; + flex-flow: column nowrap; + gap: 1.5rem; + } + + .SheetView__title { + color: rgb(var(--v-theme-main-blue)); + font-size: $font-size-h5; + font-weight: 700; + width: 100%; + padding-bottom: .5rem; + + &--divider { + border-bottom: solid 4px rgb(var(--v-theme-light-yellow)); + } + } + + .SheetView__block { + display: flex; + flex-flow: column wrap; + gap: 1rem; + + &--left { + grid-area: 1 / 1 / 2 / 2; + } + + &--right { + gap: 2rem; + grid-area: 1 / 2 / 2 / 3; + transform: translateY(-4rem); + } + + &--bottom { + grid-area: 2 / 1 / 3 / 3; + } + } +} + +@media (max-width: $bp-xl) { + .SheetView { + grid-template-columns: minmax(0, 1fr); + + // grid-template-columns: 1fr 22.5rem; + grid-template-rows: auto; + max-width: 100%; + row-gap: 2rem; + + .SheetView__block { + &--left { + grid-area: 1; + } + + &--right { + grid-area: 2; + transform: translateY(0); + } + + &--bottom { + grid-area: 3; + } + } + } +} \ No newline at end of file diff --git a/vue/src/assets/translations/fr/actors.json b/vue/src/assets/translations/fr/actors.json index 1bff3dc3..fae5c97c 100644 --- a/vue/src/assets/translations/fr/actors.json +++ b/vue/src/assets/translations/fr/actors.json @@ -2,8 +2,7 @@ "actors": { "actor": "acteur", "actors": "acteurs", - "title": "Annuaire", - "subtitle": "des acteurs", + "title": "Annuaire\ndes acteurs", "desc": "Consultez l’annuaire des acteurs du secteur de l’urbanisme au Cameroun.", "search": "Rechercher un acteur", "filtersTitle": "Affinez votre recherche", diff --git a/vue/src/assets/translations/fr/common.json b/vue/src/assets/translations/fr/common.json index 100fbdbf..66ad364c 100644 --- a/vue/src/assets/translations/fr/common.json +++ b/vue/src/assets/translations/fr/common.json @@ -71,15 +71,25 @@ }, "labels": { "updatedAt": "Mis à jour le", + "lastUpdatedAt": "Dernière mise à jour le", "reset": "Réinitialiser" }, + "buttons": { + "print": "Imprimer" + }, "placeholders": { "all": "Tous", "sortBy": "Trier par" }, "content": { + "createAMap": "Créer une carte", "website": "Visiter le site internet", "mail": "Contacter par mail", "edit": "Modifier" + }, + "kpi": { + "actors": "acteurs", + "resources": "ressources", + "data": "données" } } \ No newline at end of file diff --git a/vue/src/assets/translations/fr/projects.json b/vue/src/assets/translations/fr/projects.json index 443e22b1..0e857035 100644 --- a/vue/src/assets/translations/fr/projects.json +++ b/vue/src/assets/translations/fr/projects.json @@ -39,5 +39,16 @@ }, "showTheProjects": "Afficher les {count} projets" } + }, + "projectPage": { + "projectOwner": "Responsable du projet", + "focalPoint": "Point de contact", + "about": "À propos", + "partners": "Partenaires", + "keyNumbers": "Chiffres-clés", + "projectCalendar": "Calendrier du projet", + "projectDeliverables": "Livrables du projet", + "inImages": "En images", + "otherProjectsWithSameThematics": "D’autres projets avec des thématiques communes" } } \ No newline at end of file diff --git a/vue/src/components/banners/PageBanner.vue b/vue/src/components/banners/PageBanner.vue index eba157f2..4cad2787 100644 --- a/vue/src/components/banners/PageBanner.vue +++ b/vue/src/components/banners/PageBanner.vue @@ -1,52 +1,50 @@ \ No newline at end of file diff --git a/vue/src/components/banners/SectionBanner.vue b/vue/src/components/banners/SectionBanner.vue index e385af33..8227c030 100644 --- a/vue/src/components/banners/SectionBanner.vue +++ b/vue/src/components/banners/SectionBanner.vue @@ -1,41 +1,24 @@ \ No newline at end of file diff --git a/vue/src/components/content/ContactCard.vue b/vue/src/components/content/ContactCard.vue new file mode 100644 index 00000000..adedf625 --- /dev/null +++ b/vue/src/components/content/ContactCard.vue @@ -0,0 +1,47 @@ + + + + + \ No newline at end of file diff --git a/vue/src/components/content/ContentBanner.vue b/vue/src/components/content/ContentBanner.vue deleted file mode 100644 index 91e985ff..00000000 --- a/vue/src/components/content/ContentBanner.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - \ No newline at end of file diff --git a/vue/src/components/content/KPI.vue b/vue/src/components/content/KPI.vue new file mode 100644 index 00000000..2581fcdc --- /dev/null +++ b/vue/src/components/content/KPI.vue @@ -0,0 +1,74 @@ + + + + + \ No newline at end of file diff --git a/vue/src/components/global/Autocomplete.vue b/vue/src/components/global/Autocomplete.vue index f3bab972..fef9a952 100644 --- a/vue/src/components/global/Autocomplete.vue +++ b/vue/src/components/global/Autocomplete.vue @@ -4,17 +4,19 @@ :items="items" variant="solo" bg-color="white" - elevation="2" density="comfortable" :hide-details="true" menu-icon="" - prepend-inner-icon="mdi-magnify" :placeholder="placeholder" :item-title="itemTitle" :item-value="itemValue" :custom-filter="customFilter" v-model="selectedItem" - > + > + + @@ -32,6 +34,11 @@ defineProps<{ padding: 25px; display: flex; flex-direction: column; + height: fit-content; + + &[light="true"] { + box-shadow: none; + } &:hover, &[active="true"] { @@ -68,6 +75,7 @@ defineProps<{ display: flex; align-items: center; flex-flow: row nowrap; + gap: .5rem; } } diff --git a/vue/src/components/global/LikeButton.vue b/vue/src/components/global/LikeButton.vue index 41933ae3..217fe90b 100644 --- a/vue/src/components/global/LikeButton.vue +++ b/vue/src/components/global/LikeButton.vue @@ -1,6 +1,6 @@ diff --git a/vue/src/components/global/PrintButton.vue b/vue/src/components/global/PrintButton.vue new file mode 100644 index 00000000..f5dc1efe --- /dev/null +++ b/vue/src/components/global/PrintButton.vue @@ -0,0 +1,36 @@ + + + + + \ No newline at end of file diff --git a/vue/src/components/map/Map.vue b/vue/src/components/map/Map.vue index f20cb817..fc90ed1f 100644 --- a/vue/src/components/map/Map.vue +++ b/vue/src/components/map/Map.vue @@ -1,18 +1,23 @@ + + \ No newline at end of file diff --git a/vue/src/components/map/controls/ToggleSidebarControl.ts b/vue/src/components/map/controls/ToggleSidebarControl.ts deleted file mode 100644 index b5720cfa..00000000 --- a/vue/src/components/map/controls/ToggleSidebarControl.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useProjectStore } from "@/stores/projectStore"; -export default class ToggleSidebarControl { - _map: any - _container: any - - onAdd(map: any) { - this._map = map; - this._container = document.createElement('div'); - this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group'; - const btn = document.createElement("button"); - btn.className = 'maplibregl-ctrl-toggle-sidebar' - const span = document.createElement("span"); - span.className = 'maplibregl-ctrl-icon' - btn.appendChild(span) - this._container.appendChild(btn); - this._container.addEventListener('click', () => { - useProjectStore().isProjectMapFullWidth = !useProjectStore().isProjectMapFullWidth - btn.setAttribute('active', useProjectStore().isProjectMapFullWidth.toString()) - }) - - return this._container; - } - - onRemove() { - this._container.parentNode.removeChild(this._container); - this._map = undefined; - } -} diff --git a/vue/src/components/map/controls/ToggleSidebarControl.vue b/vue/src/components/map/controls/ToggleSidebarControl.vue new file mode 100644 index 00000000..2f7a5894 --- /dev/null +++ b/vue/src/components/map/controls/ToggleSidebarControl.vue @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/vue/src/components/text-elements/PageTitle.vue b/vue/src/components/text-elements/PageTitle.vue index 6837ffd3..58d85fb9 100644 --- a/vue/src/components/text-elements/PageTitle.vue +++ b/vue/src/components/text-elements/PageTitle.vue @@ -1,14 +1,15 @@ - \ No newline at end of file diff --git a/vue/src/composables/useDate.ts b/vue/src/composables/useDate.ts index 3312719e..bcd90ad2 100644 --- a/vue/src/composables/useDate.ts +++ b/vue/src/composables/useDate.ts @@ -1,9 +1,9 @@ import { ref } from "vue" -export function useDate(date: Date) { +export function useDate(date: Date, format: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' }) { const localeDate = ref('') - localeDate.value = date.toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) + localeDate.value = date.toLocaleDateString('fr-FR', format) return { localeDate } } \ No newline at end of file diff --git a/vue/src/models/interfaces/Project.ts b/vue/src/models/interfaces/Project.ts index dacec22e..8ad6d1e9 100644 --- a/vue/src/models/interfaces/Project.ts +++ b/vue/src/models/interfaces/Project.ts @@ -3,12 +3,17 @@ import type { Thematic } from "@/models/interfaces/Thematic"; import type { AdministrativeScope } from "@/models/enums/AdministrativeScope"; import type { Timestampable } from "@/models/interfaces/common/Timestampable"; import type { Status } from "@/models/enums/contents/Status"; +import type { User } from "@/models/interfaces/auth/User"; export interface Project extends Timestampable { id: number; name: string; + slug: string; + createdBy: User; location: string; coords: string; + calendar: string; + deliverables: string; status: Status; description: string; images: string[]; diff --git a/vue/src/router/index.ts b/vue/src/router/index.ts index b5bb8c32..42a70cdc 100644 --- a/vue/src/router/index.ts +++ b/vue/src/router/index.ts @@ -1,4 +1,4 @@ -import { createRouter, createWebHistory } from 'vue-router' +import { createRouter, createWebHistory, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'; import HomeView from '@/views/home/HomeView.vue' import { useApplicationStore } from '@/stores/applicationStore' import ActorProfile from '@/views/actors/components/ActorProfile.vue' @@ -8,9 +8,14 @@ import AdminComments from '@/views/admin/components/AdminComments.vue' import { useAdminStore } from '@/stores/adminStore' import { AdministrationPanels } from '@/models/enums/app/AdministrationPanels' import { DialogKey } from '@/models/enums/app/DialogKey' +import { useProjectStore } from '@/stores/projectStore' +import { onBeforeUnmount, onBeforeMount } from 'vue'; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), + scrollBehavior(to, from, savedPosition) { + return savedPosition ? savedPosition : { el: '#app', top: 0, behavior: 'smooth' } + }, routes: [ { path: '/', @@ -30,7 +35,17 @@ const router = createRouter({ { path: '/projects', name: 'projects', - component: () => import('@/views/projects/ProjectsView.vue') + component: () => import('@/views/projects/ProjectListView.vue') + }, + { + path: '/projects/:slug', + name: 'projectPage', + component: () => import('@/views/projects/ProjectSheetView.vue'), + beforeEnter: async (to, from, next) => { + const projectStore = useProjectStore() + await projectStore.loadProjectBySlug(to.params.slug) + next() + } }, { path: '/resources', diff --git a/vue/src/services/map/MapService.ts b/vue/src/services/map/MapService.ts index aec71461..a1c142bf 100644 --- a/vue/src/services/map/MapService.ts +++ b/vue/src/services/map/MapService.ts @@ -4,7 +4,7 @@ export default class MapService { static getGeojson(data: any[]): any { const geojson: any = []; - Array.prototype.forEach.call(data , (item: any) => { + Array.prototype.forEach.call(data, (item: any) => { geojson.push({ 'id': item.id, 'properties': { @@ -24,10 +24,31 @@ export default class MapService { }; } - static getBounds (features: any) { + static getBounds(features: any) { const coordinates = features.map((f: any) => f.geometry.coordinates) return coordinates.reduce((bounds: any, coord: any) => { - return bounds.extend(coord); + return bounds.extend(coord); }, new maplibregl.LngLatBounds(coordinates[0], coordinates[0])); } +} + +export class IControl { + _map: any + _container: any + control: any + + constructor(control: any) { + this.control = control + } + + onAdd(map: any) { + this._map = map + this._container = this.control.value.$el + return this._container + } + onRemove() { + if (this._container.parentNode == null) return + this._container.parentNode.removeChild(this._container); + this._map.value = undefined; + } } \ No newline at end of file diff --git a/vue/src/services/projects/ProjectService.ts b/vue/src/services/projects/ProjectService.ts index f04b7b9d..a80bd314 100644 --- a/vue/src/services/projects/ProjectService.ts +++ b/vue/src/services/projects/ProjectService.ts @@ -3,7 +3,17 @@ import type { Project } from "@/models/interfaces/Project"; export class ProjectService { static async getAll(): Promise { - return await apiClient.get('/api/projects') + return await apiClient.get('/api/projects/all') + .then((response) => response.data['hydra:member']) + } + + static async get(search: Partial): Promise { + return await apiClient.get('/api/projects', { params: search }) + .then((response) => response.data['hydra:member'][0]) + } + + static async getSimilarProjects(project: Project): Promise { + return await apiClient.get('/api/projects/' + project.id + '/similar') .then((response) => response.data['hydra:member']) } } \ No newline at end of file diff --git a/vue/src/stores/projectStore.ts b/vue/src/stores/projectStore.ts index 4aecc261..9e6d67c7 100644 --- a/vue/src/stores/projectStore.ts +++ b/vue/src/stores/projectStore.ts @@ -1,6 +1,6 @@ import { StoresList } from '@/models/enums/app/StoresList' import { defineStore } from 'pinia' -import { computed, reactive, ref, type Ref, type Reactive } from 'vue'; +import { computed, reactive, ref, type Ref, type Reactive, watch } from 'vue'; import type { Project } from '@/models/interfaces/Project' import { ProjectService } from '@/services/projects/ProjectService' import maplibregl from 'maplibre-gl'; @@ -12,6 +12,8 @@ import type { Actor } from '@/models/interfaces/Actor'; export const useProjectStore = defineStore(StoresList.PROJECTS, () => { const projects: Ref = ref([]) + const project: Ref = ref(null) + const similarProjects: Ref = ref([]) const hoveredProjectId: Ref = ref(null) const activeProjectId: Ref = ref(null) const map: Ref = ref(null) @@ -41,6 +43,24 @@ export const useProjectStore = defineStore(StoresList.PROJECTS, () => { } } + async function loadProjectBySlug(slug: string | string []): Promise { + if (project.value?.slug !== slug && typeof slug === 'string') { + project.value = await ProjectService.get({ slug }) + } + } + + async function loadSimilarProjects(): Promise { + if (project.value) { + similarProjects.value = await ProjectService.getSimilarProjects(project.value) + } + } + + watch(() => project, () => { + if (project.value == null) { + similarProjects.value = [] + } + }) + const hoveredProject = computed(() => { if (hoveredProjectId.value) { return projects.value.find((project) => project.id === hoveredProjectId.value) @@ -100,7 +120,7 @@ export const useProjectStore = defineStore(StoresList.PROJECTS, () => { } return { - projects, filters, isProjectMapFullWidth, isFilterModalShown, sortingProjectsSelectedMethod, hoveredProjectId, hoveredProject, activeProjectId, activeProject, filteredProjects, orderedProjects, map, - getAll, resetFilters + projects, project, similarProjects,filters, isProjectMapFullWidth, isFilterModalShown, sortingProjectsSelectedMethod, hoveredProjectId, hoveredProject, activeProjectId, activeProject, filteredProjects, orderedProjects, map, + getAll, resetFilters, loadProjectBySlug, loadSimilarProjects } }) diff --git a/vue/src/views/_layout/sheet/SheetContentBanner.vue b/vue/src/views/_layout/sheet/SheetContentBanner.vue new file mode 100644 index 00000000..0f11549e --- /dev/null +++ b/vue/src/views/_layout/sheet/SheetContentBanner.vue @@ -0,0 +1,70 @@ + + + + + \ No newline at end of file diff --git a/vue/src/views/_layout/sheet/UpdatedAtLabel.vue b/vue/src/views/_layout/sheet/UpdatedAtLabel.vue new file mode 100644 index 00000000..6abe8ba1 --- /dev/null +++ b/vue/src/views/_layout/sheet/UpdatedAtLabel.vue @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/vue/src/views/actors/ActorsView.vue b/vue/src/views/actors/ActorsView.vue index 7a1f571a..98f802aa 100644 --- a/vue/src/views/actors/ActorsView.vue +++ b/vue/src/views/actors/ActorsView.vue @@ -2,8 +2,7 @@
- - + {{ $t('actors.desc') }}
- +
{{ $t('actors.add') }} @@ -48,9 +47,9 @@ import Wip from '@/components/global/Wip.vue'; import ActorCard from '@/views/actors/components/ActorCard.vue'; import { useApplicationStore } from '@/stores/applicationStore'; import { useUserStore } from '@/stores/userStore'; -import { computed, onMounted, ref } from 'vue'; +import { computed, ref } from 'vue'; import { UserRoles } from '@/models/enums/auth/UserRoles'; -; + const appStore = useApplicationStore(); const actorsStore = useActorsStore(); const userStore = useUserStore(); diff --git a/vue/src/views/actors/components/ActorProfile.vue b/vue/src/views/actors/components/ActorProfile.vue index edc67d63..6578a361 100644 --- a/vue/src/views/actors/components/ActorProfile.vue +++ b/vue/src/views/actors/components/ActorProfile.vue @@ -3,10 +3,18 @@
- - + + -

{{actor.description}}

+

{{ actor.description }}

@@ -14,7 +22,7 @@
- + {{ actor.administrativeScopes.map(x => x.name).join(", ") }}
@@ -25,7 +33,7 @@
- + {{ actor.contactName }} {{ actor.contactPosition }}
@@ -40,7 +48,7 @@ import type { Actor } from '@/models/interfaces/Actor'; import { useActorsStore } from '@/stores/actorsStore'; import { computed, onMounted, watchEffect } from 'vue'; import { useRoute } from 'vue-router'; -import ContentBanner from '@/components/content/ContentBanner.vue'; +import SheetContentBanner from '@/views/_layout/sheet/SheetContentBanner.vue'; import SectionTitle from '@/components/text-elements/SectionTitle.vue'; import ContentDivider from '@/components/content/ContentDivider.vue'; import ActorRelatedContent from './ActorRelatedContent.vue'; @@ -74,6 +82,8 @@ function editActor(actor: Actor) { } \ No newline at end of file diff --git a/vue/src/views/projects/components/ProjectCard.vue b/vue/src/views/projects/components/ProjectCard.vue index 474cdef6..3b10f572 100644 --- a/vue/src/views/projects/components/ProjectCard.vue +++ b/vue/src/views/projects/components/ProjectCard.vue @@ -1,5 +1,5 @@