diff --git a/migrations/Version20240212183141.php b/migrations/Version20240212183141.php new file mode 100644 index 0000000..7b6157c --- /dev/null +++ b/migrations/Version20240212183141.php @@ -0,0 +1,57 @@ +addSql('CREATE TABLE footer_link (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, url VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, priority SMALLINT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE link_block (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, title VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE sub_link_block (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, url VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, link_block_id INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_6B58312447E2C5 ON sub_link_block (link_block_id)'); + $this->addSql('ALTER TABLE sub_link_block ADD CONSTRAINT FK_6B58312447E2C5 FOREIGN KEY (link_block_id) REFERENCES link_block (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE mascot_group ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE mascot_group ALTER directories TYPE JSON USING (directories::JSON)'); + $this->addSql('COMMENT ON COLUMN mascot_group.directories IS \'\''); + $this->addSql('ALTER TABLE rss__group ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE rss__result ADD created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL'); + $this->addSql('ALTER TABLE rss__result ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE rss__result ALTER seen_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE'); + $this->addSql('COMMENT ON COLUMN rss__result.seen_at IS \'\''); + $this->addSql('ALTER TABLE rss__search ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + } + + 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 sub_link_block DROP CONSTRAINT FK_6B58312447E2C5'); + $this->addSql('DROP TABLE footer_link'); + $this->addSql('DROP TABLE link_block'); + $this->addSql('DROP TABLE sub_link_block'); + $this->addSql('ALTER TABLE mascot_group ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE mascot_group ALTER directories TYPE TEXT'); + $this->addSql('COMMENT ON COLUMN mascot_group.directories IS \'(DC2Type:array)\''); + $this->addSql('ALTER TABLE rss__group ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE rss__search ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE rss__result DROP created_at'); + $this->addSql('ALTER TABLE rss__result ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE rss__result ALTER seen_at TYPE DATE'); + $this->addSql('COMMENT ON COLUMN rss__result.seen_at IS \'(DC2Type:date_immutable)\''); + } +} diff --git a/package-lock.json b/package-lock.json index 968c1fc..5484222 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "vue": "^3.4.15", - "vue-router": "^4.2.5" + "vue-router": "^4.2.5", + "vue-safe-teleport": "^0.1.2" }, "devDependencies": { "@rushstack/eslint-patch": "^1.3.3", @@ -4841,6 +4842,14 @@ "vue": "^3.2.0" } }, + "node_modules/vue-safe-teleport": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/vue-safe-teleport/-/vue-safe-teleport-0.1.2.tgz", + "integrity": "sha512-L6S/ALd5I7hXWi2T5HETHKCW9bku0hNx4ocIWRsk46h1IfNvjXxtLE9ECV4SDJFkcTAn3pMf9yxftBYLoZ3USQ==", + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/vue-template-compiler": { "version": "2.7.16", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", @@ -8177,6 +8186,12 @@ "@vue/devtools-api": "^6.5.0" } }, + "vue-safe-teleport": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/vue-safe-teleport/-/vue-safe-teleport-0.1.2.tgz", + "integrity": "sha512-L6S/ALd5I7hXWi2T5HETHKCW9bku0hNx4ocIWRsk46h1IfNvjXxtLE9ECV4SDJFkcTAn3pMf9yxftBYLoZ3USQ==", + "requires": {} + }, "vue-template-compiler": { "version": "2.7.16", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", diff --git a/package.json b/package.json index a203944..4353830 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "vue": "^3.4.15", - "vue-router": "^4.2.5" + "vue-router": "^4.2.5", + "vue-safe-teleport": "^0.1.2" }, "devDependencies": { "@rushstack/eslint-patch": "^1.3.3", diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php index ad30ca6..2f8683d 100644 --- a/src/Controller/Admin/DashboardController.php +++ b/src/Controller/Admin/DashboardController.php @@ -2,6 +2,8 @@ namespace App\Controller\Admin; +use App\Entity\FooterLink; +use App\Entity\LinkBlock; use App\Entity\MascotGroup; use App\Entity\Rss\Group; use App\Entity\Rss\Result; @@ -38,6 +40,11 @@ public function configureMenuItems(): iterable yield MenuItem::linkToCrud('Groups', 'fa fa-home', Group::class); yield MenuItem::linkToCrud('Results', 'fa fa-home', Result::class); yield MenuItem::linkToCrud('Searches', 'fa fa-home', Search::class); + + yield MenuItem::section(); + + yield MenuItem::linkToCrud('Link blocks', 'fa fa-home', LinkBlock::class); + yield MenuItem::linkToCrud('Footer links', 'fa fa-home', FooterLink::class); } public function configureCrud(): Crud diff --git a/src/Controller/Admin/FooterLinkCrudController.php b/src/Controller/Admin/FooterLinkCrudController.php new file mode 100644 index 0000000..68b0012 --- /dev/null +++ b/src/Controller/Admin/FooterLinkCrudController.php @@ -0,0 +1,29 @@ +hideOnForm(); + + yield TextField::new('url'); + yield TextField::new('title'); + yield NumberField::new('priority'); + } +} \ No newline at end of file diff --git a/src/Controller/Admin/LinkBlockCrudController.php b/src/Controller/Admin/LinkBlockCrudController.php new file mode 100644 index 0000000..f020adf --- /dev/null +++ b/src/Controller/Admin/LinkBlockCrudController.php @@ -0,0 +1,35 @@ +hideOnForm(); + + yield TextField::new('title'); + yield TextField::new('description'); + yield CollectionField::new('subLinkBlocks') + ->setEntryType(SubLinkBlockType::class) + ->setFormTypeOption('by_reference', false) + ->allowAdd() + ->allowDelete(); + } +} \ No newline at end of file diff --git a/src/Controller/ApiController.php b/src/Controller/ApiController.php index 36d83be..914c7cc 100644 --- a/src/Controller/ApiController.php +++ b/src/Controller/ApiController.php @@ -3,6 +3,8 @@ namespace App\Controller; use App\Entity\Rss\Result; +use App\Repository\FooterLinkRepository; +use App\Repository\LinkBlockRepository; use App\Repository\Rss\ResultRepository; use App\Service\MascotService; use App\Service\QBitTorrentService; @@ -68,4 +70,26 @@ public function getPageTitles( ): JsonResponse { return $this->json($service->getTitles()); } + + #[Route('/footer-links', name: 'footer_links', methods: ['GET'])] + public function getFooterLinks( + FooterLinkRepository $repository + ): JsonResponse { + return $this->json($repository->findBy([], ['priority' => 'DESC']), context: [ + AbstractNormalizer::GROUPS => [ + 'api', + ], + ]); + } + + #[Route('/link-blocks', name: 'link_blocks', methods: ['GET'])] + public function getLinkBlocks( + LinkBlockRepository $repository + ): JsonResponse { + return $this->json($repository->findAll(), context: [ + AbstractNormalizer::GROUPS => [ + 'api', + ], + ]); + } } diff --git a/src/Entity/FooterLink.php b/src/Entity/FooterLink.php new file mode 100644 index 0000000..38a2f64 --- /dev/null +++ b/src/Entity/FooterLink.php @@ -0,0 +1,72 @@ +id; + } + + public function getUrl(): string|null + { + return $this->url; + } + + public function setUrl( + string $url + ): static { + $this->url = $url; + + return $this; + } + + public function getTitle(): string|null + { + return $this->title; + } + + public function setTitle( + string $title + ): static { + $this->title = $title; + + return $this; + } + + public function getPriority(): int|null + { + return $this->priority; + } + + public function setPriority( + int $priority + ): static { + $this->priority = $priority; + + return $this; + } +} diff --git a/src/Entity/LinkBlock.php b/src/Entity/LinkBlock.php new file mode 100644 index 0000000..082960c --- /dev/null +++ b/src/Entity/LinkBlock.php @@ -0,0 +1,98 @@ +subLinkBlocks = new ArrayCollection(); + } + + public function getId(): int|null + { + return $this->id; + } + + public function getTitle(): string|null + { + return $this->title; + } + + public function setTitle( + string $title + ): static { + $this->title = $title; + + return $this; + } + + public function getDescription(): string|null + { + return $this->description; + } + + public function setDescription( + string|null $description + ): static { + $this->description = $description; + + return $this; + } + + /** + * @return Collection + */ + public function getSubLinkBlocks(): Collection + { + return $this->subLinkBlocks; + } + + public function addSubLinkBlock( + SubLinkBlock $subLinkBlock + ): static { + if (!$this->subLinkBlocks->contains($subLinkBlock)) { + $this->subLinkBlocks->add($subLinkBlock); + $subLinkBlock->setLinkBlock($this); + } + + return $this; + } + + public function removeSubLinkBlock( + SubLinkBlock $subLinkBlock + ): static { + if ($this->subLinkBlocks->removeElement($subLinkBlock)) { + // set the owning side to null (unless already changed) + if ($subLinkBlock->getLinkBlock() === $this) { + $subLinkBlock->setLinkBlock(null); + } + } + + return $this; + } +} diff --git a/src/Entity/Rss/Result.php b/src/Entity/Rss/Result.php index 36e73c6..3940a13 100644 --- a/src/Entity/Rss/Result.php +++ b/src/Entity/Rss/Result.php @@ -37,10 +37,15 @@ class Result #[Groups('api')] private Search|null $search; - #[ORM\Column(type: Types::DATETIME_IMMUTABLE, options: ['default' => 'NOW()'])] + #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] #[Groups('api')] private \DateTimeImmutable|null $createdAt = null; + public function __construct() + { + $this->createdAt = new \DateTimeImmutable(); + } + public function getId(): int|null { return $this->id; diff --git a/src/Entity/SubLinkBlock.php b/src/Entity/SubLinkBlock.php new file mode 100644 index 0000000..4f30c09 --- /dev/null +++ b/src/Entity/SubLinkBlock.php @@ -0,0 +1,72 @@ +id; + } + + public function getLinkBlock(): LinkBlock|null + { + return $this->linkBlock; + } + + public function setLinkBlock( + LinkBlock|null $linkBlock + ): static { + $this->linkBlock = $linkBlock; + + return $this; + } + + public function getUrl(): string|null + { + return $this->url; + } + + public function setUrl( + string $url + ): static { + $this->url = $url; + + return $this; + } + + public function getTitle(): string|null + { + return $this->title; + } + + public function setTitle( + string $title + ): static { + $this->title = $title; + + return $this; + } +} diff --git a/src/Form/SubLinkBlockType.php b/src/Form/SubLinkBlockType.php new file mode 100644 index 0000000..962ba42 --- /dev/null +++ b/src/Form/SubLinkBlockType.php @@ -0,0 +1,28 @@ +add('title', TextType::class, [ + 'required' => true, + ]) + ->add('url', TextType::class, [ + 'required' => true + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', SubLinkBlock::class); + } +} diff --git a/src/Repository/FooterLinkRepository.php b/src/Repository/FooterLinkRepository.php new file mode 100644 index 0000000..6bc8e60 --- /dev/null +++ b/src/Repository/FooterLinkRepository.php @@ -0,0 +1,24 @@ + + * + * @method FooterLink|null find($id, $lockMode = null, $lockVersion = null) + * @method FooterLink|null findOneBy(array $criteria, array $orderBy = null) + * @method FooterLink[] findAll() + * @method FooterLink[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class FooterLinkRepository extends ServiceEntityRepository +{ + public function __construct( + ManagerRegistry $registry + ) { + parent::__construct($registry, FooterLink::class); + } +} diff --git a/src/Repository/LinkBlockRepository.php b/src/Repository/LinkBlockRepository.php new file mode 100644 index 0000000..2432068 --- /dev/null +++ b/src/Repository/LinkBlockRepository.php @@ -0,0 +1,24 @@ + + * + * @method LinkBlock|null find($id, $lockMode = null, $lockVersion = null) + * @method LinkBlock|null findOneBy(array $criteria, array $orderBy = null) + * @method LinkBlock[] findAll() + * @method LinkBlock[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class LinkBlockRepository extends ServiceEntityRepository +{ + public function __construct( + ManagerRegistry $registry + ) { + parent::__construct($registry, LinkBlock::class); + } +} diff --git a/src/Repository/SubLinkBlockRepository.php b/src/Repository/SubLinkBlockRepository.php new file mode 100644 index 0000000..5d1b7e6 --- /dev/null +++ b/src/Repository/SubLinkBlockRepository.php @@ -0,0 +1,24 @@ + + * + * @method SubLinkBlock|null find($id, $lockMode = null, $lockVersion = null) + * @method SubLinkBlock|null findOneBy(array $criteria, array $orderBy = null) + * @method SubLinkBlock[] findAll() + * @method SubLinkBlock[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class SubLinkBlockRepository extends ServiceEntityRepository +{ + public function __construct( + ManagerRegistry $registry + ) { + parent::__construct($registry, SubLinkBlock::class); + } +} diff --git a/vue/App.vue b/vue/App.vue index 27af286..f1bc11e 100644 --- a/vue/App.vue +++ b/vue/App.vue @@ -2,19 +2,37 @@ import RssResultList from "./components/RssResultList.vue"; import PageTitle from "./components/PageTitle.vue"; import Mascot from "./components/Mascot.vue"; +import Footer from "./components/Footer.vue"; +import LinkBlockList from "./components/LinkBlockList.vue"; + + \ No newline at end of file diff --git a/vue/components/Footer.vue b/vue/components/Footer.vue new file mode 100644 index 0000000..9cf30f2 --- /dev/null +++ b/vue/components/Footer.vue @@ -0,0 +1,50 @@ + + + + + \ No newline at end of file diff --git a/vue/components/HeaderBlock.vue b/vue/components/HeaderBlock.vue index 1cfe42a..a5dcf03 100644 --- a/vue/components/HeaderBlock.vue +++ b/vue/components/HeaderBlock.vue @@ -1,5 +1,5 @@