From 596d984dccb31fe6e840d70726bd11a49ad891ef Mon Sep 17 00:00:00 2001 From: Pierre-Louis Date: Mon, 7 Oct 2024 16:38:04 +0200 Subject: [PATCH 01/16] feat(project-list): Add project list + map --- .github/workflows/deploy.yml | 2 +- symfony/fixtures/projects.yaml | 5 +- symfony/migrations/Version20241002093519.php | 42 +++ symfony/src/Entity/Actor.php | 2 + symfony/src/Entity/Project.php | 42 ++- symfony/src/Entity/Thematic.php | 2 + .../src/Entity/Trait/TimestampableEntity.php | 3 + vue/package.json | 2 +- vue/src/App.vue | 63 +++- vue/src/assets/images/icons/map/mdi-close.svg | 3 + ...age-filter-center-focus-strong-outline.svg | 6 + vue/src/assets/images/icons/map/mdi-minus.svg | 6 + vue/src/assets/images/icons/map/mdi-plus.svg | 6 + .../assets/images/icons/map/mdi-toggle.svg | 3 + .../assets/images/icons/map/project_icon.png | Bin 0 -> 2481 bytes .../images/icons/map/project_icon_hover.png | Bin 0 -> 2449 bytes vue/src/assets/plugins/i18n.ts | 3 +- vue/src/assets/plugins/vuetify.ts | 7 +- vue/src/assets/styles/global/app.scss | 7 +- .../styles/global/vuetifyOverrides.scss | 8 + vue/src/assets/translations/fr/common.json | 15 +- vue/src/assets/translations/fr/projects.json | 6 + .../components/generic-components/Chip.vue | 5 + .../generic-components/ChipList.vue | 32 ++ vue/src/components/generic-components/Map.vue | 346 ++++++++++++++++++ .../map-controls/ResetMapExtentControl.ts | 28 ++ .../map-controls/ToggleSidebarControl.ts | 28 ++ .../views-components/actors/ActorCard.vue | 2 + .../views-components/global/Card.vue | 98 ++--- .../views-components/global/LikeButton.vue | 4 +- .../views-components/header/HeaderDesktop.vue | 1 + .../views-components/projects/ProjectCard.vue | 84 ++++- .../views-components/projects/ProjectMap.vue | 123 +++++++ vue/src/composables/useDate.ts | 9 + vue/src/models/enums/SortKey.ts | 7 + vue/src/models/enums/StoresList.ts | 3 +- vue/src/models/interfaces/Project.ts | 5 +- .../models/interfaces/common/Timestampable.ts | 4 + vue/src/services/map/MapService.ts | 33 ++ vue/src/services/projects/ProjectService.ts | 9 + vue/src/stores/applicationStore.ts | 14 +- vue/src/stores/projectStore.ts | 62 ++++ vue/src/views/ProjectsView.vue | 158 +++++++- vue/yarn.lock | 180 ++++++--- 44 files changed, 1305 insertions(+), 163 deletions(-) create mode 100644 symfony/migrations/Version20241002093519.php create mode 100644 vue/src/assets/images/icons/map/mdi-close.svg create mode 100644 vue/src/assets/images/icons/map/mdi-image-filter-center-focus-strong-outline.svg create mode 100644 vue/src/assets/images/icons/map/mdi-minus.svg create mode 100644 vue/src/assets/images/icons/map/mdi-plus.svg create mode 100644 vue/src/assets/images/icons/map/mdi-toggle.svg create mode 100644 vue/src/assets/images/icons/map/project_icon.png create mode 100644 vue/src/assets/images/icons/map/project_icon_hover.png create mode 100644 vue/src/assets/translations/fr/projects.json create mode 100644 vue/src/components/generic-components/Chip.vue create mode 100644 vue/src/components/generic-components/ChipList.vue create mode 100644 vue/src/components/generic-components/Map.vue create mode 100644 vue/src/components/generic-components/map-controls/ResetMapExtentControl.ts create mode 100644 vue/src/components/generic-components/map-controls/ToggleSidebarControl.ts create mode 100644 vue/src/components/views-components/projects/ProjectMap.vue create mode 100644 vue/src/composables/useDate.ts create mode 100644 vue/src/models/enums/SortKey.ts create mode 100644 vue/src/models/interfaces/common/Timestampable.ts create mode 100644 vue/src/services/map/MapService.ts create mode 100644 vue/src/services/projects/ProjectService.ts create mode 100644 vue/src/stores/projectStore.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e45f3303..9d773483 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -22,4 +22,4 @@ jobs: - name: Deploy to Server depending on environment run: | ssh -o StrictHostKeyChecking=no -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \ - "cd ${{ secrets.REMOTE_PATH }} && git fetch origin && git reset --hard origin/${{ github.ref_name }} && make deploy" \ No newline at end of file + "cd ${{ secrets.REMOTE_PATH }} && git fetch origin && git reset --hard origin/${{ github.ref_name }} && make deploy && make run-fixtures" \ No newline at end of file diff --git a/symfony/fixtures/projects.yaml b/symfony/fixtures/projects.yaml index 4f721c60..58944902 100644 --- a/symfony/fixtures/projects.yaml +++ b/symfony/fixtures/projects.yaml @@ -11,9 +11,10 @@ App\Entity\Thematic: App\Entity\Project: project_{1..500}: - title: Urban Development + name: Urban Development location: - coords: 'POINT( )' + # coords: 'POINT( )' + coords: 'POINT( )' status: ')>' # Remplace par les valeurs de ton Enum Status description: images: [] diff --git a/symfony/migrations/Version20241002093519.php b/symfony/migrations/Version20241002093519.php new file mode 100644 index 00000000..3204f1c1 --- /dev/null +++ b/symfony/migrations/Version20241002093519.php @@ -0,0 +1,42 @@ +addSql('ALTER TABLE project ALTER updated_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE'); + $this->addSql('ALTER TABLE project ALTER updated_at SET NOT NULL'); + $this->addSql('ALTER TABLE project ALTER created_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE'); + $this->addSql('ALTER TABLE project RENAME COLUMN title TO name'); + $this->addSql('COMMENT ON COLUMN project.updated_at IS NULL'); + $this->addSql('COMMENT ON COLUMN project.created_at IS NULL'); + } + + 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('ALTER TABLE project ALTER created_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE'); + $this->addSql('ALTER TABLE project ALTER updated_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE'); + $this->addSql('ALTER TABLE project ALTER updated_at DROP NOT NULL'); + $this->addSql('ALTER TABLE project RENAME COLUMN name TO title'); + $this->addSql('COMMENT ON COLUMN project.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN project.updated_at IS \'(DC2Type:datetime_immutable)\''); + } +} diff --git a/symfony/src/Entity/Actor.php b/symfony/src/Entity/Actor.php index 96e0a759..d780e998 100644 --- a/symfony/src/Entity/Actor.php +++ b/symfony/src/Entity/Actor.php @@ -7,6 +7,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Attribute\Groups; #[ORM\Entity(repositoryClass: ActorRepository::class)] #[ApiResource] @@ -18,6 +19,7 @@ class Actor private ?int $id = null; #[ORM\Column(length: 255)] + #[Groups([Project::PROJECT_READ_ALL])] private ?string $name = null; /** diff --git a/symfony/src/Entity/Project.php b/symfony/src/Entity/Project.php index 32ced84e..ce87d7e6 100644 --- a/symfony/src/Entity/Project.php +++ b/symfony/src/Entity/Project.php @@ -3,6 +3,7 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; use App\Entity\Trait\TimestampableEntity; use App\Enum\AdminLevel; use App\Enum\Status; @@ -12,28 +13,43 @@ use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Jsor\Doctrine\PostGIS\Types\PostGISType; +use Symfony\Component\Serializer\Attribute\Groups; #[ORM\Entity(repositoryClass: ProjectRepository::class)] -#[ApiResource] +#[ApiResource( + paginationEnabled: false, + operations: [ + new GetCollection( + normalizationContext: ['groups' => [self::PROJECT_READ_ALL]] + ) + ] +)] class Project { use TimestampableEntity; + public const PROJECT_READ = 'project:read'; + public const PROJECT_READ_ALL = 'project:read:all'; + #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] + #[Groups([self::PROJECT_READ_ALL])] private ?int $id = null; #[ORM\Column(length: 255)] - private ?string $title = null; + #[Groups([self::PROJECT_READ_ALL])] + private ?string $name = null; #[ORM\Column(length: 255)] + #[Groups([self::PROJECT_READ_ALL])] private ?string $location = null; #[ORM\Column( type: PostGISType::GEOMETRY, options: ['geometry_type' => 'POINT'], )] + #[Groups([self::PROJECT_READ_ALL])] private ?string $coords = null; #[ORM\Column(enumType: Status::class)] @@ -55,6 +71,7 @@ class Project * @var Collection */ #[ORM\ManyToMany(targetEntity: Thematic::class, inversedBy: 'projects')] + #[Groups([self::PROJECT_READ_ALL])] private Collection $thematics; #[ORM\Column(length: 255, nullable: true)] @@ -76,6 +93,7 @@ class Project private ?string $website = null; #[ORM\Column(length: 255, nullable: true)] + #[Groups([self::PROJECT_READ_ALL])] private ?string $logo = null; /** @@ -95,6 +113,7 @@ class Project #[ORM\ManyToOne(inversedBy: 'projects')] #[ORM\JoinColumn(nullable: false)] + #[Groups([self::PROJECT_READ_ALL])] private ?Actor $actor = null; public function __construct() @@ -109,14 +128,14 @@ public function getId(): ?int return $this->id; } - public function getTitle(): ?string + public function getName(): ?string { - return $this->title; + return $this->name; } - public function setTitle(string $title): static + public function setName(string $name): static { - $this->title = $title; + $this->name = $name; return $this; } @@ -133,9 +152,14 @@ public function setLocation(string $location): static return $this; } - public function getCoords(): ?string - { - return $this->coords; + public function getCoords(): ?array { + if (preg_match('/POINT\(([-\d\.]+) ([-\d\.]+)\)/', $this->coords, $matches)) { + return [ + 'lat' => (float)$matches[1], + 'long' => (float)$matches[2], + ]; + } + return null; } public function setCoords(string $coords): static diff --git a/symfony/src/Entity/Thematic.php b/symfony/src/Entity/Thematic.php index 8fe9dc31..41c74a45 100644 --- a/symfony/src/Entity/Thematic.php +++ b/symfony/src/Entity/Thematic.php @@ -7,6 +7,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Attribute\Groups; #[ORM\Entity(repositoryClass: ThematicRepository::class)] #[ApiResource()] @@ -18,6 +19,7 @@ class Thematic private ?int $id = null; #[ORM\Column(length: 255)] + #[Groups([Project::PROJECT_READ_ALL])] private ?string $name = null; /** diff --git a/symfony/src/Entity/Trait/TimestampableEntity.php b/symfony/src/Entity/Trait/TimestampableEntity.php index acf13441..522e318f 100644 --- a/symfony/src/Entity/Trait/TimestampableEntity.php +++ b/symfony/src/Entity/Trait/TimestampableEntity.php @@ -2,9 +2,11 @@ namespace App\Entity\Trait; +use App\Entity\Project; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; +use Symfony\Component\Serializer\Attribute\Groups; trait TimestampableEntity { @@ -14,6 +16,7 @@ trait TimestampableEntity #[Gedmo\Timestampable(on: 'update')] #[ORM\Column(type: Types::DATETIME_MUTABLE)] + #[Groups([Project::PROJECT_READ_ALL])] protected ?\DateTimeInterface $updatedAt; public function getCreatedAt(): ?\DateTimeInterface diff --git a/vue/package.json b/vue/package.json index e3750050..1b80326c 100644 --- a/vue/package.json +++ b/vue/package.json @@ -20,7 +20,7 @@ "maplibre-gl": "^4.5.2", "pinia": "^2.1.7", "vee-validate": "^4.13.2", - "vue": "^3.4.29", + "vue": "^3.5.10", "vue-i18n": "9", "vue-router": "^4.3.3", "vuetify": "^3.7.0", diff --git a/vue/src/App.vue b/vue/src/App.vue index 3778ab66..ca9f5b12 100644 --- a/vue/src/App.vue +++ b/vue/src/App.vue @@ -1,22 +1,23 @@ + \ No newline at end of file diff --git a/vue/src/components/generic-components/Map.vue b/vue/src/components/generic-components/Map.vue new file mode 100644 index 00000000..a3b3e7a9 --- /dev/null +++ b/vue/src/components/generic-components/Map.vue @@ -0,0 +1,346 @@ + + + + \ No newline at end of file diff --git a/vue/src/components/generic-components/map-controls/ResetMapExtentControl.ts b/vue/src/components/generic-components/map-controls/ResetMapExtentControl.ts new file mode 100644 index 00000000..a826f026 --- /dev/null +++ b/vue/src/components/generic-components/map-controls/ResetMapExtentControl.ts @@ -0,0 +1,28 @@ +import { useApplicationStore } from "@/stores/applicationStore"; + +export default class ResetMapExtentControl { + _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-zoom-extent' + const span = document.createElement("span"); + span.className = 'maplibregl-ctrl-icon' + btn.appendChild(span) + this._container.appendChild(btn); + this._container.addEventListener('click', () => { + useApplicationStore().triggerZoomReset = !useApplicationStore().triggerZoomReset + }) + + return this._container; + } + + onRemove() { + this._container.parentNode.removeChild(this._container); + this._map = undefined; + } +} diff --git a/vue/src/components/generic-components/map-controls/ToggleSidebarControl.ts b/vue/src/components/generic-components/map-controls/ToggleSidebarControl.ts new file mode 100644 index 00000000..b0a29935 --- /dev/null +++ b/vue/src/components/generic-components/map-controls/ToggleSidebarControl.ts @@ -0,0 +1,28 @@ +import { useApplicationStore } from "@/stores/applicationStore"; +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', () => { + useApplicationStore().isProjectMapFullWidth = !useApplicationStore().isProjectMapFullWidth + btn.setAttribute('active', useApplicationStore().isProjectMapFullWidth.toString()) + }) + + return this._container; + } + + onRemove() { + this._container.parentNode.removeChild(this._container); + this._map = undefined; + } +} diff --git a/vue/src/components/views-components/actors/ActorCard.vue b/vue/src/components/views-components/actors/ActorCard.vue index bce8c200..06c9561a 100644 --- a/vue/src/components/views-components/actors/ActorCard.vue +++ b/vue/src/components/views-components/actors/ActorCard.vue @@ -28,6 +28,7 @@ import LikeButton from '@/components/views-components/global/LikeButton.vue'; defineProps<{ actor: Actor; }>(); + const getActorUrl = (name: string) => { return `/actors/${name}` } @@ -35,6 +36,7 @@ const getActorUrl = (name: string) => { \ No newline at end of file diff --git a/vue/src/components/views-components/global/LikeButton.vue b/vue/src/components/views-components/global/LikeButton.vue index e8941b60..cd90fa3f 100644 --- a/vue/src/components/views-components/global/LikeButton.vue +++ b/vue/src/components/views-components/global/LikeButton.vue @@ -1,7 +1,7 @@ diff --git a/vue/src/components/views-components/header/HeaderDesktop.vue b/vue/src/components/views-components/header/HeaderDesktop.vue index 126c7dc9..83f3df2b 100644 --- a/vue/src/components/views-components/header/HeaderDesktop.vue +++ b/vue/src/components/views-components/header/HeaderDesktop.vue @@ -99,6 +99,7 @@ const appStore = useApplicationStore(); &--left { .Header__appLogo { z-index: 10; + position: relative; height: $dim-logo; transform: translateY(-$dim-banner) } diff --git a/vue/src/components/views-components/projects/ProjectCard.vue b/vue/src/components/views-components/projects/ProjectCard.vue index a2e20bf9..e33a4678 100644 --- a/vue/src/components/views-components/projects/ProjectCard.vue +++ b/vue/src/components/views-components/projects/ProjectCard.vue @@ -1,22 +1,27 @@