diff --git a/frontend/rabbi_gerzi/cypress/e2e/home.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/home.cy.ts index 2e9e197..1b2adae 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/home.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/home.cy.ts @@ -162,6 +162,220 @@ describe('homepage testimonials carousel', () => { }) }) + it('shows faded half-card previews beside the active testimonial', () => { + cy.viewport(1200, 800) + cy.get('[data-cy="testimonials"]').within(() => { + cy.get('[data-cy="testimonial-active"]').as('activeCard') + cy.get('[data-cy="testimonial-previous-preview"]').as( + 'previousPreview', + ) + cy.get('[data-cy="testimonial-next-preview"]').as('nextPreview') + cy.get('[data-cy="testimonials-stage"]').as('testimonialStage') + }) + + cy.get('@activeCard').should('be.visible') + cy.get('@previousPreview').should('be.visible') + cy.get('@nextPreview').should('be.visible') + + cy.get('@activeCard').then((activeCardElements) => { + const activeOpacity = Number( + getComputedStyle(activeCardElements[0]).opacity, + ) + + cy.get('@previousPreview').then((previousPreviewElements) => { + const previousPreview = previousPreviewElements[0] + const previousOpacity = Number( + getComputedStyle(previousPreview).opacity, + ) + + cy.wrap(previousOpacity).should('be.lessThan', activeOpacity) + }) + + cy.get('@nextPreview').then((nextPreviewElements) => { + const nextPreview = nextPreviewElements[0] + const nextOpacity = Number(getComputedStyle(nextPreview).opacity) + + cy.wrap(nextOpacity).should('be.lessThan', activeOpacity) + }) + }) + + cy.get('@testimonialStage').then((testimonialStageElements) => { + const stageBounds = testimonialStageElements[0].getBoundingClientRect() + + cy.get('@previousPreview').then((previousPreviewElements) => { + const previousBounds = + previousPreviewElements[0].getBoundingClientRect() + const previousVisibleWidth = previousBounds.right - stageBounds.left + + cy.wrap(previousVisibleWidth).should( + 'be.closeTo', + previousBounds.width / 2, + 1, + ) + }) + + cy.get('@nextPreview').then((nextPreviewElements) => { + const nextBounds = nextPreviewElements[0].getBoundingClientRect() + const nextVisibleWidth = stageBounds.right - nextBounds.left + + cy.wrap(nextVisibleWidth).should( + 'be.closeTo', + nextBounds.width / 2, + 1, + ) + }) + }) + }) + + it('keeps desktop cards equal sized and separated', () => { + const expectedCardGap = 24 + const expectedControlsGap = 12 + + cy.viewport(1200, 800) + cy.get('[data-cy="testimonials"]').within(() => { + cy.get('[data-cy="testimonial-active"]').as('activeCard') + cy.get('[data-cy="testimonial-previous-preview"]').as( + 'previousPreview', + ) + cy.get('[data-cy="testimonial-next-preview"]').as('nextPreview') + cy.get('[data-cy="testimonial-prev"]') + .parent() + .as('testimonialControls') + }) + + cy.get('@activeCard').then((activeCardElements) => { + const activeBounds = activeCardElements[0].getBoundingClientRect() + + cy.get('@previousPreview').then((previousPreviewElements) => { + const previousBounds = + previousPreviewElements[0].getBoundingClientRect() + const previousGap = activeBounds.left - previousBounds.right + + cy.wrap(previousBounds.height).should( + 'be.closeTo', + activeBounds.height, + 1, + ) + cy.wrap(previousGap).should('be.closeTo', expectedCardGap, 1) + }) + + cy.get('@nextPreview').then((nextPreviewElements) => { + const nextBounds = nextPreviewElements[0].getBoundingClientRect() + const nextGap = nextBounds.left - activeBounds.right + + cy.wrap(nextBounds.height).should( + 'be.closeTo', + activeBounds.height, + 1, + ) + cy.wrap(nextGap).should('be.closeTo', expectedCardGap, 1) + }) + + cy.get('@testimonialControls').then((testimonialControlsElements) => { + const controlsBounds = + testimonialControlsElements[0].getBoundingClientRect() + const controlsGap = controlsBounds.top - activeBounds.bottom + + cy.wrap(controlsGap).should( + 'be.closeTo', + expectedControlsGap, + 1, + ) + }) + }) + }) + + it('rounds the outer corners of clipped preview cards', () => { + cy.viewport(1200, 800) + cy.get('[data-cy="testimonials"]').within(() => { + cy.get('[data-cy="testimonials-stage"]').as('testimonialStage') + cy.get('[data-cy="testimonial-active"]').as('activeCard') + }) + + cy.get('@activeCard').then((activeCardElements) => { + const activeCardStyle = getComputedStyle(activeCardElements[0]) + + cy.get('@testimonialStage').then((testimonialStageElements) => { + const testimonialStageStyle = getComputedStyle( + testimonialStageElements[0], + ) + + expect(testimonialStageStyle.borderTopLeftRadius).to.equal( + activeCardStyle.borderTopLeftRadius, + ) + expect(testimonialStageStyle.borderTopRightRadius).to.equal( + activeCardStyle.borderTopRightRadius, + ) + expect(testimonialStageStyle.borderBottomLeftRadius).to.equal( + activeCardStyle.borderBottomLeftRadius, + ) + expect(testimonialStageStyle.borderBottomRightRadius).to.equal( + activeCardStyle.borderBottomRightRadius, + ) + }) + }) + }) + + it('brings entering side previews in from the clicked direction', () => { + const transitionSampleDelay = 100 + const transitionSettleDelay = 1100 + + cy.viewport(1200, 800) + cy.get('[data-cy="testimonials"]').within(() => { + cy.get('[data-cy="testimonial-prev"]').click() + cy.get('[data-cy="testimonial-previous-preview"]').as( + 'enteringPreviousPreview', + ) + }) + + cy.wait(transitionSampleDelay) + cy.get('@enteringPreviousPreview').then((previousPreviewElements) => { + const earlyPreviousLeft = + previousPreviewElements[0].getBoundingClientRect().left + + cy.wait(transitionSettleDelay) + cy.get('@enteringPreviousPreview').then((finalPreviewElements) => { + const finalPreviousLeft = + finalPreviewElements[0].getBoundingClientRect().left + + expect(earlyPreviousLeft).to.be.lessThan(finalPreviousLeft) + }) + }) + + cy.visit('/') + cy.viewport(1200, 800) + cy.get('[data-cy="testimonials"]').within(() => { + cy.get('[data-cy="testimonial-next"]').click() + cy.get('[data-cy="testimonial-next-preview"]').as( + 'enteringNextPreview', + ) + }) + + cy.wait(transitionSampleDelay) + cy.get('@enteringNextPreview').then((nextPreviewElements) => { + const earlyNextLeft = + nextPreviewElements[0].getBoundingClientRect().left + + cy.wait(transitionSettleDelay) + cy.get('@enteringNextPreview').then((finalPreviewElements) => { + const finalNextLeft = + finalPreviewElements[0].getBoundingClientRect().left + + expect(earlyNextLeft).to.be.greaterThan(finalNextLeft) + }) + }) + }) + + it('hides side previews on narrow screens', () => { + cy.viewport(390, 800) + cy.get('[data-cy="testimonials"]').within(() => { + cy.get('[data-cy="testimonial-active"]').should('be.visible') + cy.get('[data-cy="testimonial-previous-preview"]') + .should('not.be.visible') + cy.get('[data-cy="testimonial-next-preview"]').should('not.be.visible') + }) + }) + it('advances to the next testimonial when next is clicked', () => { cy.get('[data-cy="testimonials"]').within(() => { cy.get('[data-cy="testimonial-active"]') diff --git a/frontend/rabbi_gerzi/src/views/HomePage.vue b/frontend/rabbi_gerzi/src/views/HomePage.vue index 1a28333..32ae304 100644 --- a/frontend/rabbi_gerzi/src/views/HomePage.vue +++ b/frontend/rabbi_gerzi/src/views/HomePage.vue @@ -23,6 +23,8 @@ interface Testimonial { quote: string } +type TestimonialPosition = 'active' | 'previous' | 'next' | 'hidden' + const testimonials: Testimonial[] = [ { name: 'Nathaniel M', @@ -41,16 +43,109 @@ const testimonials: Testimonial[] = [ name: 'Anonymous Student', quote: 'My understanding of Torah has been profoundly improved through ' + 'his teachings.', }, + { + name: 'Placeholder Student 1', + quote: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do ' + + 'eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + { + name: 'Placeholder Student 2', + quote: + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco ' + + 'laboris nisi ut aliquip ex ea commodo consequat.', + }, + { + name: 'Placeholder Student 3', + quote: + 'Duis aute irure dolor in reprehenderit in voluptate velit esse ' + + 'cillum dolore eu fugiat nulla pariatur.', + }, ] const testimonialIndex = ref(0) +function testimonialOffset(index: number): number { + const relativeIndex = index - testimonialIndex.value + testimonials.length + + return relativeIndex % testimonials.length +} + +function previousTestimonialIndex(): number { + if (testimonialIndex.value === 0) { + return testimonials.length - 1 + } + + return testimonialIndex.value - 1 +} + +function nextTestimonialIndex(): number { + return (testimonialIndex.value + 1) % testimonials.length +} + +function testimonialPosition(index: number): TestimonialPosition { + const offset = testimonialOffset(index) + + if (offset === 0) { + return 'active' + } + + if (offset === testimonials.length - 1) { + return 'previous' + } + + if (offset === 1) { + return 'next' + } + + return 'hidden' +} + +function testimonialDataCy(index: number): string { + const position = testimonialPosition(index) + + if (position === 'active') { + return 'testimonial-active' + } + + if (position === 'previous') { + return 'testimonial-previous-preview' + } + + if (position === 'next') { + return 'testimonial-next-preview' + } + + return 'testimonial-hidden' +} + +function testimonialHiddenClass(index: number): string { + const offset = testimonialOffset(index) + + if (offset < testimonials.length / 2) { + return 'testimonials__card--hidden-next' + } + + return 'testimonials__card--hidden-previous' +} + +function testimonialClass(index: number): string[] { + const position = testimonialPosition(index) + const positionClass = `testimonials__card--${position}` + + if (position === 'hidden') { + return [positionClass, testimonialHiddenClass(index)] + } + + return [positionClass] +} + function nextTestimonial(): void { - testimonialIndex.value = (testimonialIndex.value + 1) % testimonials.length + testimonialIndex.value = nextTestimonialIndex() } function previousTestimonial(): void { - testimonialIndex.value = (testimonialIndex.value - 1 + testimonials.length) % testimonials.length + testimonialIndex.value = previousTestimonialIndex() } const newsletterFullName = ref('') @@ -229,16 +324,14 @@ const projects: Project[] = [

What People Are Saying

-
+

{{ testimonial.name }}

“{{ testimonial.quote }}”

@@ -648,10 +741,15 @@ const projects: Project[] = [ .testimonials { background: var(--color-cream); padding: 5rem 2rem; + --testimonials-card-entry-gap: 48px; + --testimonials-card-gap: 24px; + --testimonials-card-height: 220px; + --testimonials-controls-gap: 0.75rem; + --testimonials-stage-width: 1008px; } .testimonials__inner { - max-width: 960px; + max-width: var(--testimonials-stage-width); margin: 0 auto; text-align: center; } @@ -666,7 +764,11 @@ const projects: Project[] = [ .testimonials__stage { position: relative; - min-height: 220px; + width: min(100%, var(--testimonials-stage-width)); + height: var(--testimonials-card-height); + border-radius: 14px; + margin: 0 auto; + overflow: hidden; } .testimonials__card { @@ -674,18 +776,50 @@ const projects: Project[] = [ border-radius: 14px; padding: 2rem; text-align: left; - max-width: 480px; - margin: 0 auto; + width: calc(50% - var(--testimonials-card-gap)); + height: var(--testimonials-card-height); opacity: 0; visibility: hidden; position: absolute; - inset: 0; - transition: opacity 0.3s ease; + top: 0; + left: 50%; + margin: 0; + box-sizing: border-box; + pointer-events: none; + transform: translateX(-50%); + transition: + opacity 1s cubic-bezier(0.22, 1, 0.36, 1), + transform 1s cubic-bezier(0.22, 1, 0.36, 1); } .testimonials__card--active { opacity: 1; visibility: visible; + z-index: 2; + transform: translateX(-50%); +} + +.testimonials__card--previous, +.testimonials__card--next { + opacity: 0.35; + visibility: visible; + z-index: 1; +} + +.testimonials__card--previous { + transform: translateX(calc(-150% - var(--testimonials-card-gap))); +} + +.testimonials__card--next { + transform: translateX(calc(50% + var(--testimonials-card-gap))); +} + +.testimonials__card--hidden-previous { + transform: translateX(calc(-250% - var(--testimonials-card-entry-gap))); +} + +.testimonials__card--hidden-next { + transform: translateX(calc(150% + var(--testimonials-card-entry-gap))); } .testimonials__name { @@ -709,7 +843,7 @@ const projects: Project[] = [ display: flex; justify-content: center; gap: 0.75rem; - margin-top: 2rem; + margin-top: var(--testimonials-controls-gap); } .testimonials__arrow { @@ -1050,6 +1184,10 @@ const projects: Project[] = [ } @media (max-width: 768px) { + .testimonials { + --testimonials-card-height: 320px; + } + .hero__content { grid-template-columns: 1fr; text-align: center; @@ -1069,6 +1207,27 @@ const projects: Project[] = [ grid-template-columns: 1fr; } + .testimonials__stage { + width: min(100%, 480px); + } + + .testimonials__card { + width: 100%; + left: 0; + transform: none; + } + + .testimonials__card--active { + transform: none; + } + + .testimonials__card--previous, + .testimonials__card--next { + opacity: 0; + visibility: hidden; + transform: none; + } + .journey__cards { grid-template-columns: 1fr; }