From dec9451d7e55385353de7db75c99fcac016ee9e4 Mon Sep 17 00:00:00 2001 From: "Sakith B." Date: Sat, 30 Aug 2025 11:12:13 +0530 Subject: [PATCH 01/17] wip --- .../App/Controller/CollectionController.php | 35 +++++++++++++ .../src/routes/app/(reader)/+layout.svelte | 49 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/backend/src/Api/App/Controller/CollectionController.php b/backend/src/Api/App/Controller/CollectionController.php index 17c8a92..840d1dd 100644 --- a/backend/src/Api/App/Controller/CollectionController.php +++ b/backend/src/Api/App/Controller/CollectionController.php @@ -8,8 +8,10 @@ use Hyvor\Internal\Auth\AuthUser; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class CollectionController extends AbstractController @@ -58,5 +60,38 @@ public function getCollection(string $slug): JsonResponse ]); } + #[Route('/collections', methods: ['POST'])] + public function createCollection(Request $request): JsonResponse + { + $user = $this->getUser(); + if (!$user instanceof AuthUser) { + throw new AccessDeniedHttpException('Authentication required'); + } + + $data = json_decode($request->getContent(), true); + if (!is_array($data)) { + throw new BadRequestHttpException('Invalid JSON body'); + } + + $name = trim((string)($data['name'] ?? '')); + if ($name === '') { + throw new BadRequestHttpException('name is required'); + } + + $isPublic = false; + if (array_key_exists('is_public', $data)) { + if (!is_bool($data['is_public'])) { + throw new BadRequestHttpException('is_public must be boolean'); + } + $isPublic = $data['is_public']; + } + + $collection = $this->collectionService->createCollection($user->id, $name, $isPublic); + + return $this->json([ + 'collection' => new CollectionObject($collection, $user->id), + ]); + } + } \ No newline at end of file diff --git a/frontend/src/routes/app/(reader)/+layout.svelte b/frontend/src/routes/app/(reader)/+layout.svelte index 91fa835..e7720a3 100644 --- a/frontend/src/routes/app/(reader)/+layout.svelte +++ b/frontend/src/routes/app/(reader)/+layout.svelte @@ -24,6 +24,9 @@ let showAddPublicationModal = $state(false); let rssUrl = $state(''); let publicationTitle = $state(''); + let showCreateCollectionModal = $state(false); + let collectionName = $state(''); + let collectionIsPublic = $state(false); let selectedItem: Item | null = $state(null); let currentItemIndex = $derived( selectedItem ? $items.findIndex(item => item.id === selectedItem!.id) : -1 @@ -33,6 +36,22 @@ goto(`/app/${collection.slug}`); } + async function handleCreateCollection() { + const trimmed = collectionName.trim(); + if (!trimmed) return; + try { + const res = await api.post('/collections', { name: trimmed, is_public: collectionIsPublic }); + const created: Collection = res.collection; + $collections = [...$collections, created]; + showCreateCollectionModal = false; + collectionName = ''; + collectionIsPublic = false; + goto(`/app/${created.slug}`); + } catch (e) { + console.error('Failed to create collection', e); + } + } + function selectPublication(publication?: Publication) { if (publication) { goto(`/app/${$page.params.collection_slug}/${publication.slug}`); @@ -164,6 +183,9 @@ {collection.name} {/each} + { showCreateCollectionModal = true; showCollections = false; }}> + + Create collection + {/snippet} @@ -316,6 +338,33 @@ + + + + {#snippet footer()} + + {/snippet} + {#snippet footer()} From 71fd4142fddfb993b0eb28f15dbb5d0d839b45ad Mon Sep 17 00:00:00 2001 From: "Sakith B." Date: Wed, 3 Sep 2025 10:16:24 +0530 Subject: [PATCH 03/17] wip --- .../App/Controller/CollectionController.php | 25 ++++--------------- .../src/Api/App/Input/AddCollectionInput.php | 19 ++++++++++++++ .../src/routes/app/(reader)/+layout.svelte | 21 ++++++++++------ 3 files changed, 37 insertions(+), 28 deletions(-) create mode 100644 backend/src/Api/App/Input/AddCollectionInput.php diff --git a/backend/src/Api/App/Controller/CollectionController.php b/backend/src/Api/App/Controller/CollectionController.php index 840d1dd..2dc649b 100644 --- a/backend/src/Api/App/Controller/CollectionController.php +++ b/backend/src/Api/App/Controller/CollectionController.php @@ -8,11 +8,11 @@ use Hyvor\Internal\Auth\AuthUser; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use App\Api\App\Input\AddCollectionInput; class CollectionController extends AbstractController { @@ -61,30 +61,15 @@ public function getCollection(string $slug): JsonResponse } #[Route('/collections', methods: ['POST'])] - public function createCollection(Request $request): JsonResponse + public function createCollection(#[MapRequestPayload] AddCollectionInput $payload): JsonResponse { $user = $this->getUser(); if (!$user instanceof AuthUser) { throw new AccessDeniedHttpException('Authentication required'); } - $data = json_decode($request->getContent(), true); - if (!is_array($data)) { - throw new BadRequestHttpException('Invalid JSON body'); - } - - $name = trim((string)($data['name'] ?? '')); - if ($name === '') { - throw new BadRequestHttpException('name is required'); - } - - $isPublic = false; - if (array_key_exists('is_public', $data)) { - if (!is_bool($data['is_public'])) { - throw new BadRequestHttpException('is_public must be boolean'); - } - $isPublic = $data['is_public']; - } + $name = trim($payload->name); + $isPublic = $payload->is_public; $collection = $this->collectionService->createCollection($user->id, $name, $isPublic); diff --git a/backend/src/Api/App/Input/AddCollectionInput.php b/backend/src/Api/App/Input/AddCollectionInput.php new file mode 100644 index 0000000..6fc5c96 --- /dev/null +++ b/backend/src/Api/App/Input/AddCollectionInput.php @@ -0,0 +1,19 @@ + - + { showCreateCollectionModal = false; }} + on:confirm={handleCreateCollection} +> - - {#snippet footer()} - - {/snippet} From aaec74f816bdf64514732d4d14e47fc0ec919cb5 Mon Sep 17 00:00:00 2001 From: "Sakith B." Date: Wed, 3 Sep 2025 10:26:02 +0530 Subject: [PATCH 04/17] add serializer package --- backend/composer.json | 6 + backend/composer.lock | 385 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 390 insertions(+), 1 deletion(-) diff --git a/backend/composer.json b/backend/composer.json index 9e5fa94..9c3cd84 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -15,6 +15,8 @@ "doctrine/orm": "^3.5.0", "hyvor/internal": "^3.0.1", "nesbot/carbon": "^3.10.1", + "phpdocumentor/reflection-docblock": "^5.6", + "phpstan/phpdoc-parser": "^2.3", "runtime/frankenphp-symfony": "^0.2.0", "symfony/console": "7.2.*", "symfony/doctrine-messenger": "7.2.*", @@ -24,9 +26,13 @@ "symfony/http-client": "7.2.*", "symfony/lock": "7.2.*", "symfony/messenger": "7.2.*", + "symfony/property-access": "7.2.*", + "symfony/property-info": "7.2.*", "symfony/runtime": "7.2.*", "symfony/scheduler": "7.2.*", "symfony/security-bundle": "7.2.*", + "symfony/serializer": "7.2.*", + "symfony/serializer-pack": "^1.3", "symfony/uid": "7.2.*", "symfony/yaml": "7.2.*" }, diff --git a/backend/composer.lock b/backend/composer.lock index 25c1487..760309d 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c85d912520189a58e9dab9f90473a9fd", + "content-hash": "9f4d9c1521478da437679e4fecb46070", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -2418,6 +2418,228 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", + "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" + }, + "time": "2025-08-01T19:43:32+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "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/2.3.0" + }, + "time": "2025-08-30T15:50:23+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "10.1.16", @@ -7675,6 +7897,109 @@ ], "time": "2025-06-24T04:04:14+00:00" }, + { + "name": "symfony/serializer", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "5608b04d8daaf29432d76ecc618b0fac169c2dfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/5608b04d8daaf29432d76ecc618b0fac169c2dfb", + "reference": "5608b04d8daaf29432d76ecc618b0fac169c2dfb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php84": "^1.30" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<6.4", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/uid": "<6.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^7.2", + "symfony/error-handler": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.1.8", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-27T11:34:33+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.0", @@ -9096,6 +9421,64 @@ } ], "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" } ], "packages-dev": [ From c91dc6a0bef65c88454f34e1ac27e93e65a58416 Mon Sep 17 00:00:00 2001 From: "Sakith B." Date: Mon, 18 Aug 2025 14:33:31 +0530 Subject: [PATCH 05/17] ui --- .../src/routes/app/(reader)/+layout.svelte | 106 +++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/app/(reader)/+layout.svelte b/frontend/src/routes/app/(reader)/+layout.svelte index 9e2b077..acc9dcd 100644 --- a/frontend/src/routes/app/(reader)/+layout.svelte +++ b/frontend/src/routes/app/(reader)/+layout.svelte @@ -1,7 +1,8 @@