add element child list
This commit is contained in:
parent
aa746fe3f0
commit
7350d747f3
9 changed files with 186 additions and 10 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -16,4 +16,9 @@ interface ElementRepository
|
|||
* @return Element[]
|
||||
*/
|
||||
public function findBySet(DomainSet $set): array;
|
||||
|
||||
/**
|
||||
* @return Element[]
|
||||
*/
|
||||
public function findByParentElement(Element $parentElement): array;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
backend/app/Element/UseCases/GetElement/GetElementResult.php
Normal file
30
backend/app/Element/UseCases/GetElement/GetElementResult.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue