Merge branch 'feature/element-sibling-navigation'
This commit is contained in:
commit
14de828b4b
18 changed files with 684 additions and 65 deletions
|
|
@ -46,18 +46,15 @@ class ElementController
|
||||||
}
|
}
|
||||||
|
|
||||||
$element = $result->getElement();
|
$element = $result->getElement();
|
||||||
$childElements = [];
|
|
||||||
foreach ($result->getChildElements() as $childElement) {
|
|
||||||
$childElements[] = [
|
|
||||||
'id' => $childElement->getId(),
|
|
||||||
'title' => $childElement->getTitle(),
|
|
||||||
'description' => $childElement->getDescription(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'childElements' => $childElements,
|
'childElements' => $this->buildElementSummaryPayloads(
|
||||||
|
$result->getChildElements(),
|
||||||
|
),
|
||||||
'element' => $this->buildElementPayload($element),
|
'element' => $this->buildElementPayload($element),
|
||||||
|
'siblingElements' => $this->buildElementSummaryPayloads(
|
||||||
|
$result->getSiblingElements(),
|
||||||
|
),
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,6 +250,28 @@ class ElementController
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Element[] $elements
|
||||||
|
* @return array<int, array{
|
||||||
|
* id: int,
|
||||||
|
* title: string,
|
||||||
|
* description: string,
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
private function buildElementSummaryPayloads(array $elements): array
|
||||||
|
{
|
||||||
|
$payloads = [];
|
||||||
|
foreach ($elements as $element) {
|
||||||
|
$payloads[] = [
|
||||||
|
'id' => $element->getId(),
|
||||||
|
'title' => $element->getTitle(),
|
||||||
|
'description' => $element->getDescription(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payloads;
|
||||||
|
}
|
||||||
|
|
||||||
private function storageUrl(?string $path, string $folderPrefix): ?string
|
private function storageUrl(?string $path, string $folderPrefix): ?string
|
||||||
{
|
{
|
||||||
if ($path === null) {
|
if ($path === null) {
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,19 @@ class GetElement
|
||||||
throw new NotFoundException('Element not found');
|
throw new NotFoundException('Element not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$parentElement = $element->getParentElement();
|
||||||
|
if ($parentElement === null) {
|
||||||
|
$siblingElements = [$element];
|
||||||
|
} else {
|
||||||
|
$siblingElements = $this->elementRepository
|
||||||
|
->findByParentElement($parentElement);
|
||||||
|
}
|
||||||
|
|
||||||
return new GetElementResult(
|
return new GetElementResult(
|
||||||
element: $element,
|
element: $element,
|
||||||
childElements: $this->elementRepository
|
childElements: $this->elementRepository
|
||||||
->findByParentElement($element),
|
->findByParentElement($element),
|
||||||
|
siblingElements: $siblingElements,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ class GetElementResult
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param Element[] $childElements
|
* @param Element[] $childElements
|
||||||
|
* @param Element[] $siblingElements
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private Element $element,
|
private Element $element,
|
||||||
private array $childElements,
|
private array $childElements,
|
||||||
|
private array $siblingElements,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,4 +29,12 @@ class GetElementResult
|
||||||
{
|
{
|
||||||
return $this->childElements;
|
return $this->childElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Element[]
|
||||||
|
*/
|
||||||
|
public function getSiblingElements(): array
|
||||||
|
{
|
||||||
|
return $this->siblingElements;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,74 @@ class ElementsEndpointTest extends TestCase
|
||||||
'longPdfPath' => '/assets/pdfs/baderech-long.pdf',
|
'longPdfPath' => '/assets/pdfs/baderech-long.pdf',
|
||||||
'youtubeUrl' => $sampleYoutubeUrl,
|
'youtubeUrl' => $sampleYoutubeUrl,
|
||||||
],
|
],
|
||||||
|
'siblingElements' => [
|
||||||
|
[
|
||||||
|
'id' => $element->getId(),
|
||||||
|
'title' => 'Baderech HaAvodah',
|
||||||
|
'description' => 'A structured path for growth',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsSameParentSiblingElements(): void
|
||||||
|
{
|
||||||
|
$setRepository = app(SetRepository::class);
|
||||||
|
$elementRepository = app(ElementRepository::class);
|
||||||
|
$set = $this->createSet($setRepository);
|
||||||
|
$parentElement = $this->createElement(
|
||||||
|
$elementRepository,
|
||||||
|
$set,
|
||||||
|
'Baderech HaAvodah',
|
||||||
|
'A structured path for growth',
|
||||||
|
null,
|
||||||
|
'',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
$firstChildElement = $this->createElement(
|
||||||
|
$elementRepository,
|
||||||
|
$set,
|
||||||
|
'Avodah Foundations',
|
||||||
|
'Foundations for steady avodah',
|
||||||
|
null,
|
||||||
|
'<p>Foundations rich text</p>',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$parentElement,
|
||||||
|
);
|
||||||
|
$secondChildElement = $this->createElement(
|
||||||
|
$elementRepository,
|
||||||
|
$set,
|
||||||
|
'Daily Practice',
|
||||||
|
'Daily practices for growth',
|
||||||
|
null,
|
||||||
|
'',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$parentElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->getJson(
|
||||||
|
"/api/elements/{$firstChildElement->getId()}"
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonPath('siblingElements', [
|
||||||
|
[
|
||||||
|
'id' => $firstChildElement->getId(),
|
||||||
|
'title' => 'Avodah Foundations',
|
||||||
|
'description' => 'Foundations for steady avodah',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => $secondChildElement->getId(),
|
||||||
|
'title' => 'Daily Practice',
|
||||||
|
'description' => 'Daily practices for growth',
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,68 @@ class ElementControllerTest extends TestCase
|
||||||
'description' => 'Daily practices for growth',
|
'description' => 'Daily practices for growth',
|
||||||
],
|
],
|
||||||
], $body['childElements']);
|
], $body['childElements']);
|
||||||
|
$this->assertSame([
|
||||||
|
[
|
||||||
|
'id' => $element->getId(),
|
||||||
|
'title' => 'Baderech HaAvodah',
|
||||||
|
'description' => 'A structured path for growth',
|
||||||
|
],
|
||||||
|
], $body['siblingElements']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShowReturnsSameParentSiblingPayload(): void
|
||||||
|
{
|
||||||
|
$set = $this->createSet(1, 'Baderech');
|
||||||
|
$parentElement = $this->createElement(
|
||||||
|
$set,
|
||||||
|
'Baderech HaAvodah',
|
||||||
|
'A structured path for growth',
|
||||||
|
null,
|
||||||
|
'',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
$firstChildElement = $this->createElement(
|
||||||
|
$set,
|
||||||
|
'Avodah Foundations',
|
||||||
|
'Foundations for steady avodah',
|
||||||
|
null,
|
||||||
|
'<p>Foundations rich text</p>',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$parentElement,
|
||||||
|
);
|
||||||
|
$secondChildElement = $this->createElement(
|
||||||
|
$set,
|
||||||
|
'Daily Practice',
|
||||||
|
'Daily practices for growth',
|
||||||
|
null,
|
||||||
|
'',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$parentElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->controller->show($firstChildElement->getId());
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getContent(), true);
|
||||||
|
$this->assertSame([
|
||||||
|
[
|
||||||
|
'id' => $firstChildElement->getId(),
|
||||||
|
'title' => 'Avodah Foundations',
|
||||||
|
'description' => 'Foundations for steady avodah',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => $secondChildElement->getId(),
|
||||||
|
'title' => 'Daily Practice',
|
||||||
|
'description' => 'Daily practices for growth',
|
||||||
|
],
|
||||||
|
], $body['siblingElements']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testShowReturns400WhenIdMissing(): void
|
public function testShowReturns400WhenIdMissing(): void
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,134 @@ class GetElementTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testReturnsSiblingElementsWithSameParent(): void
|
||||||
|
{
|
||||||
|
$set = $this->createSet(1, 'Baderech');
|
||||||
|
$parentElement = $this->createElement(
|
||||||
|
$set,
|
||||||
|
'Baderech HaAvodah',
|
||||||
|
'A structured path for growth',
|
||||||
|
null,
|
||||||
|
'<p>A structured path for growth</p>',
|
||||||
|
'/assets/pdfs/baderech.pdf',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
$firstChildElement = $this->createElement(
|
||||||
|
$set,
|
||||||
|
'Avodah Foundations',
|
||||||
|
'Foundations for steady avodah',
|
||||||
|
null,
|
||||||
|
'<p>Foundations rich text</p>',
|
||||||
|
'/assets/pdfs/foundations.pdf',
|
||||||
|
null,
|
||||||
|
$parentElement,
|
||||||
|
);
|
||||||
|
$secondChildElement = $this->createElement(
|
||||||
|
$set,
|
||||||
|
'Daily Practice',
|
||||||
|
'Daily practices for growth',
|
||||||
|
null,
|
||||||
|
'<p>Daily practice rich text</p>',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$parentElement,
|
||||||
|
);
|
||||||
|
$this->createElement(
|
||||||
|
$set,
|
||||||
|
'Nested Practice',
|
||||||
|
'Nested description',
|
||||||
|
null,
|
||||||
|
'<p>Nested rich text</p>',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$firstChildElement,
|
||||||
|
);
|
||||||
|
$otherSet = $this->createSet(2, 'Daily Learning');
|
||||||
|
$otherParentElement = $this->createElement(
|
||||||
|
$otherSet,
|
||||||
|
'Other Parent',
|
||||||
|
'Other parent description',
|
||||||
|
null,
|
||||||
|
'<p>Other parent rich text</p>',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
$this->createElement(
|
||||||
|
$otherSet,
|
||||||
|
'Other Child',
|
||||||
|
'Other child description',
|
||||||
|
null,
|
||||||
|
'<p>Other child rich text</p>',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$otherParentElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $this->getElement->execute(new GetElementRequest(
|
||||||
|
id: $firstChildElement->getId(),
|
||||||
|
));
|
||||||
|
$siblingElements = $result->getSiblingElements();
|
||||||
|
|
||||||
|
$this->assertCount(2, $siblingElements);
|
||||||
|
$this->assertSame(
|
||||||
|
$firstChildElement->getId(),
|
||||||
|
$siblingElements[0]->getId(),
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'Avodah Foundations',
|
||||||
|
$siblingElements[0]->getTitle(),
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'Foundations for steady avodah',
|
||||||
|
$siblingElements[0]->getDescription(),
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
$secondChildElement->getId(),
|
||||||
|
$siblingElements[1]->getId(),
|
||||||
|
);
|
||||||
|
$this->assertSame('Daily Practice', $siblingElements[1]->getTitle());
|
||||||
|
$this->assertSame(
|
||||||
|
'Daily practices for growth',
|
||||||
|
$siblingElements[1]->getDescription(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsRootElementAsOnlySibling(): void
|
||||||
|
{
|
||||||
|
$set = $this->createSet(1, 'Baderech');
|
||||||
|
$rootElement = $this->createElement(
|
||||||
|
$set,
|
||||||
|
'Baderech HaAvodah',
|
||||||
|
'A structured path for growth',
|
||||||
|
null,
|
||||||
|
'<p>A structured path for growth</p>',
|
||||||
|
'/assets/pdfs/baderech.pdf',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
$this->createElement(
|
||||||
|
$set,
|
||||||
|
'Avodah Foundations',
|
||||||
|
'Foundations for steady avodah',
|
||||||
|
null,
|
||||||
|
'<p>Foundations rich text</p>',
|
||||||
|
'/assets/pdfs/foundations.pdf',
|
||||||
|
null,
|
||||||
|
$rootElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $this->getElement->execute(new GetElementRequest(
|
||||||
|
id: $rootElement->getId(),
|
||||||
|
));
|
||||||
|
$siblingElements = $result->getSiblingElements();
|
||||||
|
|
||||||
|
$this->assertCount(1, $siblingElements);
|
||||||
|
$this->assertSame($rootElement->getId(), $siblingElements[0]->getId());
|
||||||
|
$this->assertSame('Baderech HaAvodah', $siblingElements[0]->getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
public function testThrowsWhenIdMissing(): void
|
public function testThrowsWhenIdMissing(): void
|
||||||
{
|
{
|
||||||
$this->expectException(BadRequestException::class);
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,4 @@ indent_style = space
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
max_line_length = 100
|
max_line_length = 80
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"printWidth": 80,
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true
|
"singleQuote": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
function loginAsAdmin(): void {
|
||||||
|
cy.visit('/login')
|
||||||
|
|
||||||
|
cy.get('[data-cy="login-email"]').type('admin@rabbigerzi.test')
|
||||||
|
cy.get('[data-cy="login-password"]').type('password123!@#')
|
||||||
|
cy.get('[data-cy="login-submit"]').click()
|
||||||
|
|
||||||
|
cy.url().should('eq', Cypress.config().baseUrl + '/')
|
||||||
|
cy.getCookie('auth_token').should('exist')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('element sibling navigation', () => {
|
||||||
|
it('navigates public same-parent elements with wraparound', () => {
|
||||||
|
cy.visit('/element/1')
|
||||||
|
cy.contains('h1', 'Baderech HaAvodah').should('be.visible')
|
||||||
|
cy.get('[data-cy="element-sibling-navigation"]').should('not.exist')
|
||||||
|
|
||||||
|
cy.visit('/element/2')
|
||||||
|
cy.contains('h1', '1. Introduction').should('be.visible')
|
||||||
|
cy.get('[data-cy="child-element-list"]').then((childList) => {
|
||||||
|
cy.get('[data-cy="element-sibling-navigation"]').then(
|
||||||
|
(siblingNavigation) => {
|
||||||
|
expect(
|
||||||
|
childList[0].compareDocumentPosition(siblingNavigation[0])
|
||||||
|
& Node.DOCUMENT_POSITION_FOLLOWING,
|
||||||
|
).to.not.equal(0)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
cy.get('[data-cy="element-sibling-navigation"]').should('be.visible')
|
||||||
|
cy.get('[data-cy="element-sibling-previous"]')
|
||||||
|
.should('have.attr', 'href', '/element/7')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.location('pathname').should('eq', '/element/7')
|
||||||
|
cy.contains('h1', '6. Fluid Integration').should('be.visible')
|
||||||
|
cy.get('[data-cy="element-sibling-next"]')
|
||||||
|
.should('have.attr', 'href', '/element/2')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.location('pathname').should('eq', '/element/2')
|
||||||
|
cy.contains('h1', '1. Introduction').should('be.visible')
|
||||||
|
cy.get('[data-cy="element-sibling-next"]')
|
||||||
|
.should('have.attr', 'href', '/element/3')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.location('pathname').should('eq', '/element/3')
|
||||||
|
cy.contains('h1', '2. Foundations').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('navigates admin same-parent elements with wraparound', () => {
|
||||||
|
loginAsAdmin()
|
||||||
|
|
||||||
|
cy.visit('/admin/element/1')
|
||||||
|
cy.get('[data-cy="admin-element-title"]')
|
||||||
|
.should('have.value', 'Baderech HaAvodah')
|
||||||
|
cy.get('[data-cy="admin-sibling-navigation"]').should('not.exist')
|
||||||
|
|
||||||
|
cy.visit('/admin/element/2')
|
||||||
|
cy.get('[data-cy="admin-element-title"]')
|
||||||
|
.should('have.value', '1. Introduction')
|
||||||
|
cy.get('[data-cy="admin-element-save"]').then((saveButton) => {
|
||||||
|
cy.get('[data-cy="admin-sibling-navigation"]').then(
|
||||||
|
(siblingNavigation) => {
|
||||||
|
expect(
|
||||||
|
saveButton[0].compareDocumentPosition(siblingNavigation[0])
|
||||||
|
& Node.DOCUMENT_POSITION_FOLLOWING,
|
||||||
|
).to.not.equal(0)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
cy.get('[data-cy="admin-sibling-navigation"]').should('be.visible')
|
||||||
|
cy.get('[data-cy="admin-sibling-previous"]')
|
||||||
|
.should('have.attr', 'href', '/admin/element/7')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.location('pathname').should('eq', '/admin/element/7')
|
||||||
|
cy.get('[data-cy="admin-element-title"]')
|
||||||
|
.should('have.value', '6. Fluid Integration')
|
||||||
|
cy.get('[data-cy="admin-sibling-next"]')
|
||||||
|
.should('have.attr', 'href', '/admin/element/2')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.location('pathname').should('eq', '/admin/element/2')
|
||||||
|
cy.get('[data-cy="admin-element-title"]')
|
||||||
|
.should('have.value', '1. Introduction')
|
||||||
|
cy.get('[data-cy="admin-sibling-next"]')
|
||||||
|
.should('have.attr', 'href', '/admin/element/3')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.location('pathname').should('eq', '/admin/element/3')
|
||||||
|
cy.get('[data-cy="admin-element-title"]')
|
||||||
|
.should('have.value', '2. Foundations')
|
||||||
|
})
|
||||||
|
})
|
||||||
138
frontend/rabbi_gerzi/src/components/ElementSiblingNavigation.vue
Normal file
138
frontend/rabbi_gerzi/src/components/ElementSiblingNavigation.vue
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import {
|
||||||
|
ChevronLeft as ChevronLeftIcon,
|
||||||
|
ChevronRight as ChevronRightIcon,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import type { ChildElement } from '@/stores/elements'
|
||||||
|
|
||||||
|
type ElementRouteName = 'element' | 'admin-element'
|
||||||
|
type ElementNavigationPrefix = 'element' | 'admin'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentElementId: number
|
||||||
|
dataCyPrefix: ElementNavigationPrefix
|
||||||
|
routeName: ElementRouteName
|
||||||
|
siblingElements: ChildElement[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const currentSiblingIndex = computed(() => {
|
||||||
|
return props.siblingElements.findIndex((siblingElement) => {
|
||||||
|
return siblingElement.id === props.currentElementId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasSiblingNavigation = computed(() => {
|
||||||
|
return props.siblingElements.length > 1 && currentSiblingIndex.value !== -1
|
||||||
|
})
|
||||||
|
|
||||||
|
const previousSibling = computed(() => {
|
||||||
|
return siblingElementAtOffset(-1)
|
||||||
|
})
|
||||||
|
|
||||||
|
const nextSibling = computed(() => {
|
||||||
|
return siblingElementAtOffset(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
const navigationDataCy = computed(() => {
|
||||||
|
return `${props.dataCyPrefix}-sibling-navigation`
|
||||||
|
})
|
||||||
|
|
||||||
|
const previousDataCy = computed(() => {
|
||||||
|
return `${props.dataCyPrefix}-sibling-previous`
|
||||||
|
})
|
||||||
|
|
||||||
|
const nextDataCy = computed(() => {
|
||||||
|
return `${props.dataCyPrefix}-sibling-next`
|
||||||
|
})
|
||||||
|
|
||||||
|
function siblingElementAtOffset(offset: number): ChildElement | null {
|
||||||
|
if (!hasSiblingNavigation.value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const siblingCount = props.siblingElements.length
|
||||||
|
const targetIndex =
|
||||||
|
(currentSiblingIndex.value + offset + siblingCount) % siblingCount
|
||||||
|
|
||||||
|
return props.siblingElements[targetIndex] ?? null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
v-if="
|
||||||
|
hasSiblingNavigation && previousSibling !== null && nextSibling !== null
|
||||||
|
"
|
||||||
|
class="element-sibling-navigation"
|
||||||
|
:data-cy="navigationDataCy"
|
||||||
|
aria-label="Sibling elements"
|
||||||
|
>
|
||||||
|
<RouterLink
|
||||||
|
:to="{
|
||||||
|
name: routeName,
|
||||||
|
params: { id: previousSibling.id },
|
||||||
|
}"
|
||||||
|
class="element-sibling-navigation__link"
|
||||||
|
:data-cy="previousDataCy"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon :size="18" aria-hidden="true" />
|
||||||
|
<span>Previous</span>
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink
|
||||||
|
:to="{
|
||||||
|
name: routeName,
|
||||||
|
params: { id: nextSibling.id },
|
||||||
|
}"
|
||||||
|
class="element-sibling-navigation__link"
|
||||||
|
:data-cy="nextDataCy"
|
||||||
|
>
|
||||||
|
<span>Next</span>
|
||||||
|
<ChevronRightIcon :size="18" aria-hidden="true" />
|
||||||
|
</RouterLink>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.element-sibling-navigation {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 1.35rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-sibling-navigation__link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
padding: 0.55rem 0.95rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-white);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
color 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-sibling-navigation__link:hover,
|
||||||
|
.element-sibling-navigation__link:focus-visible {
|
||||||
|
border-color: var(--color-slate);
|
||||||
|
color: var(--color-slate);
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-sibling-navigation__link:focus-visible {
|
||||||
|
outline: 3px solid rgb(61 78 93 / 24%);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -73,5 +73,13 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user, loginError, isSubmitting, isAuthenticated, login, fetchUser, logout }
|
return {
|
||||||
|
user,
|
||||||
|
loginError,
|
||||||
|
isSubmitting,
|
||||||
|
isAuthenticated,
|
||||||
|
login,
|
||||||
|
fetchUser,
|
||||||
|
logout,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ type ElementFileType = 'iconImage' | 'shortPdf' | 'longPdf'
|
||||||
interface ElementResponse {
|
interface ElementResponse {
|
||||||
element: Element
|
element: Element
|
||||||
childElements: ChildElement[]
|
childElements: ChildElement[]
|
||||||
|
siblingElements: ChildElement[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateElementInput {
|
export interface UpdateElementInput {
|
||||||
|
|
@ -58,6 +59,7 @@ const ELEMENT_UPDATE_URL = `${API_BASE_URL}/api/element/update`
|
||||||
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<ChildElement[]>([])
|
const childElements = ref<ChildElement[]>([])
|
||||||
|
const siblingElements = ref<ChildElement[]>([])
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
|
|
@ -71,6 +73,7 @@ export const useElementsStore = defineStore('elements', () => {
|
||||||
async function fetchElement(elementId: string): Promise<void> {
|
async function fetchElement(elementId: string): Promise<void> {
|
||||||
element.value = null
|
element.value = null
|
||||||
childElements.value = []
|
childElements.value = []
|
||||||
|
siblingElements.value = []
|
||||||
error.value = null
|
error.value = null
|
||||||
saveError.value = null
|
saveError.value = null
|
||||||
childActionError.value = null
|
childActionError.value = null
|
||||||
|
|
@ -94,8 +97,10 @@ 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
|
childElements.value = data.childElements
|
||||||
|
siblingElements.value = data.siblingElements
|
||||||
} catch {
|
} catch {
|
||||||
childElements.value = []
|
childElements.value = []
|
||||||
|
siblingElements.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
|
||||||
|
|
@ -153,8 +158,8 @@ export const useElementsStore = defineStore('elements', () => {
|
||||||
childElementId.toString(),
|
childElementId.toString(),
|
||||||
)
|
)
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${ELEMENTS_URL}/${encodedParentElementId}`
|
`${ELEMENTS_URL}/${encodedParentElementId}` +
|
||||||
+ `/children/${encodedChildElementId}`,
|
`/children/${encodedChildElementId}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|
@ -388,6 +393,7 @@ export const useElementsStore = defineStore('elements', () => {
|
||||||
return {
|
return {
|
||||||
element,
|
element,
|
||||||
childElements,
|
childElements,
|
||||||
|
siblingElements,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,8 @@ const teachers: Teacher[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Rabbi Efraim Greenblatt',
|
name: 'Rabbi Efraim Greenblatt',
|
||||||
description: 'Halachic authority and loyal student of Rabbi Moshe Feinstein.',
|
description:
|
||||||
|
'Halachic authority and loyal student of Rabbi Moshe Feinstein.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'The Sassover Rebbe, The Biala Rebbe, and Rabbi Mordechai Zukerman',
|
name: 'The Sassover Rebbe, The Biala Rebbe, and Rabbi Mordechai Zukerman',
|
||||||
|
|
@ -230,8 +231,8 @@ function submitNewsletter(submitEvent: Event): void {
|
||||||
<p class="about-hero__eyebrow">Pilzno Institute</p>
|
<p class="about-hero__eyebrow">Pilzno Institute</p>
|
||||||
<h1 class="about-hero__heading">About Rabbi Gerzi</h1>
|
<h1 class="about-hero__heading">About Rabbi Gerzi</h1>
|
||||||
<p class="about-hero__lede">
|
<p class="about-hero__lede">
|
||||||
Healthy, Integrated and Balanced Torah living, rooted in mesorah and made practical
|
Healthy, Integrated and Balanced Torah living, rooted in mesorah
|
||||||
for real people building real lives.
|
and made practical for real people building real lives.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
|
|
@ -261,25 +262,32 @@ function submitNewsletter(submitEvent: Event): void {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="about-section about-section--slate" data-cy="about-vision">
|
<section
|
||||||
|
class="about-section about-section--slate"
|
||||||
|
data-cy="about-vision"
|
||||||
|
>
|
||||||
<div class="about-section__inner about-section__inner--narrow">
|
<div class="about-section__inner about-section__inner--narrow">
|
||||||
<p class="about-section__kicker">Vision</p>
|
<p class="about-section__kicker">Vision</p>
|
||||||
<h2 class="about-section__heading">Rabbi Gerzi's Vision</h2>
|
<h2 class="about-section__heading">Rabbi Gerzi's Vision</h2>
|
||||||
<p class="about-section__paragraph">
|
<p class="about-section__paragraph">
|
||||||
Rabbi Gerzi guides individuals from vagueness to clarity, from surviving to thriving,
|
Rabbi Gerzi guides individuals from vagueness to clarity, from
|
||||||
from a galut to a geula mindset. Through profound teachings rooted in Torah wisdom,
|
surviving to thriving, from a galut to a geula mindset. Through
|
||||||
Rabbi Gerzi aims to connect students with their divine purpose, discover balance, and
|
profound teachings rooted in Torah wisdom, Rabbi Gerzi aims to
|
||||||
|
connect students with their divine purpose, discover balance, and
|
||||||
live a life of authentic joy and fulfillment.
|
live a life of authentic joy and fulfillment.
|
||||||
</p>
|
</p>
|
||||||
<p class="about-section__paragraph">
|
<p class="about-section__paragraph">
|
||||||
As spiritual growth can only be accomplished within a fertile social framework, Rabbi
|
As spiritual growth can only be accomplished within a fertile social
|
||||||
Gerzi hosts regular weekly chaburot and various events that create the conditions for
|
framework, Rabbi Gerzi hosts regular weekly chaburot and various
|
||||||
authentic life transformation.
|
events that create the conditions for authentic life transformation.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="about-section about-section--cream" data-cy="about-mission">
|
<section
|
||||||
|
class="about-section about-section--cream"
|
||||||
|
data-cy="about-mission"
|
||||||
|
>
|
||||||
<div class="about-section__inner">
|
<div class="about-section__inner">
|
||||||
<div class="about-section__header">
|
<div class="about-section__header">
|
||||||
<p class="about-section__kicker">Mission</p>
|
<p class="about-section__kicker">Mission</p>
|
||||||
|
|
@ -287,9 +295,10 @@ function submitNewsletter(submitEvent: Event): void {
|
||||||
The Mission: To Provide a Clear Path to Wholesome Living
|
The Mission: To Provide a Clear Path to Wholesome Living
|
||||||
</h2>
|
</h2>
|
||||||
<p class="about-section__intro">
|
<p class="about-section__intro">
|
||||||
Rabbi Gerzi provides a structured path to wisdom, personal growth, and transformation.
|
Rabbi Gerzi provides a structured path to wisdom, personal growth,
|
||||||
His educational system is designed to guide individuals through essential life areas,
|
and transformation. His educational system is designed to guide
|
||||||
creating not just intellectual understanding but genuine change.
|
individuals through essential life areas, creating not just
|
||||||
|
intellectual understanding but genuine change.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -318,27 +327,35 @@ function submitNewsletter(submitEvent: Event): void {
|
||||||
</div>
|
</div>
|
||||||
<div class="about-section__body">
|
<div class="about-section__body">
|
||||||
<p class="about-section__paragraph">
|
<p class="about-section__paragraph">
|
||||||
Rabbi Gerzi's teachings are deeply influenced by his mentor, Rabbi Yosef Singer, the
|
Rabbi Gerzi's teachings are deeply influenced by his mentor, Rabbi
|
||||||
Pilzno Rav. Under Rabbi Singer's guidance, Rabbi Gerzi absorbed the teachings of the
|
Yosef Singer, the Pilzno Rav. Under Rabbi Singer's guidance, Rabbi
|
||||||
Baal Shem Tov, Chassidut, Kabbalah, and Halacha.
|
Gerzi absorbed the teachings of the Baal Shem Tov, Chassidut,
|
||||||
|
Kabbalah, and Halacha.
|
||||||
</p>
|
</p>
|
||||||
<p class="about-section__paragraph">
|
<p class="about-section__paragraph">
|
||||||
In 2005, the Pilzno Rav instructed Rabbi Gerzi to continue his legacy by building a
|
In 2005, the Pilzno Rav instructed Rabbi Gerzi to continue his
|
||||||
community in Eretz Yisrael dedicated to this unique form of Healthy, Integrated and
|
legacy by building a community in Eretz Yisrael dedicated to this
|
||||||
Balanced Torah living.
|
unique form of Healthy, Integrated and Balanced Torah living.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="about-section about-section--cream" data-cy="about-teachers">
|
<section
|
||||||
|
class="about-section about-section--cream"
|
||||||
|
data-cy="about-teachers"
|
||||||
|
>
|
||||||
<div class="about-section__inner">
|
<div class="about-section__inner">
|
||||||
<div class="about-section__header">
|
<div class="about-section__header">
|
||||||
<p class="about-section__kicker">Teachers</p>
|
<p class="about-section__kicker">Teachers</p>
|
||||||
<h2 class="about-section__heading">Rabbi Gerzi's Teachers</h2>
|
<h2 class="about-section__heading">Rabbi Gerzi's Teachers</h2>
|
||||||
</div>
|
</div>
|
||||||
<ol class="teacher-list">
|
<ol class="teacher-list">
|
||||||
<li v-for="teacher in teachers" :key="teacher.name" class="teacher-list__item">
|
<li
|
||||||
|
v-for="teacher in teachers"
|
||||||
|
:key="teacher.name"
|
||||||
|
class="teacher-list__item"
|
||||||
|
>
|
||||||
<span class="teacher-list__name">{{ teacher.name }}</span>
|
<span class="teacher-list__name">{{ teacher.name }}</span>
|
||||||
<span class="teacher-list__description">
|
<span class="teacher-list__description">
|
||||||
{{ teacher.description }}
|
{{ teacher.description }}
|
||||||
|
|
@ -397,7 +414,8 @@ function submitNewsletter(submitEvent: Event): void {
|
||||||
<div class="get-involved__copy">
|
<div class="get-involved__copy">
|
||||||
<h2 class="get-involved__heading">Get Involved</h2>
|
<h2 class="get-involved__heading">Get Involved</h2>
|
||||||
<p class="get-involved__subtext">
|
<p class="get-involved__subtext">
|
||||||
Subscribe for updates as we release new content, chaburot and events.
|
Subscribe for updates as we release new content, chaburot and
|
||||||
|
events.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="get-involved__form" @submit="submitNewsletter">
|
<form class="get-involved__form" @submit="submitNewsletter">
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import ElementSiblingNavigation from '@/components/ElementSiblingNavigation.vue'
|
||||||
import RichTextEditor from '@/components/RichTextEditor.vue'
|
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||||
import SiteHeader from '@/components/SiteHeader.vue'
|
import SiteHeader from '@/components/SiteHeader.vue'
|
||||||
import { useElementsStore } from '@/stores/elements'
|
import { useElementsStore } from '@/stores/elements'
|
||||||
|
|
@ -23,6 +24,7 @@ const elementsStore = useElementsStore()
|
||||||
const {
|
const {
|
||||||
element,
|
element,
|
||||||
childElements,
|
childElements,
|
||||||
|
siblingElements,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
|
@ -617,6 +619,13 @@ function resetInput(changeEvent: Event): void {
|
||||||
{{ statusMessage }}
|
{{ statusMessage }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ElementSiblingNavigation
|
||||||
|
:current-element-id="element.id"
|
||||||
|
:sibling-elements="siblingElements"
|
||||||
|
data-cy-prefix="admin"
|
||||||
|
route-name="admin-element"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -40,15 +40,20 @@ function submitNewsletter(submitEvent: Event): void {
|
||||||
<p class="contact-hero__eyebrow">Contact</p>
|
<p class="contact-hero__eyebrow">Contact</p>
|
||||||
<h1 class="contact-hero__heading">Contact</h1>
|
<h1 class="contact-hero__heading">Contact</h1>
|
||||||
<p class="contact-hero__body">
|
<p class="contact-hero__body">
|
||||||
Rabbi Gerzi receives numerous messages each week. It may take some time to get back to
|
Rabbi Gerzi receives numerous messages each week. It may take some
|
||||||
you, but we will do our best to respond in a timely manner.
|
time to get back to you, but we will do our best to respond in a
|
||||||
|
timely manner.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="contact-main">
|
<section class="contact-main">
|
||||||
<div class="contact-main__inner">
|
<div class="contact-main__inner">
|
||||||
<form class="contact-form" data-cy="contact-form" @submit="submitContact">
|
<form
|
||||||
|
class="contact-form"
|
||||||
|
data-cy="contact-form"
|
||||||
|
@submit="submitContact"
|
||||||
|
>
|
||||||
<h2 class="contact-form__heading">Send Rabbi Gerzi a Message</h2>
|
<h2 class="contact-form__heading">Send Rabbi Gerzi a Message</h2>
|
||||||
<div class="contact-form__grid">
|
<div class="contact-form__grid">
|
||||||
<label class="contact-form__field">
|
<label class="contact-form__field">
|
||||||
|
|
@ -121,7 +126,9 @@ function submitNewsletter(submitEvent: Event): void {
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<aside class="contact-social" data-cy="contact-social">
|
<aside class="contact-social" data-cy="contact-social">
|
||||||
<h2 class="contact-social__heading">Follow Rabbi Gerzi on Social Media</h2>
|
<h2 class="contact-social__heading">
|
||||||
|
Follow Rabbi Gerzi on Social Media
|
||||||
|
</h2>
|
||||||
<div class="contact-social__links">
|
<div class="contact-social__links">
|
||||||
<a
|
<a
|
||||||
v-for="socialLink in socialLinks"
|
v-for="socialLink in socialLinks"
|
||||||
|
|
@ -143,7 +150,8 @@ function submitNewsletter(submitEvent: Event): void {
|
||||||
<div class="get-involved__copy">
|
<div class="get-involved__copy">
|
||||||
<h2 class="get-involved__heading">Get Involved</h2>
|
<h2 class="get-involved__heading">Get Involved</h2>
|
||||||
<p class="get-involved__subtext">
|
<p class="get-involved__subtext">
|
||||||
Subscribe for updates as we release new content, chaburot and events.
|
Subscribe for updates as we release new content, chaburot and
|
||||||
|
events.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="get-involved__form" @submit="submitNewsletter">
|
<form class="get-involved__form" @submit="submitNewsletter">
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import ElementSiblingNavigation from '@/components/ElementSiblingNavigation.vue'
|
||||||
import SiteHeader from '@/components/SiteHeader.vue'
|
import SiteHeader from '@/components/SiteHeader.vue'
|
||||||
import { useElementsStore } from '@/stores/elements'
|
import { useElementsStore } from '@/stores/elements'
|
||||||
|
|
||||||
|
|
@ -9,7 +10,8 @@ type TimestampPart = string | undefined
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const elementsStore = useElementsStore()
|
const elementsStore = useElementsStore()
|
||||||
const { element, childElements, isLoading, error } = storeToRefs(elementsStore)
|
const { element, childElements, siblingElements, isLoading, error } =
|
||||||
|
storeToRefs(elementsStore)
|
||||||
|
|
||||||
const youtubeIframeAllow = [
|
const youtubeIframeAllow = [
|
||||||
'accelerometer',
|
'accelerometer',
|
||||||
|
|
@ -297,6 +299,13 @@ function isShortYoutubeHost(hostname: string): boolean {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<ElementSiblingNavigation
|
||||||
|
:current-element-id="element.id"
|
||||||
|
:sibling-elements="siblingElements"
|
||||||
|
data-cy-prefix="element"
|
||||||
|
route-name="element"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,9 @@ const testimonials: Testimonial[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Anonymous Student',
|
name: 'Anonymous Student',
|
||||||
quote: 'My understanding of Torah has been profoundly improved through ' + 'his teachings.',
|
quote:
|
||||||
|
'My understanding of Torah has been profoundly improved through ' +
|
||||||
|
'his teachings.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Placeholder Student 1',
|
name: 'Placeholder Student 1',
|
||||||
|
|
@ -267,7 +269,8 @@ function projectCardStyle(project: Project): string {
|
||||||
<div class="hero__text">
|
<div class="hero__text">
|
||||||
<h1 class="hero__headline">
|
<h1 class="hero__headline">
|
||||||
Let’s upgrade our lives to a
|
Let’s upgrade our lives to a
|
||||||
<em>healthy</em>, <em>integrated</em>, and <em>balanced</em> Torah existence.
|
<em>healthy</em>, <em>integrated</em>, and <em>balanced</em> Torah
|
||||||
|
existence.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="hero__attribution">Rabbi Yehoshua Gerzi</p>
|
<p class="hero__attribution">Rabbi Yehoshua Gerzi</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -297,23 +300,29 @@ function projectCardStyle(project: Project): string {
|
||||||
<path d="M10 65 L40 25 L55 45 L70 15 L90 45 L110 65" />
|
<path d="M10 65 L40 25 L55 45 L70 15 L90 45 L110 65" />
|
||||||
<path d="M30 65 C45 55, 60 60, 75 50 S100 55, 110 65" />
|
<path d="M30 65 C45 55, 60 60, 75 50 S100 55, 110 65" />
|
||||||
</svg>
|
</svg>
|
||||||
<h2 class="discover-path__heading">Discover a Path to a Holistically Engaged Life</h2>
|
<h2 class="discover-path__heading">
|
||||||
|
Discover a Path to a Holistically Engaged Life
|
||||||
|
</h2>
|
||||||
<p class="discover-path__paragraph">
|
<p class="discover-path__paragraph">
|
||||||
Rabbi Yehoshua Gerzi has dedicated his life to empowering individuals to achieve a state
|
Rabbi Yehoshua Gerzi has dedicated his life to empowering individuals
|
||||||
of “Healthy, Integrated and Balanced Torah Living.” His teachings weave
|
to achieve a state of “Healthy, Integrated and Balanced Torah
|
||||||
together timeless Torah wisdom, mystical insights and practical guidance, making ancient
|
Living.” His teachings weave together timeless Torah wisdom,
|
||||||
wisdom accessible and transformative for modern lives.
|
mystical insights and practical guidance, making ancient wisdom
|
||||||
|
accessible and transformative for modern lives.
|
||||||
</p>
|
</p>
|
||||||
<p class="discover-path__paragraph">
|
<p class="discover-path__paragraph">
|
||||||
Rabbi Gerzi is based in Ramat Beit Shemesh, where he serves as Rav of Kehillas Beis David
|
Rabbi Gerzi is based in Ramat Beit Shemesh, where he serves as Rav of
|
||||||
and Director of the Pilzno Institute of Higher Learning.
|
Kehillas Beis David and Director of the Pilzno Institute of Higher
|
||||||
|
Learning.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="core-teachings" data-cy="core-teachings">
|
<section class="core-teachings" data-cy="core-teachings">
|
||||||
<div class="core-teachings__inner">
|
<div class="core-teachings__inner">
|
||||||
<h2 class="core-teachings__heading">Baderech HaAvodah (Core Teachings)</h2>
|
<h2 class="core-teachings__heading">
|
||||||
|
Baderech HaAvodah (Core Teachings)
|
||||||
|
</h2>
|
||||||
<div class="core-teachings__grid">
|
<div class="core-teachings__grid">
|
||||||
<div
|
<div
|
||||||
v-for="teaching in coreTeachings"
|
v-for="teaching in coreTeachings"
|
||||||
|
|
@ -323,7 +332,9 @@ function projectCardStyle(project: Project): string {
|
||||||
<span class="core-teachings__glyph" aria-hidden="true">
|
<span class="core-teachings__glyph" aria-hidden="true">
|
||||||
{{ teaching.glyph }}
|
{{ teaching.glyph }}
|
||||||
</span>
|
</span>
|
||||||
<p class="core-teachings__label">{{ teaching.number }}) {{ teaching.title }}</p>
|
<p class="core-teachings__label">
|
||||||
|
{{ teaching.number }}) {{ teaching.title }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -348,13 +359,18 @@ function projectCardStyle(project: Project): string {
|
||||||
</svg>
|
</svg>
|
||||||
<h2 class="unique-voice__heading">A Unique Voice for the Generation</h2>
|
<h2 class="unique-voice__heading">A Unique Voice for the Generation</h2>
|
||||||
<p class="unique-voice__paragraph">
|
<p class="unique-voice__paragraph">
|
||||||
Rabbi Gerzi provides a wholly unique and much needed perspective to his students. Carrying
|
Rabbi Gerzi provides a wholly unique and much needed perspective to
|
||||||
decades of experience guided by a handful of the generations most pre-eminent Rabbanim, he
|
his students. Carrying decades of experience guided by a handful of
|
||||||
teaches a message of practically engaged positivity. His Torah is one of unity, bridging
|
the generations most pre-eminent Rabbanim, he teaches a message of
|
||||||
hashkafic boundaries and engaging a diverse cadre of Jews who are open minded to embracing
|
practically engaged positivity. His Torah is one of unity, bridging
|
||||||
meaningful and experiential Judaism.
|
hashkafic boundaries and engaging a diverse cadre of Jews who are open
|
||||||
|
minded to embracing meaningful and experiential Judaism.
|
||||||
</p>
|
</p>
|
||||||
<button class="unique-voice__cta-button" type="button" @click="goToAbout">
|
<button
|
||||||
|
class="unique-voice__cta-button"
|
||||||
|
type="button"
|
||||||
|
@click="goToAbout"
|
||||||
|
>
|
||||||
Learn about Rabbi Gerzi’s Approach
|
Learn about Rabbi Gerzi’s Approach
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -373,7 +389,9 @@ function projectCardStyle(project: Project): string {
|
||||||
:aria-hidden="testimonialPosition(index) !== 'active'"
|
:aria-hidden="testimonialPosition(index) !== 'active'"
|
||||||
>
|
>
|
||||||
<h3 class="testimonials__name">{{ testimonial.name }}</h3>
|
<h3 class="testimonials__name">{{ testimonial.name }}</h3>
|
||||||
<p class="testimonials__quote">“{{ testimonial.quote }}”</p>
|
<p class="testimonials__quote">
|
||||||
|
“{{ testimonial.quote }}”
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="testimonials__controls">
|
<div class="testimonials__controls">
|
||||||
|
|
@ -401,7 +419,9 @@ function projectCardStyle(project: Project): string {
|
||||||
|
|
||||||
<section class="sponsors" data-cy="sponsors">
|
<section class="sponsors" data-cy="sponsors">
|
||||||
<div class="sponsors__inner">
|
<div class="sponsors__inner">
|
||||||
<h2 class="sponsors__heading">Organizations Rabbi Gerzi has Worked With</h2>
|
<h2 class="sponsors__heading">
|
||||||
|
Organizations Rabbi Gerzi has Worked With
|
||||||
|
</h2>
|
||||||
<div class="sponsors__carousel" data-cy="sponsor-carousel">
|
<div class="sponsors__carousel" data-cy="sponsor-carousel">
|
||||||
<div class="sponsors__track" data-cy="sponsor-track">
|
<div class="sponsors__track" data-cy="sponsor-track">
|
||||||
<div
|
<div
|
||||||
|
|
@ -447,9 +467,15 @@ function projectCardStyle(project: Project): string {
|
||||||
<div class="journey__inner">
|
<div class="journey__inner">
|
||||||
<h2 class="journey__heading">Start Your Journey</h2>
|
<h2 class="journey__heading">Start Your Journey</h2>
|
||||||
<div class="journey__cards">
|
<div class="journey__cards">
|
||||||
<a class="journey__card journey__card--olive" href="/media" data-cy="journey-media">
|
<a
|
||||||
|
class="journey__card journey__card--olive"
|
||||||
|
href="/media"
|
||||||
|
data-cy="journey-media"
|
||||||
|
>
|
||||||
<h3 class="journey__card-title">Access<br />Torah Media</h3>
|
<h3 class="journey__card-title">Access<br />Torah Media</h3>
|
||||||
<p class="journey__card-body">Explore a wealth of video, audio and written content.</p>
|
<p class="journey__card-body">
|
||||||
|
Explore a wealth of video, audio and written content.
|
||||||
|
</p>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="journey__card journey__card--slate"
|
class="journey__card journey__card--slate"
|
||||||
|
|
@ -470,7 +496,8 @@ function projectCardStyle(project: Project): string {
|
||||||
<div class="get-involved__copy">
|
<div class="get-involved__copy">
|
||||||
<h2 class="get-involved__heading">Get Involved</h2>
|
<h2 class="get-involved__heading">Get Involved</h2>
|
||||||
<p class="get-involved__subtext">
|
<p class="get-involved__subtext">
|
||||||
Subscribe for updates as we release new content, chaburot and events.
|
Subscribe for updates as we release new content, chaburot and
|
||||||
|
events.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="get-involved__form" @submit="submitNewsletter">
|
<form class="get-involved__form" @submit="submitNewsletter">
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,11 @@ async function handleSubmit(): Promise<void> {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<p v-if="authStore.loginError" data-cy="login-error" class="login-card__error">
|
<p
|
||||||
|
v-if="authStore.loginError"
|
||||||
|
data-cy="login-error"
|
||||||
|
class="login-card__error"
|
||||||
|
>
|
||||||
{{ authStore.loginError }}
|
{{ authStore.loginError }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue