Compare commits

...

8 commits

18 changed files with 684 additions and 65 deletions

View file

@ -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) {

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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
} }

View file

@ -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')
})
})

View 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>

View file

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

View file

@ -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,

View file

@ -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">

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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&rsquo;s upgrade our lives to a Let&rsquo;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 &ldquo;Healthy, Integrated and Balanced Torah Living.&rdquo; His teachings weave to achieve a state of &ldquo;Healthy, Integrated and Balanced Torah
together timeless Torah wisdom, mystical insights and practical guidance, making ancient Living.&rdquo; 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&rsquo;s Approach Learn about Rabbi Gerzi&rsquo;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">&ldquo;{{ testimonial.quote }}&rdquo;</p> <p class="testimonials__quote">
&ldquo;{{ testimonial.quote }}&rdquo;
</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">

View file

@ -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>