Compare commits

..

No commits in common. "f204f471d095441ad4b019b06c9870561761b159" and "d950fe55ddb9159dc92721059991f0d861746dae" have entirely different histories.

13 changed files with 25 additions and 323 deletions

View file

@ -17,7 +17,7 @@ class ElementController
public function show(?int $id): JsonResponse public function show(?int $id): JsonResponse
{ {
try { try {
$result = $this->getElement->execute( $element = $this->getElement->execute(
new GetElementRequest(id: $id) new GetElementRequest(id: $id)
); );
} catch (BadRequestException $exception) { } catch (BadRequestException $exception) {
@ -30,17 +30,7 @@ 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,9 +16,4 @@ 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,25 +61,6 @@ 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,6 +2,7 @@
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;
@ -16,7 +17,7 @@ class GetElement
* @throws BadRequestException * @throws BadRequestException
* @throws NotFoundException * @throws NotFoundException
*/ */
public function execute(GetElementRequest $request): GetElementResult public function execute(GetElementRequest $request): Element
{ {
if ($request->id === null) { if ($request->id === null) {
throw new BadRequestException('id is required'); throw new BadRequestException('id is required');
@ -27,10 +28,6 @@ class GetElement
throw new NotFoundException('Element not found'); throw new NotFoundException('Element not found');
} }
return new GetElementResult( return $element;
element: $element,
childElements: $this->elementRepository
->findByParentElement($element),
);
} }
} }

View file

@ -1,30 +0,0 @@
<?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,20 +14,10 @@ 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);
$rootElement = $elementRepository->create(new CreateElementDto( $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,25 +66,6 @@ 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

@ -27,31 +27,11 @@ class ElementsEndpointTest extends TestCase
title: 'Baderech HaAvodah', title: 'Baderech HaAvodah',
parentElement: null, parentElement: null,
)); ));
$firstChildElement = $elementRepository->create(new CreateElementDto(
set: $set,
title: 'Avodah Foundations',
parentElement: $element,
));
$secondChildElement = $elementRepository->create(new CreateElementDto(
set: $set,
title: 'Daily Practice',
parentElement: $element,
));
$response = $this->getJson("/api/elements/{$element->getId()}"); $response = $this->getJson("/api/elements/{$element->getId()}");
$response->assertOk(); $response->assertOk();
$response->assertExactJson([ $response->assertExactJson([
'childElements' => [
[
'id' => $firstChildElement->getId(),
'title' => 'Avodah Foundations',
],
[
'id' => $secondChildElement->getId(),
'title' => 'Daily Practice',
],
],
'element' => [ 'element' => [
'id' => $element->getId(), 'id' => $element->getId(),
'title' => 'Baderech HaAvodah', 'title' => 'Baderech HaAvodah',

View file

@ -25,18 +25,7 @@ class ElementControllerTest extends TestCase
public function testShowReturnsElementPayload(): void public function testShowReturnsElementPayload(): void
{ {
$set = $this->createSet(1, 'Baderech'); $element = $this->createElement('Baderech HaAvodah');
$element = $this->createElement($set, 'Baderech HaAvodah', null);
$firstChildElement = $this->createElement(
$set,
'Avodah Foundations',
$element,
);
$secondChildElement = $this->createElement(
$set,
'Daily Practice',
$element,
);
$response = $this->controller->show($element->getId()); $response = $this->controller->show($element->getId());
@ -44,16 +33,6 @@ class ElementControllerTest extends TestCase
$body = json_decode($response->getContent(), true); $body = json_decode($response->getContent(), true);
$this->assertSame($element->getId(), $body['element']['id']); $this->assertSame($element->getId(), $body['element']['id']);
$this->assertSame('Baderech HaAvodah', $body['element']['title']); $this->assertSame('Baderech HaAvodah', $body['element']['title']);
$this->assertSame([
[
'id' => $firstChildElement->getId(),
'title' => 'Avodah Foundations',
],
[
'id' => $secondChildElement->getId(),
'title' => 'Daily Practice',
],
], $body['childElements']);
} }
public function testShowReturns400WhenIdMissing(): void public function testShowReturns400WhenIdMissing(): void
@ -78,25 +57,19 @@ class ElementControllerTest extends TestCase
); );
} }
private function createSet(int $id, string $name): DomainSet private function createElement(string $title): Element
{ {
return new DomainSet( $set = new DomainSet(
id: $id, id: 1,
name: $name, name: 'Baderech',
description: "$name description", description: 'Baderech description',
iconImageUrl: '/assets/baderech-icon.png', iconImageUrl: '/assets/baderech-icon.png',
); );
}
private function createElement(
DomainSet $set,
string $title,
?Element $parentElement,
): Element {
return $this->elementRepo->create(new CreateElementDto( return $this->elementRepo->create(new CreateElementDto(
set: $set, set: $set,
title: $title, title: $title,
parentElement: $parentElement, parentElement: null,
)); ));
} }
} }

View file

