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
{
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(),

View file

@ -16,4 +16,9 @@ interface ElementRepository
* @return Element[]
*/
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 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);

View file

@ -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),
);
}
}

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);
$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,
));
}
}

View file

@ -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();

View file

@ -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<Element | null>(null)
const childElements = ref<Element[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
async function fetchElement(elementId: string): Promise<void> {
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 }
})

View file

@ -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 }}
</p>
<h1 v-else-if="element" class="element-page__heading">
{{ element.title }}
</h1>
<section v-else-if="element" class="element-page__content">
<h1 class="element-page__heading">
{{ 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>
</div>
</template>
@ -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;
}
}
</style>