Merge branch 'feature/element-sibling-navigation'

This commit is contained in:
Yisroel Baum 2026-06-22 09:31:20 +03:00
commit 14de828b4b
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
18 changed files with 684 additions and 65 deletions

View file

@ -46,18 +46,15 @@ class ElementController
}
$element = $result->getElement();
$childElements = [];
foreach ($result->getChildElements() as $childElement) {
$childElements[] = [
'id' => $childElement->getId(),
'title' => $childElement->getTitle(),
'description' => $childElement->getDescription(),
];
}
return new JsonResponse([
'childElements' => $childElements,
'childElements' => $this->buildElementSummaryPayloads(
$result->getChildElements(),
),
'element' => $this->buildElementPayload($element),
'siblingElements' => $this->buildElementSummaryPayloads(
$result->getSiblingElements(),
),
], 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
{
if ($path === null) {

View file

@ -27,10 +27,19 @@ class GetElement
throw new NotFoundException('Element not found');
}
$parentElement = $element->getParentElement();
if ($parentElement === null) {
$siblingElements = [$element];
} else {
$siblingElements = $this->elementRepository
->findByParentElement($parentElement);
}
return new GetElementResult(
element: $element,
childElements: $this->elementRepository
->findByParentElement($element),
siblingElements: $siblingElements,
);
}
}

View file

