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;
+ }
}