@ -26,76 +26,17 @@ class GetElementTest extends TestCase
public function testReturnsElementWhenFound(): void public function testReturnsElementWhenFound(): void
{ {
$set = $this->createSet(1, 'Baderech'); $element = $this->createElement('Baderech HaAvodah');
$element = $this->createElement(
$set,
'Baderech HaAvodah',
null,
);
$result = $this->getElement->execute(new GetElementRequest( $foundElement = $this->getElement->execute(new GetElementRequest(
id: $element->getId(), id: $element->getId(),
)); ));
$foundElement = $result->getElement();
$this->assertInstanceOf(Element::class, $foundElement); $this->assertInstanceOf(Element::class, $foundElement);
$this->assertSame($element->getId(), $foundElement->getId()); $this->assertSame($element->getId(), $foundElement->getId());
$this->assertSame('Baderech HaAvodah', $foundElement->getTitle()); $this->assertSame('Baderech HaAvodah', $foundElement->getTitle());
} }
public function testReturnsDirectChildElements(): void
{
$set = $this->createSet(1, 'Baderech');
$parentElement = $this->createElement(
$set,
'Baderech HaAvodah',
null,
);
$firstChildElement = $this->createElement(
$set,
'Avodah Foundations',
$parentElement,
);
$secondChildElement = $this->createElement(
$set,
'Daily Practice',
$parentElement,
);
$this->createElement(
$set,
'Nested Practice',
$firstChildElement,
);
$otherSet = $this->createSet(2, 'Daily Learning');
$otherParentElement = $this->createElement(
$otherSet,
'Other Parent',
null,
);
$this->createElement(
$otherSet,
'Other Child',
$otherParentElement,
);
$result = $this->getElement->execute(new GetElementRequest(
id: $parentElement->getId(),
));
$childElements = $result->getChildElements();
$this->assertCount(2, $childElements);
$this->assertSame(
$firstChildElement->getId(),
$childElements[0]->getId(),
);
$this->assertSame('Avodah Foundations', $childElements[0]->getTitle());
$this->assertSame(
$secondChildElement->getId(),
$childElements[1]->getId(),
);
$this->assertSame('Daily Practice', $childElements[1]->getTitle());
}
public function testThrowsWhenIdMissing(): void public function testThrowsWhenIdMissing(): void
{ {
$this->expectException(BadRequestException::class); $this->expectException(BadRequestException::class);
@ -112,25 +53,19 @@ class GetElementTest extends TestCase
$this->getElement->execute(new GetElementRequest(id: 999)); $this->getElement->execute(new GetElementRequest(id: 999));
} }
private function createSet(int $id, string $name): DomainSet private function createElement(string $title): Element
{ {
return new DomainSet( $set = new DomainSet(
id: $id, id: 1,
name: $name, name: 'Baderech',
description: "$name description", description: 'Baderech description',
iconImageUrl: '/assets/baderech-icon.png', iconImageUrl: '/assets/baderech-icon.png',
); );
}
private function createElement(
DomainSet $set,
string $title,
?Element $parentElement,
): Element {
return $this->elementRepo->create(new CreateElementDto( return $this->elementRepo->create(new CreateElementDto(
set: $set, set: $set,
title: $title, title: $title,
parentElement: $parentElement, parentElement: null,
)); ));
} }
} }

View file

@ -41,15 +41,5 @@ describe('media page sets', () => {
cy.location('pathname').should('eq', '/element/1') cy.location('pathname').should('eq', '/element/1')
cy.get('[data-cy="element-page"]').should('be.visible') cy.get('[data-cy="element-page"]').should('be.visible')
cy.contains('h1', 'Baderech HaAvodah').should('be.visible') cy.contains('h1', 'Baderech HaAvodah').should('be.visible')
cy.get('[data-cy="child-element-list"]').should('be.visible')
cy.contains('[data-cy="child-element-link"]', 'Avodah Foundations')
.should('have.attr', 'href', '/element/2')
cy.contains('[data-cy="child-element-link"]', 'Daily Practice')
.should('have.attr', 'href', '/element/3')
cy.contains('[data-cy="child-element-link"]', 'Avodah Foundations')
.click()
cy.location('pathname').should('eq', '/element/2')
cy.contains('h1', 'Avodah Foundations').should('be.visible')
}) })
}) })

View file

@ -8,20 +8,17 @@ 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
@ -42,14 +39,12 @@ 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, childElements, isLoading, error, fetchElement } return { element, 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, childElements, isLoading, error } = storeToRefs(elementsStore) const { element, isLoading, error } = storeToRefs(elementsStore)
const elementId = computed(() => { const elementId = computed(() => {
const routeElementId = route.params.id const routeElementId = route.params.id
@ -44,33 +44,9 @@ watch(
> >
{{ error }} {{ error }}
</p> </p>
<section v-else-if="element" class="element-page__content"> <h1 v-else-if="element" class="element-page__heading">
<h1 class="element-page__heading"> {{ element.title }}
{{ element.title }} </h1>
</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>
@ -86,11 +62,6 @@ 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;
@ -101,48 +72,6 @@ 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;
@ -163,9 +92,5 @@ watch(
.element-page__main { .element-page__main {
padding: 3rem 1rem; padding: 3rem 1rem;
} }
.element-page__children {
margin-top: 2rem;
}
} }
</style> </style>