@ -8,10 +8,12 @@ class GetElementResult
{
/**
* @param Element[] $childElements
* @param Element[] $siblingElements
*/
public function __construct(
private Element $element,
private array $childElements,
private array $siblingElements,
) {
}
@ -27,4 +29,12 @@ class GetElementResult
{
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',
'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',
],
], $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

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
{
$this->expectException(BadRequestException::class);

View file

@ -5,4 +5,4 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
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",
"printWidth": 80,
"semi": false,
"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 {
element: Element
childElements: ChildElement[]
siblingElements: ChildElement[]
}
export interface UpdateElementInput {
@ -58,6 +59,7 @@ const ELEMENT_UPDATE_URL = `${API_BASE_URL}/api/element/update`
export const useElementsStore = defineStore('elements', () => {
const element = ref<Element | null>(null)
const childElements = ref<ChildElement[]>([])
const siblingElements = ref<ChildElement[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
const isSaving = ref(false)
@ -71,6 +73,7 @@ export const useElementsStore = defineStore('elements', () => {
async function fetchElement(elementId: string): Promise<void> {
element.value = null
childElements.value = []
siblingElements.value = []
error.value = null
saveError.value = null
childActionError.value = null
@ -94,8 +97,10 @@ export const useElementsStore = defineStore('elements', () => {
const data: ElementResponse = await response.json()
element.value = data.element
childElements.value = data.childElements
siblingElements.value = data.siblingElements
} catch {
childElements.value = []
siblingElements.value = []
error.value = 'Network error - could not load element'
} finally {
isLoading.value = false
@ -153,8 +158,8 @@ export const useElementsStore = defineStore('elements', () => {
childElementId.toString(),
)
const response = await fetch(
`${ELEMENTS_URL}/${encodedParentElementId}`
+ `/children/${encodedChildElementId}`,
`${ELEMENTS_URL}/${encodedParentElementId}` +
`/children/${encodedChildElementId}`,
{
method: 'DELETE',
credentials: 'include',
@ -388,6 +393,7 @@ export const useElementsStore = defineStore('elements', () => {
return {
element,
childElements,
siblingElements,
isLoading,
error,
isSaving,

View file

@ -73,7 +73,8 @@ const teachers: Teacher[] = [
},
{
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',
@ -230,8 +231,8 @@ function submitNewsletter(submitEvent: Event): void {
<p class="about-hero__eyebrow">Pilzno Institute</p>
<h1 class="about-hero__heading">About Rabbi Gerzi</h1>
<p class="about-hero__lede">
Healthy, Integrated and Balanced Torah living, rooted in mesorah and made practical
for real people building real lives.
Healthy, Integrated and Balanced Torah living, rooted in mesorah
and made practical for real people building real lives.
</p>
</div>
<img
@ -261,25 +262,32 @@ function submitNewsletter(submitEvent: Event): void {
</div>
</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">
<p class="about-section__kicker">Vision</p>
<h2 class="about-section__heading">Rabbi Gerzi's Vision</h2>
<p class="about-section__paragraph">
Rabbi Gerzi guides individuals from vagueness to clarity, from surviving to thriving,
from a galut to a geula mindset. Through profound teachings rooted in Torah wisdom,
Rabbi Gerzi aims to connect students with their divine purpose, discover balance, and
Rabbi Gerzi guides individuals from vagueness to clarity, from
surviving to thriving, from a galut to a geula mindset. Through
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.
</p>
<p class="about-section__paragraph">
As spiritual growth can only be accomplished within a fertile social framework, Rabbi
Gerzi hosts regular weekly chaburot and various events that create the conditions for
authentic life transformation.
As spiritual growth can only be accomplished within a fertile social
framework, Rabbi Gerzi hosts regular weekly chaburot and various
events that create the conditions for authentic life transformation.
</p>
</div>
</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__header">
<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
</h2>
<p class="about-section__intro">
Rabbi Gerzi provides a structured path to wisdom, personal growth, and transformation.
His educational system is designed to guide individuals through essential life areas,
creating not just intellectual understanding but genuine change.
Rabbi Gerzi provides a structured path to wisdom, personal growth,
and transformation. His educational system is designed to guide
individuals through essential life areas, creating not just
intellectual understanding but genuine change.
</p>
</div>
@ -318,27 +327,35 @@ function submitNewsletter(submitEvent: Event): void {
</div>
<div class="about-section__body">
<p class="about-section__paragraph">
Rabbi Gerzi's teachings are deeply influenced by his mentor, Rabbi Yosef Singer, the
Pilzno Rav. Under Rabbi Singer's guidance, Rabbi Gerzi absorbed the teachings of the
Baal Shem Tov, Chassidut, Kabbalah, and Halacha.
Rabbi Gerzi's teachings are deeply influenced by his mentor, Rabbi
Yosef Singer, the Pilzno Rav. Under Rabbi Singer's guidance, Rabbi
Gerzi absorbed the teachings of the Baal Shem Tov, Chassidut,
Kabbalah, and Halacha.
</p>
<p class="about-section__paragraph">
In 2005, the Pilzno Rav instructed Rabbi Gerzi to continue his legacy by building a
community in Eretz Yisrael dedicated to this unique form of Healthy, Integrated and
Balanced Torah living.
In 2005, the Pilzno Rav instructed Rabbi Gerzi to continue his
legacy by building a community in Eretz Yisrael dedicated to this
unique form of Healthy, Integrated and Balanced Torah living.
</p>
</div>
</div>
</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__header">
<p class="about-section__kicker">Teachers</p>
<h2 class="about-section__heading">Rabbi Gerzi's Teachers</h2>
</div>
<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__description">
{{ teacher.description }}
@ -397,7 +414,8 @@ function submitNewsletter(submitEvent: Event): void {
<div class="get-involved__copy">
<h2 class="get-involved__heading">Get Involved</h2>
<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>
</div>
<form class="get-involved__form" @submit="submitNewsletter">

View file

@ -2,6 +2,7 @@
import { storeToRefs } from 'pinia'
import { computed, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import ElementSiblingNavigation from '@/components/ElementSiblingNavigation.vue'
import RichTextEditor from '@/components/RichTextEditor.vue'
import SiteHeader from '@/components/SiteHeader.vue'
import { useElementsStore } from '@/stores/elements'
@ -23,6 +24,7 @@ const elementsStore = useElementsStore()
const {
element,
childElements,
siblingElements,
isLoading,
error,
isSaving,
@ -617,6 +619,13 @@ function resetInput(changeEvent: Event): void {
{{ statusMessage }}
</p>
</div>
<ElementSiblingNavigation
:current-element-id="element.id"
:sibling-elements="siblingElements"
data-cy-prefix="admin"
route-name="admin-element"
/>
</form>
</main>
</div>

View file

@ -40,15 +40,20 @@ function submitNewsletter(submitEvent: Event): void {
<p class="contact-hero__eyebrow">Contact</p>
<h1 class="contact-hero__heading">Contact</h1>
<p class="contact-hero__body">
Rabbi Gerzi receives numerous messages each week. It may take some time to get back to
you, but we will do our best to respond in a timely manner.
Rabbi Gerzi receives numerous messages each week. It may take some
time to get back to you, but we will do our best to respond in a
timely manner.
</p>
</div>
</section>
<section class="contact-main">
<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>
<div class="contact-form__grid">
<label class="contact-form__field">
@ -121,7 +126,9 @@ function submitNewsletter(submitEvent: Event): void {
</form>
<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">
<a
v-for="socialLink in socialLinks"
@ -143,7 +150,8 @@ function submitNewsletter(submitEvent: Event): void {
<div class="get-involved__copy">
<h2 class="get-involved__heading">Get Involved</h2>
<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>
</div>
<form class="get-involved__form" @submit="submitNewsletter">

View file

@ -2,6 +2,7 @@
import { storeToRefs } from 'pinia'
import { computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import ElementSiblingNavigation from '@/components/ElementSiblingNavigation.vue'
import SiteHeader from '@/components/SiteHeader.vue'
import { useElementsStore } from '@/stores/elements'
@ -9,7 +10,8 @@ type TimestampPart = string | undefined
const route = useRoute()
const elementsStore = useElementsStore()
const { element, childElements, isLoading, error } = storeToRefs(elementsStore)
const { element, childElements, siblingElements, isLoading, error } =
storeToRefs(elementsStore)
const youtubeIframeAllow = [
'accelerometer',
@ -297,6 +299,13 @@ function isShortYoutubeHost(hostname: string): boolean {
</li>
</ul>
</nav>
<ElementSiblingNavigation
:current-element-id="element.id"
:sibling-elements="siblingElements"
data-cy-prefix="element"
route-name="element"
/>
</section>
</main>
</div>

View file

@ -42,7 +42,9 @@ const testimonials: Testimonial[] = [
},
{
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',
@ -267,7 +269,8 @@ function projectCardStyle(project: Project): string {
<div class="hero__text">
<h1 class="hero__headline">
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>
<p class="hero__attribution">Rabbi Yehoshua Gerzi</p>
</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="M30 65 C45 55, 60 60, 75 50 S100 55, 110 65" />
</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">
Rabbi Yehoshua Gerzi has dedicated his life to empowering individuals to achieve a state
of &ldquo;Healthy, Integrated and Balanced Torah Living.&rdquo; His teachings weave
together timeless Torah wisdom, mystical insights and practical guidance, making ancient
wisdom accessible and transformative for modern lives.
Rabbi Yehoshua Gerzi has dedicated his life to empowering individuals
to achieve a state of &ldquo;Healthy, Integrated and Balanced Torah
Living.&rdquo; His teachings weave together timeless Torah wisdom,
mystical insights and practical guidance, making ancient wisdom
accessible and transformative for modern lives.
</p>
<p class="discover-path__paragraph">
Rabbi Gerzi is based in Ramat Beit Shemesh, where he serves as Rav of Kehillas Beis David
and Director of the Pilzno Institute of Higher Learning.
Rabbi Gerzi is based in Ramat Beit Shemesh, where he serves as Rav of
Kehillas Beis David and Director of the Pilzno Institute of Higher
Learning.
</p>
</div>
</section>
<section class="core-teachings" data-cy="core-teachings">
<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
v-for="teaching in coreTeachings"
@ -323,7 +332,9 @@ function projectCardStyle(project: Project): string {
<span class="core-teachings__glyph" aria-hidden="true">
{{ teaching.glyph }}
</span>
<p class="core-teachings__label">{{ teaching.number }}) {{ teaching.title }}</p>
<p class="core-teachings__label">
{{ teaching.number }}) {{ teaching.title }}
</p>
</div>
</div>
</div>
@ -348,13 +359,18 @@ function projectCardStyle(project: Project): string {
</svg>
<h2 class="unique-voice__heading">A Unique Voice for the Generation</h2>
<p class="unique-voice__paragraph">
Rabbi Gerzi provides a wholly unique and much needed perspective to his students. Carrying
decades of experience guided by a handful of the generations most pre-eminent Rabbanim, he
teaches a message of practically engaged positivity. His Torah is one of unity, bridging
hashkafic boundaries and engaging a diverse cadre of Jews who are open minded to embracing
meaningful and experiential Judaism.
Rabbi Gerzi provides a wholly unique and much needed perspective to
his students. Carrying decades of experience guided by a handful of
the generations most pre-eminent Rabbanim, he teaches a message of
practically engaged positivity. His Torah is one of unity, bridging
hashkafic boundaries and engaging a diverse cadre of Jews who are open
minded to embracing meaningful and experiential Judaism.
</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
</button>
</div>
@ -373,7 +389,9 @@ function projectCardStyle(project: Project): string {
:aria-hidden="testimonialPosition(index) !== 'active'"
>
<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 class="testimonials__controls">
@ -401,7 +419,9 @@ function projectCardStyle(project: Project): string {
<section class="sponsors" data-cy="sponsors">
<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__track" data-cy="sponsor-track">
<div
@ -447,9 +467,15 @@ function projectCardStyle(project: Project): string {
<div class="journey__inner">
<h2 class="journey__heading">Start Your Journey</h2>
<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>
<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
class="journey__card journey__card--slate"
@ -470,7 +496,8 @@ function projectCardStyle(project: Project): string {
<div class="get-involved__copy">
<h2 class="get-involved__heading">Get Involved</h2>
<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>
</div>
<form class="get-involved__form" @submit="submitNewsletter">

View file

@ -45,7 +45,11 @@ async function handleSubmit(): Promise<void> {
/>
</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 }}
</p>