Compare commits

...

7 commits

8 changed files with 308 additions and 337 deletions

View file

@ -12,6 +12,16 @@ describe('homepage hero', () => {
}) })
}) })
it('links the navbar Donate item to Causematch', () => {
const donateHref = 'https://causematch.com/pilzno_donate'
cy.contains('header.site-header a', 'Donate')
.should('be.visible')
.and('have.attr', 'href', donateHref)
.and('have.attr', 'target', '_blank')
.and('have.attr', 'rel', 'noopener')
})
it('opens the Torah Media page from the header navigation', () => { it('opens the Torah Media page from the header navigation', () => {
cy.contains('header.site-header a', 'Torah Media').click() cy.contains('header.site-header a', 'Torah Media').click()
@ -572,4 +582,65 @@ describe('homepage get involved and footer', () => {
cy.contains(/Rabbi Gerzi 2025/).should('be.visible') cy.contains(/Rabbi Gerzi 2025/).should('be.visible')
}) })
}) })
it('links the footer Donate item to Causematch', () => {
const donateHref = 'https://causematch.com/pilzno_donate'
cy.contains('[data-cy="footer"] a', 'Donate')
.should('be.visible')
.and('have.attr', 'href', donateHref)
.and('have.attr', 'target', '_blank')
.and('have.attr', 'rel', 'noopener')
})
it('links footer social icons to the contact social pages', () => {
const expectedSocialLinks = [
{
label: 'YouTube',
href: 'https://www.youtube.com/@rabbigerzi',
},
{
label: 'Spotify',
href:
'https://open.spotify.com/show/' +
'49H1X9A7KZWAnnIbcoQXQR?si=6253691f23be44ad',
},
]
cy.get('[data-cy="footer"]').within(() => {
expectedSocialLinks.forEach((expectedSocialLink) => {
cy.get(`a[aria-label="${expectedSocialLink.label}"]`)
.should('be.visible')
.and('have.attr', 'href', expectedSocialLink.href)
.and('have.attr', 'target', '_blank')
.and('have.attr', 'rel', 'noopener')
})
})
})
it('navigates footer links without reloading the app', () => {
const footerNavigationMarker = 'client-navigation'
cy.window().then((applicationWindow) => {
const markedWindow = applicationWindow as Window & {
footerNavigationMarker?: string
}
markedWindow.footerNavigationMarker = footerNavigationMarker
})
cy.contains('[data-cy="footer"] a', 'About').click()
cy.location('pathname').should('eq', '/about')
cy.window().then((applicationWindow) => {
const markedWindow = applicationWindow as Window & {
footerNavigationMarker?: string
}
expect(markedWindow.footerNavigationMarker).to.equal(
footerNavigationMarker,
)
})
cy.contains('h1', 'About Rabbi Gerzi').should('be.visible')
})
}) })

View file

