diff --git a/ai/backend-context.md b/ai/backend-context.md index f3b5ddd..dd69678 100644 --- a/ai/backend-context.md +++ b/ai/backend-context.md @@ -35,6 +35,15 @@ intentionally omitted here - update this section as entities land. `new Set(...)`) instead of creating them through their fake repositories +## Migrations + +- This project is not in production. By default, schema changes should update + the relevant create-table migration so migrations describe the current desired + schema from scratch. +- Do not add alter-table or data-backfill migrations unless the user explicitly + asks for production-style migration safety. +- Put seed data in seeders, not migrations. + ## PHP rules - Imports: always put `use` statements at the top of the file, never use diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 2e44931..0e71cea 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -36,6 +36,7 @@ class ElementController $childElements[] = [ 'id' => $childElement->getId(), 'title' => $childElement->getTitle(), + 'description' => $childElement->getDescription(), ]; } @@ -44,6 +45,7 @@ class ElementController 'element' => [ 'id' => $element->getId(), 'title' => $element->getTitle(), + 'description' => $element->getDescription(), ], ], 200); } diff --git a/backend/app/Element/CreateElementDto.php b/backend/app/Element/CreateElementDto.php index b815300..3bad829 100644 --- a/backend/app/Element/CreateElementDto.php +++ b/backend/app/Element/CreateElementDto.php @@ -9,6 +9,7 @@ class CreateElementDto public function __construct( public Set $set, public string $title, + public string $description, public ?Element $parentElement, ) { } diff --git a/backend/app/Element/Element.php b/backend/app/Element/Element.php index 143df26..7f007ce 100644 --- a/backend/app/Element/Element.php +++ b/backend/app/Element/Element.php @@ -9,6 +9,7 @@ class Element public function __construct( private int $id, private string $title, + private string $description, private Set $set, private ?Element $parentElement, ) { @@ -24,6 +25,11 @@ class Element return $this->title; } + public function getDescription(): string + { + return $this->description; + } + public function getSet(): Set { return $this->set; diff --git a/backend/app/Element/ElementModel.php b/backend/app/Element/ElementModel.php index 119d287..4f79f3b 100644 --- a/backend/app/Element/ElementModel.php +++ b/backend/app/Element/ElementModel.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Model; * @property int $id * @property int $set_id * @property string $title + * @property string $description * @property int|null $parent_element_id * * @method static Builder|ElementModel newModelQuery() @@ -18,6 +19,7 @@ use Illuminate\Database\Eloquent\Model; * @method static Builder|ElementModel whereParentElementId($value) * @method static Builder|ElementModel whereSetId($value) * @method static Builder|ElementModel whereTitle($value) + * @method static Builder|ElementModel whereDescription($value) * * @mixin \Eloquent */ @@ -30,6 +32,7 @@ class ElementModel extends Model protected $fillable = [ 'set_id', 'title', + 'description', 'parent_element_id', ]; diff --git a/backend/app/Element/EloquentElementRepository.php b/backend/app/Element/EloquentElementRepository.php index 350e37a..7b7065a 100644 --- a/backend/app/Element/EloquentElementRepository.php +++ b/backend/app/Element/EloquentElementRepository.php @@ -17,12 +17,14 @@ class EloquentElementRepository implements ElementRepository $model = ElementModel::create([ 'set_id' => $dto->set->getId(), 'title' => $dto->title, + 'description' => $dto->description, 'parent_element_id' => $dto->parentElement?->getId(), ]); return new Element( id: $model->id, title: $dto->title, + description: $dto->description, set: $dto->set, parentElement: $dto->parentElement, ); @@ -102,6 +104,7 @@ class EloquentElementRepository implements ElementRepository return new Element( id: $model->id, title: $model->title, + description: $model->description, set: $set, parentElement: $parentElement, ); diff --git a/backend/app/Element/UseCases/CreateElement/CreateElement.php b/backend/app/Element/UseCases/CreateElement/CreateElement.php index 9b32cd7..1cb4b37 100644 --- a/backend/app/Element/UseCases/CreateElement/CreateElement.php +++ b/backend/app/Element/UseCases/CreateElement/CreateElement.php @@ -30,6 +30,7 @@ class CreateElement if ($request->title === null || $request->title === '') { throw new BadRequestException('title is required'); } + $description = $request->description ?? ''; $set = $this->setRepo->find($request->setId); if ($set === null) { @@ -44,6 +45,7 @@ class CreateElement return $this->elementRepo->create(new CreateElementDto( set: $set, title: $request->title, + description: $description, parentElement: null, )); } @@ -65,6 +67,7 @@ class CreateElement return $this->elementRepo->create(new CreateElementDto( set: $set, title: $request->title, + description: $description, parentElement: $parentElement, )); } diff --git a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php index eef7e7e..b028326 100644 --- a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php +++ b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php @@ -7,6 +7,7 @@ class CreateElementRequest public function __construct( public ?int $setId, public ?string $title, + public ?string $description, public ?int $parentElementId, ) { } diff --git a/backend/database/migrations/2026_05_24_000001_elements_table.php b/backend/database/migrations/2026_05_24_000001_elements_table.php index 377ba38..f09b3b0 100644 --- a/backend/database/migrations/2026_05_24_000001_elements_table.php +++ b/backend/database/migrations/2026_05_24_000001_elements_table.php @@ -12,6 +12,7 @@ return new class extends Migration $table->id(); $table->foreignId('set_id')->constrained('sets'); $table->string('title'); + $table->text('description')->default(''); $table->foreignId('parent_element_id') ->nullable() ->constrained('elements'); diff --git a/backend/database/seeders/ElementSeeder.php b/backend/database/seeders/ElementSeeder.php index 68ee7b9..b356df9 100644 --- a/backend/database/seeders/ElementSeeder.php +++ b/backend/database/seeders/ElementSeeder.php @@ -17,16 +17,20 @@ class ElementSeeder extends Seeder $rootElement = $elementRepository->create(new CreateElementDto( set: $baderechSet, title: $baderechSet->getName(), + description: $baderechSet->getDescription(), parentElement: null, )); $elementRepository->create(new CreateElementDto( set: $baderechSet, title: 'Avodah Foundations', + description: 'Core foundations for building a steady ' + . 'avodah practice.', parentElement: $rootElement, )); $elementRepository->create(new CreateElementDto( set: $baderechSet, title: 'Daily Practice', + description: 'Practical steps for consistent daily growth.', parentElement: $rootElement, )); } diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php index b16be7f..09acc9d 100644 --- a/backend/tests/Fakes/FakeElementRepository.php +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -20,6 +20,7 @@ class FakeElementRepository implements ElementRepository $element = new Element( id: $id, title: $dto->title, + description: $dto->description, set: $dto->set, parentElement: $dto->parentElement, ); @@ -95,6 +96,7 @@ class FakeElementRepository implements ElementRepository return new Element( id: $element->getId(), title: $element->getTitle(), + description: $element->getDescription(), set: $element->getSet(), parentElement: $parentElement, ); diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 40834f2..b4949e4 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -25,16 +25,19 @@ class ElementsEndpointTest extends TestCase $element = $elementRepository->create(new CreateElementDto( set: $set, title: 'Baderech HaAvodah', + description: 'A structured path for growth', parentElement: null, )); $firstChildElement = $elementRepository->create(new CreateElementDto( set: $set, title: 'Avodah Foundations', + description: 'Foundations for steady avodah', parentElement: $element, )); $secondChildElement = $elementRepository->create(new CreateElementDto( set: $set, title: 'Daily Practice', + description: 'Daily practices for growth', parentElement: $element, )); @@ -46,15 +49,18 @@ class ElementsEndpointTest extends TestCase [ 'id' => $firstChildElement->getId(), 'title' => 'Avodah Foundations', + 'description' => 'Foundations for steady avodah', ], [ 'id' => $secondChildElement->getId(), 'title' => 'Daily Practice', + 'description' => 'Daily practices for growth', ], ], 'element' => [ 'id' => $element->getId(), 'title' => 'Baderech HaAvodah', + 'description' => 'A structured path for growth', ], ]); } diff --git a/backend/tests/Feature/SetsEndpointTest.php b/backend/tests/Feature/SetsEndpointTest.php index 6b79a7a..a1c3ce9 100644 --- a/backend/tests/Feature/SetsEndpointTest.php +++ b/backend/tests/Feature/SetsEndpointTest.php @@ -31,6 +31,7 @@ class SetsEndpointTest extends TestCase new CreateElementDto( set: $baderechSet, title: $baderechSet->getName(), + description: $baderechSet->getDescription(), parentElement: null, ) ); diff --git a/backend/tests/Unit/Controllers/ElementControllerTest.php b/backend/tests/Unit/Controllers/ElementControllerTest.php index 20a18dc..0d6e0f2 100644 --- a/backend/tests/Unit/Controllers/ElementControllerTest.php +++ b/backend/tests/Unit/Controllers/ElementControllerTest.php @@ -26,15 +26,22 @@ class ElementControllerTest extends TestCase public function testShowReturnsElementPayload(): void { $set = $this->createSet(1, 'Baderech'); - $element = $this->createElement($set, 'Baderech HaAvodah', null); + $element = $this->createElement( + $set, + 'Baderech HaAvodah', + 'A structured path for growth', + null, + ); $firstChildElement = $this->createElement( $set, 'Avodah Foundations', + 'Foundations for steady avodah', $element, ); $secondChildElement = $this->createElement( $set, 'Daily Practice', + 'Daily practices for growth', $element, ); @@ -44,14 +51,20 @@ class ElementControllerTest extends TestCase $body = json_decode($response->getContent(), true); $this->assertSame($element->getId(), $body['element']['id']); $this->assertSame('Baderech HaAvodah', $body['element']['title']); + $this->assertSame( + 'A structured path for growth', + $body['element']['description'], + ); $this->assertSame([ [ 'id' => $firstChildElement->getId(), 'title' => 'Avodah Foundations', + 'description' => 'Foundations for steady avodah', ], [ 'id' => $secondChildElement->getId(), 'title' => 'Daily Practice', + 'description' => 'Daily practices for growth', ], ], $body['childElements']); } @@ -91,11 +104,13 @@ class ElementControllerTest extends TestCase private function createElement( DomainSet $set, string $title, + string $description, ?Element $parentElement, ): Element { return $this->elementRepo->create(new CreateElementDto( set: $set, title: $title, + description: $description, parentElement: $parentElement, )); } diff --git a/backend/tests/Unit/Element/ElementTest.php b/backend/tests/Unit/Element/ElementTest.php index 5d7ded2..3a0cdce 100644 --- a/backend/tests/Unit/Element/ElementTest.php +++ b/backend/tests/Unit/Element/ElementTest.php @@ -19,18 +19,24 @@ class ElementTest extends TestCase $rootElement = new Element( id: 1, title: 'Root', + description: 'Root description', set: $set, parentElement: null, ); $childElement = new Element( id: 2, title: 'Child', + description: 'Child description', set: $set, parentElement: $rootElement, ); $this->assertSame(2, $childElement->getId()); $this->assertSame('Child', $childElement->getTitle()); + $this->assertSame( + 'Child description', + $childElement->getDescription(), + ); $this->assertSame($set, $childElement->getSet()); $this->assertSame($rootElement, $childElement->getParentElement()); $this->assertNull($rootElement->getParentElement()); diff --git a/backend/tests/Unit/Element/UseCases/CreateElementTest.php b/backend/tests/Unit/Element/UseCases/CreateElementTest.php index 340f49e..cef6aa4 100644 --- a/backend/tests/Unit/Element/UseCases/CreateElementTest.php +++ b/backend/tests/Unit/Element/UseCases/CreateElementTest.php @@ -47,11 +47,13 @@ class CreateElementTest extends TestCase $element = $this->createElement->execute(new CreateElementRequest( setId: $set->getId(), title: 'Root', + description: 'Root description', parentElementId: null, )); $this->assertInstanceOf(Element::class, $element); $this->assertSame('Root', $element->getTitle()); + $this->assertSame('Root description', $element->getDescription()); $this->assertSame($set->getId(), $element->getSet()->getId()); $this->assertNull($element->getParentElement()); } @@ -63,6 +65,7 @@ class CreateElementTest extends TestCase new CreateElementRequest( setId: $set->getId(), title: 'Root', + description: 'Root description', parentElementId: null, ) ); @@ -71,17 +74,36 @@ class CreateElementTest extends TestCase new CreateElementRequest( setId: $set->getId(), title: 'Child', + description: 'Child description', parentElementId: $rootElement->getId(), ) ); $this->assertSame('Child', $childElement->getTitle()); + $this->assertSame( + 'Child description', + $childElement->getDescription(), + ); $this->assertSame( $rootElement->getId(), $childElement->getParentElement()->getId(), ); } + public function testCreatesElementWithBlankDescriptionWhenMissing(): void + { + $set = $this->createSet('Daily learning'); + + $element = $this->createElement->execute(new CreateElementRequest( + setId: $set->getId(), + title: 'Root', + description: null, + parentElementId: null, + )); + + $this->assertSame('', $element->getDescription()); + } + public function testThrowsWhenSetIdMissing(): void { $this->expectException(BadRequestException::class); @@ -90,6 +112,7 @@ class CreateElementTest extends TestCase $this->createElement->execute(new CreateElementRequest( setId: null, title: 'Root', + description: 'Root description', parentElementId: null, )); } @@ -102,6 +125,7 @@ class CreateElementTest extends TestCase $this->createElement->execute(new CreateElementRequest( setId: 1, title: null, + description: 'Root description', parentElementId: null, )); } @@ -114,6 +138,7 @@ class CreateElementTest extends TestCase $this->createElement->execute(new CreateElementRequest( setId: 99, title: 'Root', + description: 'Root description', parentElementId: null, )); } @@ -130,6 +155,7 @@ class CreateElementTest extends TestCase $this->createElement->execute(new CreateElementRequest( setId: $set->getId(), title: 'Child', + description: 'Child description', parentElementId: 99, )); } @@ -140,6 +166,7 @@ class CreateElementTest extends TestCase $this->createElement->execute(new CreateElementRequest( setId: $set->getId(), title: 'Root', + description: 'Root description', parentElementId: null, )); @@ -151,6 +178,7 @@ class CreateElementTest extends TestCase $this->createElement->execute(new CreateElementRequest( setId: $set->getId(), title: 'Another root', + description: 'Another root description', parentElementId: null, )); } @@ -163,6 +191,7 @@ class CreateElementTest extends TestCase new CreateElementRequest( setId: $parentSet->getId(), title: 'Parent root', + description: 'Parent root description', parentElementId: null, ) ); @@ -175,6 +204,7 @@ class CreateElementTest extends TestCase $this->createElement->execute(new CreateElementRequest( setId: $childSet->getId(), title: 'Invalid child', + description: 'Invalid child description', parentElementId: $parentElement->getId(), )); } diff --git a/backend/tests/Unit/Element/UseCases/GetElementTest.php b/backend/tests/Unit/Element/UseCases/GetElementTest.php index fc665b0..9e398f4 100644 --- a/backend/tests/Unit/Element/UseCases/GetElementTest.php +++ b/backend/tests/Unit/Element/UseCases/GetElementTest.php @@ -30,6 +30,7 @@ class GetElementTest extends TestCase $element = $this->createElement( $set, 'Baderech HaAvodah', + 'A structured path for growth', null, ); @@ -41,6 +42,10 @@ class GetElementTest extends TestCase $this->assertInstanceOf(Element::class, $foundElement); $this->assertSame($element->getId(), $foundElement->getId()); $this->assertSame('Baderech HaAvodah', $foundElement->getTitle()); + $this->assertSame( + 'A structured path for growth', + $foundElement->getDescription(), + ); } public function testReturnsDirectChildElements(): void @@ -49,32 +54,38 @@ class GetElementTest extends TestCase $parentElement = $this->createElement( $set, 'Baderech HaAvodah', + 'A structured path for growth', null, ); $firstChildElement = $this->createElement( $set, 'Avodah Foundations', + 'Foundations for steady avodah', $parentElement, ); $secondChildElement = $this->createElement( $set, 'Daily Practice', + 'Daily practices for growth', $parentElement, ); $this->createElement( $set, 'Nested Practice', + 'Nested description', $firstChildElement, ); $otherSet = $this->createSet(2, 'Daily Learning'); $otherParentElement = $this->createElement( $otherSet, 'Other Parent', + 'Other parent description', null, ); $this->createElement( $otherSet, 'Other Child', + 'Other child description', $otherParentElement, ); @@ -89,11 +100,19 @@ class GetElementTest extends TestCase $childElements[0]->getId(), ); $this->assertSame('Avodah Foundations', $childElements[0]->getTitle()); + $this->assertSame( + 'Foundations for steady avodah', + $childElements[0]->getDescription(), + ); $this->assertSame( $secondChildElement->getId(), $childElements[1]->getId(), ); $this->assertSame('Daily Practice', $childElements[1]->getTitle()); + $this->assertSame( + 'Daily practices for growth', + $childElements[1]->getDescription(), + ); } public function testThrowsWhenIdMissing(): void @@ -125,11 +144,13 @@ class GetElementTest extends TestCase private function createElement( DomainSet $set, string $title, + string $description, ?Element $parentElement, ): Element { return $this->elementRepo->create(new CreateElementDto( set: $set, title: $title, + description: $description, parentElement: $parentElement, )); } diff --git a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts index a260b94..832233d 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts @@ -43,9 +43,21 @@ describe('media page sets', () => { cy.contains('h1', 'Baderech HaAvodah').should('be.visible') cy.get('[data-cy="child-element-list"]').should('be.visible') cy.contains('[data-cy="child-element-link"]', 'Avodah Foundations') + .as('avodahFoundationsLink') .should('have.attr', 'href', '/element/2') + cy.get('@avodahFoundationsLink') + .should( + 'contain.text', + 'Core foundations for building a steady avodah practice.', + ) cy.contains('[data-cy="child-element-link"]', 'Daily Practice') + .as('dailyPracticeLink') .should('have.attr', 'href', '/element/3') + cy.get('@dailyPracticeLink') + .should( + 'contain.text', + 'Practical steps for consistent daily growth.', + ) cy.contains('[data-cy="child-element-link"]', 'Avodah Foundations') .click() diff --git a/frontend/rabbi_gerzi/src/stores/elements.ts b/frontend/rabbi_gerzi/src/stores/elements.ts index 6922157..b7ae058 100644 --- a/frontend/rabbi_gerzi/src/stores/elements.ts +++ b/frontend/rabbi_gerzi/src/stores/elements.ts @@ -4,6 +4,7 @@ import { defineStore } from 'pinia' export interface Element { id: number title: string + description: string } interface ElementResponse { diff --git a/frontend/rabbi_gerzi/src/views/ElementPage.vue b/frontend/rabbi_gerzi/src/views/ElementPage.vue index 39d0866..fd8a5fa 100644 --- a/frontend/rabbi_gerzi/src/views/ElementPage.vue +++ b/frontend/rabbi_gerzi/src/views/ElementPage.vue @@ -65,7 +65,15 @@ watch( class="element-page__child-link" data-cy="child-element-link" > - {{ childElement.title }} + + {{ childElement.title }} + + + {{ childElement.description }} + @@ -131,6 +139,19 @@ watch( transform 180ms ease; } +.element-page__child-title { + display: block; +} + +.element-page__child-description { + display: block; + margin-top: 0.4rem; + color: var(--color-text-muted); + font-family: var(--font-sans); + font-size: 0.95rem; + line-height: 1.45; +} + .element-page__child-link:hover, .element-page__child-link:focus-visible { border-color: #d4ad5f;