From 7350d747f3c308f25e582dd123d242090713c5d6 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Tue, 26 May 2026 20:16:22 +0300 Subject: [PATCH] add element child list --- backend/app/Controllers/ElementController.php | 12 ++- backend/app/Element/ElementRepository.php | 5 ++ .../app/Element/EloquentElementRepository.php | 19 +++++ .../UseCases/GetElement/GetElement.php | 9 +- .../UseCases/GetElement/GetElementResult.php | 30 +++++++ backend/database/seeders/ElementSeeder.php | 12 ++- backend/tests/Fakes/FakeElementRepository.php | 19 +++++ frontend/rabbi_gerzi/src/stores/elements.ts | 7 +- .../rabbi_gerzi/src/views/ElementPage.vue | 83 ++++++++++++++++++- 9 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 backend/app/Element/UseCases/GetElement/GetElementResult.php diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 62a6385..2e44931 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -17,7 +17,7 @@ class ElementController public function show(?int $id): JsonResponse { try { - $element = $this->getElement->execute( + $result = $this->getElement->execute( new GetElementRequest(id: $id) ); } catch (BadRequestException $exception) { @@ -30,7 +30,17 @@ class ElementController ], 404); } + $element = $result->getElement(); + $childElements = []; + foreach ($result->getChildElements() as $childElement) { + $childElements[] = [ + 'id' => $childElement->getId(), + 'title' => $childElement->getTitle(), + ]; + } + return new JsonResponse([ + 'childElements' => $childElements, 'element' => [ 'id' => $element->getId(), 'title' => $element->getTitle(), diff --git a/backend/app/Element/ElementRepository.php b/backend/app/Element/ElementRepository.php index 8ed13bd..3e89628 100644 --- a/backend/app/Element/ElementRepository.php +++ b/backend/app/Element/ElementRepository.php @@ -16,4 +16,9 @@ interface ElementRepository * @return Element[] */ public function findBySet(DomainSet $set): array; + + /** + * @return Element[] + */ + public function findByParentElement(Element $parentElement): array; } diff --git a/backend/app/Element/EloquentElementRepository.php b/backend/app/Element/EloquentElementRepository.php index 68452de..350e37a 100644 --- a/backend/app/Element/EloquentElementRepository.php +++ b/backend/app/Element/EloquentElementRepository.php @@ -61,6 +61,25 @@ class EloquentElementRepository implements ElementRepository return $elements; } + /** + * @return Element[] + */ + public function findByParentElement(Element $parentElement): array + { + $models = ElementModel::where( + 'parent_element_id', + $parentElement->getId(), + ) + ->orderBy('id') + ->get(); + $elements = []; + foreach ($models as $model) { + $elements[] = $this->toDomain($model); + } + + return $elements; + } + private function toDomain(ElementModel $model): Element { $set = $this->setRepo->find($model->set_id); diff --git a/backend/app/Element/UseCases/GetElement/GetElement.php b/backend/app/Element/UseCases/GetElement/GetElement.php index 2dc555e..7103e47 100644 --- a/backend/app/Element/UseCases/GetElement/GetElement.php +++ b/backend/app/Element/UseCases/GetElement/GetElement.php @@ -2,7 +2,6 @@ namespace App\Element\UseCases\GetElement; -use App\Element\Element; use App\Element\ElementRepository; use App\Exceptions\BadRequestException; use App\Exceptions\NotFoundException; @@ -17,7 +16,7 @@ class GetElement * @throws BadRequestException * @throws NotFoundException */ - public function execute(GetElementRequest $request): Element + public function execute(GetElementRequest $request): GetElementResult { if ($request->id === null) { throw new BadRequestException('id is required'); @@ -28,6 +27,10 @@ class GetElement throw new NotFoundException('Element not found'); } - return $element; + return new GetElementResult( + element: $element, + childElements: $this->elementRepository + ->findByParentElement($element), + ); } } diff --git a/backend/app/Element/UseCases/GetElement/GetElementResult.php b/backend/app/Element/UseCases/GetElement/GetElementResult.php new file mode 100644 index 0000000..ad5f51b --- /dev/null +++ b/backend/app/Element/UseCases/GetElement/GetElementResult.php @@ -0,0 +1,30 @@ +element; + } + + /** + * @return Element[] + */ + public function getChildElements(): array + { + return $this->childElements; + } +} diff --git a/backend/database/seeders/ElementSeeder.php b/backend/database/seeders/ElementSeeder.php index 974b2fb..68ee7b9 100644 --- a/backend/database/seeders/ElementSeeder.php +++ b/backend/database/seeders/ElementSeeder.php @@ -14,10 +14,20 @@ class ElementSeeder extends Seeder $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $baderechSet = $setRepository->find(1); - $elementRepository->create(new CreateElementDto( + $rootElement = $elementRepository->create(new CreateElementDto( set: $baderechSet, title: $baderechSet->getName(), parentElement: null, )); + $elementRepository->create(new CreateElementDto( + set: $baderechSet, + title: 'Avodah Foundations', + parentElement: $rootElement, + )); + $elementRepository->create(new CreateElementDto( + set: $baderechSet, + title: 'Daily Practice', + parentElement: $rootElement, + )); } } diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php index cbb4e13..b16be7f 100644 --- a/backend/tests/Fakes/FakeElementRepository.php +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -66,6 +66,25 @@ class FakeElementRepository implements ElementRepository return $elements; } + /** + * @return Element[] + */ + public function findByParentElement(Element $parentElement): array + { + $elements = []; + foreach ($this->elementsById as $element) { + $currentParentElement = $element->getParentElement(); + if ( + $currentParentElement !== null + && $currentParentElement->getId() === $parentElement->getId() + ) { + $elements[] = $this->cloneElement($element); + } + } + + return $elements; + } + private function cloneElement(Element $element): Element { $parentElement = $element->getParentElement(); diff --git a/frontend/rabbi_gerzi/src/stores/elements.ts b/frontend/rabbi_gerzi/src/stores/elements.ts index 0624751..6922157 100644 --- a/frontend/rabbi_gerzi/src/stores/elements.ts +++ b/frontend/rabbi_gerzi/src/stores/elements.ts @@ -8,17 +8,20 @@ export interface Element { interface ElementResponse { element: Element + childElements: Element[] } const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string export const useElementsStore = defineStore('elements', () => { const element = ref(null) + const childElements = ref([]) const isLoading = ref(false) const error = ref(null) async function fetchElement(elementId: string): Promise { element.value = null + childElements.value = [] error.value = null isLoading.value = true @@ -39,12 +42,14 @@ export const useElementsStore = defineStore('elements', () => { const data: ElementResponse = await response.json() element.value = data.element + childElements.value = data.childElements } catch { + childElements.value = [] error.value = 'Network error - could not load element' } finally { isLoading.value = false } } - return { element, isLoading, error, fetchElement } + return { element, childElements, isLoading, error, fetchElement } }) diff --git a/frontend/rabbi_gerzi/src/views/ElementPage.vue b/frontend/rabbi_gerzi/src/views/ElementPage.vue index c222cef..39d0866 100644 --- a/frontend/rabbi_gerzi/src/views/ElementPage.vue +++ b/frontend/rabbi_gerzi/src/views/ElementPage.vue @@ -7,7 +7,7 @@ import { useElementsStore } from '@/stores/elements' const route = useRoute() const elementsStore = useElementsStore() -const { element, isLoading, error } = storeToRefs(elementsStore) +const { element, childElements, isLoading, error } = storeToRefs(elementsStore) const elementId = computed(() => { const routeElementId = route.params.id @@ -44,9 +44,33 @@ watch( > {{ error }}

-

- {{ element.title }} -

+
+

+ {{ element.title }} +

+ + +
@@ -62,6 +86,11 @@ watch( padding: 5.1rem 4.15rem 5rem; } +.element-page__content { + max-width: 48rem; + margin: 0 auto; +} + .element-page__heading { margin: 0; color: #2c2c2c; @@ -72,6 +101,48 @@ watch( text-align: center; } +.element-page__children { + margin-top: 3rem; +} + +.element-page__child-list { + display: grid; + margin: 0; + padding: 0; + gap: 0.85rem; + list-style: none; +} + +.element-page__child-link { + display: block; + padding: 1rem 1.2rem; + background: var(--color-white); + border: 1px solid #e5cf9f; + border-radius: 8px; + color: #333333; + font-family: var(--font-serif); + font-size: 1.25rem; + line-height: 1.25; + text-align: center; + text-decoration: none; + transition: + border-color 180ms ease, + box-shadow 180ms ease, + transform 180ms ease; +} + +.element-page__child-link:hover, +.element-page__child-link:focus-visible { + border-color: #d4ad5f; + box-shadow: 0 10px 28px rgb(44 44 44 / 9%); + transform: translateY(-1px); +} + +.element-page__child-link:focus-visible { + outline: 3px solid rgb(212 173 95 / 45%); + outline-offset: 4px; +} + .element-page__status { max-width: 48rem; margin: 0 auto; @@ -92,5 +163,9 @@ watch( .element-page__main { padding: 3rem 1rem; } + + .element-page__children { + margin-top: 2rem; + } }