@ -0,0 +1,180 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { donateLinkAttributes } from '@/content/donateLink'
import { socialLinks, type SocialLinkLabel } from '@/content/socialLinks'
interface RouteFooterLink {
kind: 'route'
label: string
to: string
}
interface ExternalFooterLink {
kind: 'external'
label: string
linkAttributes: typeof donateLinkAttributes
}
type FooterLink = RouteFooterLink | ExternalFooterLink
const signatureImage = {
src: '/assets/signature.png',
alt: "R' Yehoshua Gerzi",
}
const socialGlyphs: Record<SocialLinkLabel, string> = {
YouTube: '▶',
Spotify: '♫',
}
const footerSocialLinks = socialLinks.map((socialLink) => {
return {
label: socialLink.label,
href: socialLink.href,
glyph: socialGlyphs[socialLink.label],
}
})
const footerLinkColumns: FooterLink[][] = [
[
{ kind: 'route', label: 'Torah Media', to: '/media' },
{ kind: 'route', label: 'About', to: '/about' },
],
[
{
kind: 'external',
label: 'Donate',
linkAttributes: donateLinkAttributes,
},
{ kind: 'route', label: 'Contact', to: '/contact' },
],
]
</script>
<template>
<footer class="site-footer" data-cy="footer">
<div class="site-footer__inner">
<div class="site-footer__brand">
<img v-bind="signatureImage" class="site-footer__signature" />
<div class="site-footer__social">
<a
v-for="socialLink in footerSocialLinks"
:key="socialLink.label"
:href="socialLink.href"
class="site-footer__social-link"
:aria-label="socialLink.label"
target="_blank"
rel="noopener"
>
{{ socialLink.glyph }}
</a>
</div>
</div>
<nav class="site-footer__links" aria-label="Footer navigation">
<ul
v-for="(footerLinkColumn, footerLinkColumnIndex) in footerLinkColumns"
:key="footerLinkColumnIndex"
class="site-footer__link-column"
>
<li v-for="footerLink in footerLinkColumn" :key="footerLink.label">
<RouterLink v-if="footerLink.kind === 'route'" :to="footerLink.to">
{{ footerLink.label }}
</RouterLink>
<a v-else v-bind="footerLink.linkAttributes">
{{ footerLink.label }}
</a>
</li>
</ul>
</nav>
</div>
<p class="site-footer__copyright">&copy; Rabbi Gerzi 2025</p>
</footer>
</template>
<style scoped>
.site-footer {
padding: 4rem 2rem 2rem;
background: var(--color-white);
border-top: 1px solid var(--color-border);
}
.site-footer__inner {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: start;
max-width: 1100px;
margin: 0 auto 2rem;
gap: 2rem;
}
.site-footer__brand {
display: flex;
flex-direction: column;
gap: 1rem;
}
.site-footer__signature {
width: auto;
height: 40px;
}
.site-footer__social {
display: flex;
gap: 0.75rem;
}
.site-footer__social-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 6px;
}
.site-footer__social-link:hover {
background: var(--color-cream);
}
.site-footer__links {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.site-footer__link-column {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
gap: 0.6rem;
list-style: none;
}
.site-footer__link-column a {
color: var(--color-text);
font-family: var(--font-sans);
font-size: 0.95rem;
}
.site-footer__link-column a:hover {
color: var(--color-slate);
}
.site-footer__copyright {
margin: 0;
color: var(--color-text-muted);
font-family: var(--font-sans);
font-size: 0.85rem;
text-align: center;
}
@media (max-width: 860px) {
.site-footer__inner,
.site-footer__links {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue'
import { RouterLink, useRoute, type RouteLocationRaw } from 'vue-router' import { RouterLink, useRoute, type RouteLocationRaw } from 'vue-router'
import { donateLinkAttributes } from '@/content/donateLink'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
interface ContextualNavAction { interface ContextualNavAction {
@ -8,6 +9,15 @@ interface ContextualNavAction {
to: RouteLocationRaw to: RouteLocationRaw
} }
interface HeaderNavItem {
label: string
icon: string
}
interface InternalHeaderNavItem extends HeaderNavItem {
href: string
}
const authStore = useAuthStore() const authStore = useAuthStore()
const route = useRoute() const route = useRoute()
@ -24,13 +34,17 @@ async function handleLogout(): Promise<void> {
await authStore.logout() await authStore.logout()
} }
const navItems = [ const navItems: InternalHeaderNavItem[] = [
{ label: 'Torah Media', href: '/media', icon: '▶' }, { label: 'Torah Media', href: '/media', icon: '▶' },
{ label: 'About', href: '/about', icon: 'ⓘ' }, { label: 'About', href: '/about', icon: 'ⓘ' },
{ label: 'Contact', href: '/contact', icon: '➤' }, { label: 'Contact', href: '/contact', icon: '➤' },
{ label: 'Donate', href: '/donate', icon: '♡' },
] ]
const donateNavItem: HeaderNavItem = {
label: 'Donate',
icon: '♡',
}
const routeElementId = computed(() => { const routeElementId = computed(() => {
const currentRouteElementId = route.params.id const currentRouteElementId = route.params.id
@ -85,6 +99,12 @@ const contextualNavAction = computed<ContextualNavAction | null>(() => {
</span> </span>
<span>{{ item.label }}</span> <span>{{ item.label }}</span>
</RouterLink> </RouterLink>
<a v-bind="donateLinkAttributes" class="site-header__nav-link">
<span class="site-header__nav-icon" aria-hidden="true">
{{ donateNavItem.icon }}
</span>
<span>{{ donateNavItem.label }}</span>
</a>
<RouterLink <RouterLink
v-if="contextualNavAction !== null" v-if="contextualNavAction !== null"
:to="contextualNavAction.to" :to="contextualNavAction.to"

View file

@ -0,0 +1,7 @@
export const donateHref = 'https://causematch.com/pilzno_donate'
export const donateLinkAttributes = {
href: donateHref,
target: '_blank',
rel: 'noopener',
}

View file

@ -0,0 +1,19 @@
export type SocialLinkLabel = 'YouTube' | 'Spotify'
export interface SocialLink {
label: SocialLinkLabel
href: string
}
const spotifyShowPath = '49H1X9A7KZWAnnIbcoQXQR?si=6253691f23be44ad'
export const socialLinks: SocialLink[] = [
{
label: 'YouTube',
href: 'https://www.youtube.com/@rabbigerzi',
},
{
label: 'Spotify',
href: 'https://open.spotify.com/show/' + spotifyShowPath,
},
]

View file

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import SiteHeader from '@/components/SiteHeader.vue' import SiteHeader from '@/components/SiteHeader.vue'
import SiteFooter from '@/components/SiteFooter.vue'
interface MissionPoint { interface MissionPoint {
title: string title: string
@ -426,28 +427,7 @@ function submitNewsletter(submitEvent: Event): void {
</section> </section>
</main> </main>
<footer class="site-footer" data-cy="footer"> <SiteFooter />
<div class="site-footer__inner">
<div class="site-footer__brand">
<img src="/assets/signature.png" alt="R' Yehoshua Gerzi" class="site-footer__signature" />
<div class="site-footer__social">
<a href="#" class="site-footer__social-link" aria-label="YouTube"> &#9654; </a>
<a href="#" class="site-footer__social-link" aria-label="Spotify"> &#9835; </a>
</div>
</div>
<nav class="site-footer__links">
<ul class="site-footer__link-column">
<li><a href="/media">Torah Media</a></li>
<li><a href="/about">About</a></li>
</ul>
<ul class="site-footer__link-column">
<li><a href="/donate">Donate</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
<p class="site-footer__copyright">&copy; Rabbi Gerzi 2025</p>
</footer>
</div> </div>
</template> </template>
@ -802,90 +782,10 @@ function submitNewsletter(submitEvent: Event): void {
background: var(--color-slate); background: var(--color-slate);
} }
.site-footer {
padding: 4rem 2rem 2rem;
background: var(--color-white);
border-top: 1px solid var(--color-border);
}
.site-footer__inner {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: start;
max-width: 1100px;
margin: 0 auto 2rem;
gap: 2rem;
}
.site-footer__brand {
display: flex;
flex-direction: column;
gap: 1rem;
}
.site-footer__signature {
width: auto;
height: 40px;
}
.site-footer__social {
display: flex;
gap: 0.75rem;
}
.site-footer__social-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 6px;
}
.site-footer__social-link:hover {
background: var(--color-cream);
}
.site-footer__links {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.site-footer__link-column {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
gap: 0.6rem;
list-style: none;
}
.site-footer__link-column a {
color: var(--color-text);
font-family: var(--font-sans);
font-size: 0.95rem;
}
.site-footer__link-column a:hover {
color: var(--color-slate);
}
.site-footer__copyright {
margin: 0;
color: var(--color-text-muted);
font-family: var(--font-sans);
font-size: 0.85rem;
text-align: center;
}
@media (max-width: 860px) { @media (max-width: 860px) {
.about-hero__inner, .about-hero__inner,
.about-section__inner--split, .about-section__inner--split,
.get-involved__card, .get-involved__card {
.site-footer__inner {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@ -924,9 +824,5 @@ function submitNewsletter(submitEvent: Event): void {
padding: 2rem; padding: 2rem;
gap: 2rem; gap: 2rem;
} }
.site-footer__links {
grid-template-columns: 1fr;
}
} }
</style> </style>

View file

@ -1,11 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import SiteHeader from '@/components/SiteHeader.vue' import SiteHeader from '@/components/SiteHeader.vue'
import SiteFooter from '@/components/SiteFooter.vue'
interface SocialLink { import { socialLinks } from '@/content/socialLinks'
label: string
href: string
}
const contactFirstName = ref('') const contactFirstName = ref('')
const contactLastName = ref('') const contactLastName = ref('')
@ -16,17 +13,6 @@ const contactMessage = ref('')
const newsletterFullName = ref('') const newsletterFullName = ref('')
const newsletterEmail = ref('') const newsletterEmail = ref('')
const socialLinks: SocialLink[] = [
{
label: 'YouTube',
href: 'https://www.youtube.com/@rabbigerzi',
},
{
label: 'Spotify',
href: 'https://open.spotify.com/show/49H1X9A7KZWAnnIbcoQXQR?si=6253691f23be44ad',
},
]
function submitContact(submitEvent: Event): void { function submitContact(submitEvent: Event): void {
submitEvent.preventDefault() submitEvent.preventDefault()
contactFirstName.value = '' contactFirstName.value = ''
@ -187,28 +173,7 @@ function submitNewsletter(submitEvent: Event): void {
</section> </section>
</main> </main>
<footer class="site-footer" data-cy="footer"> <SiteFooter />
<div class="site-footer__inner">
<div class="site-footer__brand">
<img src="/assets/signature.png" alt="R' Yehoshua Gerzi" class="site-footer__signature" />
<div class="site-footer__social">
<a href="#" class="site-footer__social-link" aria-label="YouTube"> &#9654; </a>
<a href="#" class="site-footer__social-link" aria-label="Spotify"> &#9835; </a>
</div>
</div>
<nav class="site-footer__links">
<ul class="site-footer__link-column">
<li><a href="/media">Torah Media</a></li>
<li><a href="/about">About</a></li>
</ul>
<ul class="site-footer__link-column">
<li><a href="/donate">Donate</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
<p class="site-footer__copyright">&copy; Rabbi Gerzi 2025</p>
</footer>
</div> </div>
</template> </template>
@ -440,85 +405,6 @@ function submitNewsletter(submitEvent: Event): void {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.site-footer {
padding: 4rem 2rem 2rem;
background: var(--color-white);
border-top: 1px solid var(--color-border);
}
.site-footer__inner {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: start;
max-width: 1100px;
margin: 0 auto 2rem;
gap: 2rem;
}
.site-footer__brand {
display: flex;
flex-direction: column;
gap: 1rem;
}
.site-footer__signature {
width: auto;
height: 40px;
}
.site-footer__social {
display: flex;
gap: 0.75rem;
}
.site-footer__social-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 6px;
}
.site-footer__social-link:hover {
background: var(--color-cream);
}
.site-footer__links {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.site-footer__link-column {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
gap: 0.6rem;
list-style: none;
}
.site-footer__link-column a {
color: var(--color-text);
font-family: var(--font-sans);
font-size: 0.95rem;
}
.site-footer__link-column a:hover {
color: var(--color-slate);
}
.site-footer__copyright {
margin: 0;
color: var(--color-text-muted);
font-family: var(--font-sans);
font-size: 0.85rem;
text-align: center;
}
@media (max-width: 860px) { @media (max-width: 860px) {
.contact-hero { .contact-hero {
padding: 3rem 1rem; padding: 3rem 1rem;
@ -530,8 +416,7 @@ function submitNewsletter(submitEvent: Event): void {
.contact-main__inner, .contact-main__inner,
.contact-form__grid, .contact-form__grid,
.get-involved__card, .get-involved__card {
.site-footer__inner {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@ -543,9 +428,5 @@ function submitNewsletter(submitEvent: Event): void {
padding: 2rem; padding: 2rem;
gap: 2rem; gap: 2rem;
} }
.site-footer__links {
grid-template-columns: 1fr;
}
} }
</style> </style>

View file

@ -2,6 +2,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import SiteHeader from '@/components/SiteHeader.vue' import SiteHeader from '@/components/SiteHeader.vue'
import SiteFooter from '@/components/SiteFooter.vue'
const router = useRouter() const router = useRouter()
@ -498,28 +499,7 @@ function projectCardStyle(project: Project): string {
</div> </div>
</section> </section>
<footer class="site-footer" data-cy="footer"> <SiteFooter />
<div class="site-footer__inner">
<div class="site-footer__brand">
<img src="/assets/signature.png" alt="R' Yehoshua Gerzi" class="site-footer__signature" />
<div class="site-footer__social">
<a href="#" class="site-footer__social-link" aria-label="YouTube"></a>
<a href="#" class="site-footer__social-link" aria-label="Spotify"></a>
</div>
</div>
<nav class="site-footer__links">
<ul class="site-footer__link-column">
<li><a href="/media">Torah Media</a></li>
<li><a href="/about">About</a></li>
</ul>
<ul class="site-footer__link-column">
<li><a href="/donate">Donate</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
<p class="site-footer__copyright">© Rabbi Gerzi 2025</p>
</footer>
</div> </div>
</template> </template>
@ -1200,85 +1180,6 @@ function projectCardStyle(project: Project): string {
background: var(--color-slate); background: var(--color-slate);
} }
.site-footer {
background: var(--color-white);
padding: 4rem 2rem 2rem;
border-top: 1px solid var(--color-border);
}
.site-footer__inner {
max-width: 1100px;
margin: 0 auto 2rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: start;
}
.site-footer__brand {
display: flex;
flex-direction: column;
gap: 1rem;
}
.site-footer__signature {
height: 40px;
width: auto;
}
.site-footer__social {
display: flex;
gap: 0.75rem;
}
.site-footer__social-link {
width: 2rem;
height: 2rem;
border-radius: 6px;
border: 1px solid var(--color-border);
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
}
.site-footer__social-link:hover {
background: var(--color-cream);
}
.site-footer__links {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.site-footer__link-column {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.site-footer__link-column a {
font-family: var(--font-sans);
font-size: 0.95rem;
color: var(--color-text);
}
.site-footer__link-column a:hover {
color: var(--color-slate);
}
.site-footer__copyright {
text-align: center;
font-family: var(--font-sans);
font-size: 0.85rem;
color: var(--color-text-muted);
margin: 0;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.testimonials { .testimonials {
--testimonials-card-height: 320px; --testimonials-card-height: 320px;
@ -1346,10 +1247,6 @@ function projectCardStyle(project: Project): string {
padding: 2rem; padding: 2rem;
gap: 2rem; gap: 2rem;
} }
.site-footer__inner {
grid-template-columns: 1fr;
}
} }
@media (max-width: 480px) { @media (max-width: 480px) {