add element child list

This commit is contained in:
Yisroel Baum 2026-05-26 20:16:22 +03:00
parent aa746fe3f0
commit 7350d747f3
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
9 changed files with 186 additions and 10 deletions

View file

@ -17,7 +17,7 @@ class ElementController
public function show(?int $id): JsonResponse public function show(?int $id): JsonResponse
{ {
try { try {
$element = $this->getElement->execute( $result = $this->getElement->execute(
new GetElementRequest(id: $id) new GetElementRequest(id: $id)
); );
} catch (BadRequestException $exception) { } catch (BadRequestException $exception) {
@ -30,7 +30,17 @@ class ElementController
], 404); ], 404);
} }
$element = $result->getElement();
$childElements = [];
foreach ($result->getChildElements() as $childElement) {
$childElements[] = [
'id' => $childElement->getId(),
'title' => $childElement->getTitle(),
];
}
return new JsonResponse([ return new JsonResponse([
'childElements' => $childElements,
'element' => [ 'element' => [
'id' => $element->getId(), 'id' => $element->getId(),
'title' => $element->getTitle(), 'title' => $element->getTitle(),

View file

@ -16,4 +16,9 @@ interface ElementRepository
* @return Element[] * @return Element[]
*/ */
public function findBySet(DomainSet $set): array; public function findBySet(DomainSet $set): array;
/**
* @return Element[]
*/
public function findByParentElement(Element $parentElement): array;
} }

View file

@ -61,6 +61,25 @@ class EloquentElementRepository implements ElementRepository
return $elements; 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 private function toDomain(ElementModel $model): Element
{ {
$set = $this->setRepo->find($model->set_id); $set = $this->setRepo->find($model->set_id);

View file

@ -2,7 +2,6 @@
namespace App\Element\UseCases\GetElement; namespace App\Element\UseCases\GetElement;
use App\Element\Element;
use App\Element\ElementRepository; use App\Element\ElementRepository;
use App\Exceptions\BadRequestException; use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException; use App\Exceptions\NotFoundException;
@ -17,7 +16,7 @@ class GetElement
* @throws BadRequestException * @throws BadRequestException
* @throws NotFoundException * @throws NotFoundException
*/ */
public function execute(GetElementRequest $request): Element public function execute(GetElementRequest $request): GetElementResult
{ {
if ($request->id === null) { if ($request->id === null) {
throw new BadRequestException('id is required'); throw new BadRequestException('id is required');
@ -28,6 +27,10 @@ class GetElement
throw new NotFoundException('Element not found'); throw new NotFoundException('Element not found');
} }
return $element; return new GetElementResult(
element: $element,
childElements: $this->elementRepository
->findByParentElement($element),
);
} }
} }

View file

@ -0,0 +1,30 @@
<?php
namespace App\Element\UseCases\GetElement;
use App\Element\Element;
class GetElementResult
{
/**
* @param Element[] $childElements
*/
public function __construct(
private Element $element,
private array $childElements,
) {
}
public function getElement(): Element
{
return $this->element;
}
/**
* @return Element[]
*/
public function getChildElements(): array
{
return $this->childElements;
}
}

View file

@ -14,10 +14,20 @@ class ElementSeeder extends Seeder
$setRepository = app(SetRepository::class); $setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class); $elementRepository = app(ElementRepository::class);
$baderechSet = $setRepository->find(1); $baderechSet = $setRepository->find(1);
$elementRepository->create(new CreateElementDto( $rootElement = $elementRepository->create(new CreateElementDto(
set: $baderechSet, set: $baderechSet,
title: $baderechSet->getName(), title: $baderechSet->getName(),
parentElement: null, parentElement: null,
)); ));
$elementRepository->create(new CreateElementDto(
set: $baderechSet,
title: 'Avodah Foundations',
parentElement: $rootElement,
));
$elementRepository->create(new CreateElementDto(
set: $baderechSet,
title: 'Daily Practice',
parentElement: $rootElement,
));
} }
} }

View file

@ -66,6 +66,25 @@ class FakeElementRepository implements ElementRepository
return $elements; 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 private function cloneElement(Element $element): Element
{ {
$parentElement = $element->getParentElement(); $parentElement = $element->getParentElement();

View file

@ -8,17 +8,20 @@ export interface Element {
interface ElementResponse { interface ElementResponse {
element: Element element: Element
childElements: Element[]
} }
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string
export const useElementsStore = defineStore('elements', () => { export const useElementsStore = defineStore('elements', () => {
const element = ref<Element | null>(null) const element = ref<Element | null>(null)
const childElements = ref<Element[]>([])
const isLoading = ref(false) const isLoading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
async function fetchElement(elementId: string): Promise<void> { async function fetchElement(elementId: string): Promise<void> {
element.value = null element.value = null
childElements.value = []
error.value = null error.value = null
isLoading.value = true isLoading.value = true
@ -39,12 +42,14 @@ export const useElementsStore = defineStore('elements', () => {
const data: ElementResponse = await response.json() const data: ElementResponse = await response.json()
element.value = data.element element.value = data.element
childElements.value = data.childElements
} catch { } catch {
childElements.value = []
error.value = 'Network error - could not load element' error.value = 'Network error - could not load element'
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
} }
return { element, isLoading, error, fetchElement } return { element, childElements, isLoading, error, fetchElement }
}) })

View file

@ -7,7 +7,7 @@ import { useElementsStore } from '@/stores/elements'
const route = useRoute() const route = useRoute()
const elementsStore = useElementsStore() const elementsStore = useElementsStore()
const { element, isLoading, error } = storeToRefs(elementsStore) const { element, childElements, isLoading, error } = storeToRefs(elementsStore)
const elementId = computed(() => { const elementId = computed(() => {
const routeElementId = route.params.id const routeElementId = route.params.id
@ -44,9 +44,33 @@ watch(
> >
{{ error }} {{ error }}
</p> </p>
<h1 v-else-if="element" class="element-page__heading"> <section v-else-if="element" class="element-page__content">
{{ element.title }} <h1 class="element-page__heading">
</h1> {{ element.title }}
</h1>
<nav
v-if="childElements.length > 0"
class="element-page__children"
aria-label="Child elements"
>
<ul class="element-page__child-list" data-cy="child-element-list">
<li
v-for="childElement in childElements"
:key="childElement.id"
class="element-page__child-item"
>
<RouterLink
:to="{ name: 'element', params: { id: childElement.id } }"
class="element-page__child-link"
data-cy="child-element-link"
>
{{ childElement.title }}
</RouterLink>
</li>
</ul>
</nav>
</section>
</main> </main>
</div> </div>
</template> </template>
@ -62,6 +86,11 @@ watch(
padding: 5.1rem 4.15rem 5rem; padding: 5.1rem 4.15rem 5rem;
} }
.element-page__content {
max-width: 48rem;
margin: 0 auto;
}
.element-page__heading { .element-page__heading {
margin: 0; margin: 0;
color: #2c2c2c; color: #2c2c2c;
@ -72,6 +101,48 @@ watch(
text-align: center; 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 { .element-page__status {
max-width: 48rem; max-width: 48rem;
margin: 0 auto; margin: 0 auto;
@ -92,5 +163,9 @@ watch(
.element-page__main { .element-page__main {
padding: 3rem 1rem; padding: 3rem 1rem;
} }
.element-page__children {
margin-top: 2rem;
}
} }
</style> </style>