PageSourceSearch

https://rs-dts.pages.dev/testimonial-slider.js?agent=dean-souza

js rs-dts.pages.dev collected 2026-10-03 05:54:36 UTC 45,477 bytes, 1,215 lines download raw bytes

1(function() {
2    const currentScript = document.currentScript || (function() {
3        const scripts = document.getElementsByTagName('script');
4        return scripts[scripts.length - 1];
5    })();
6    
7    const scriptSrc = currentScript.src;
8    const urlParams = new URLSearchParams(scriptSrc.split('?')[1] || '');
9    const agentSlug = urlParams.get('agent') || 'dean-souza';
10
11    const RSS_BASE_URL = 'https://rss.realsatisfied.com/rss/v1/agent/';
12    const REALSATISFIED_BASE_URL = 'https://www.realsatisfied.com/';
13    const MAX_VISIBLE_DOTS = 5;
14    
15    const styles = `
16        .rs-testimonial-container {
17            /* Mobile-first: Start with mobile styles */
18            width: 100%;
19            min-height: min(80dvh, 400px);
20            max-width: min(95dvw, 400px);
21            position: relative;
22            background: #ffffff;
23            border-radius: clamp(12px, 2.5vw, 20px);
24            box-shadow: 
25                0 4px 20px rgba(0, 0, 0, 0.1),
26                0 1px 3px rgba(0, 0, 0, 0.05);
27            overflow: hidden;
28            font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
29            margin: 0 auto;
30            container-type: inline-size;
31            /* Modern CSS: Intrinsic web design */
32            display: grid;
33            grid-template-rows: auto 1fr auto;
34        }
35        
36        /* Tablet and up */
37        @media (min-width: 48rem) {
38            .rs-testimonial-container {
39                max-width: min(90dvw, 600px);
40                min-height: min(70dvh, 500px);
41            }
42        }
43        
44        /* Desktop and up */
45        @media (min-width: 64rem) {
46            .rs-testimonial-container {
47                max-width: min(80dvw, 700px);
48                min-height: min(65dvh, 550px);
49                box-shadow: 
50                    0 20px 60px rgba(0, 0, 0, 0.15),
51                    0 4px 12px rgba(0, 0, 0, 0.1);
52            }
53        }
54        
55        /* Large screens */
56        @media (min-width: 80rem) {
57            .rs-testimonial-container {
58                max-width: min(70dvw, 900px);
59            }
60        }
61        
62        .rs-testimonial-header {
63            background: linear-gradient(135deg, #beaf87 0%, #d4c49a 100%);
64            padding: clamp(1rem, 4vw, 2rem);
65            text-align: center;
66            color: #252526;
67            /* Modern CSS: Logical properties */
68            padding-inline: clamp(1rem, 4vw, 2rem);
69            padding-block: clamp(1.25rem, 3vh, 2rem);
70        }
71        
72        .rs-agent-info h1 {
73            font-size: clamp(1.25rem, 4vw, 2rem);
74            font-weight: 700;
75            margin: 0 0 clamp(0.5rem, 1vh, 0.75rem) 0;
76            line-height: 1.2;
77            /* Modern CSS: Text balancing */
78            text-wrap: balance;
79        }
80        
81        .rs-agent-info p {
82            font-size: clamp(0.875rem, 2.5vw, 1.1rem);
83            opacity: 0.8;
84            margin: 0 0 clamp(0.75rem, 2vh, 1rem) 0;
85            text-wrap: balance;
86        }
87        
88        .rs-rating-stats {
89            display: grid;
90            grid-template-columns: repeat(auto-fit, minmax(4rem, 1fr));
91            gap: clamp(1rem, 4vw, 2rem);
92            max-width: 300px;
93            margin-inline: auto;
94        }
95        
96        /* Tablet and up: Switch to flex for better control */
97        @media (min-width: 48rem) {
98            .rs-rating-stats {
99                display: flex;
100                justify-content: center;
101                max-width: none;
102                gap: clamp(1.5rem, 5vw, 3rem);
103            }
104        }
105        
106        .rs-stat {
107            text-align: center;
108            display: flex;
109            flex-direction: column;
110            align-items: center;
111        }
112        
113        .rs-stat-number {
114            font-size: clamp(1.5rem, 5vw, 2.5rem);
115            font-weight: 700;
116            display: block;
117            line-height: 1;
118        }
119        
120        /* Tablet and up */
121        @media (min-width: 48rem) {
122            .rs-stat-number {
123                font-size: clamp(2rem, 4vw, 3.8rem);
124            }
125        }
126        
127        .rs-stat-label {
128            font-size: clamp(0.75rem, 2vw, 1.1rem);
129            opacity: 0.8;
130            margin-top: 0.25rem;
131        }
132        
133        /* Tablet and up */
134        @media (min-width: 48rem) {
135            .rs-stat-label {
136                font-size: clamp(0.875rem, 2.5vw, 1.4rem);
137            }
138        }
139        
140        .rs-slider-wrapper {
141            padding: clamp(1rem, 4vw, 1.5rem);
142            display: grid;
143            grid-template-rows: 1fr auto;
144            gap: clamp(1rem, 3vh, 1.5rem);
145            min-height: 0; /* Allow grid item to shrink */
146        }
147        
148        .rs-testimonial-slider {
149            overflow: hidden;
150            position: relative;
151            min-height: 0; /* Allow flex/grid item to shrink */
152        }
153        
154        .rs-testimonial-track {
155            display: flex;
156            transition: transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
157            align-items: stretch;
158            height: 100%;
159        }
160        
161        .rs-testimonial-slide {
162            min-width: 100%;
163            padding-inline: clamp(0.75rem, 3vw, 1.25rem);
164            box-sizing: border-box;
165            display: flex;
166            flex-direction: column;
167            justify-content: center;
168        }
169        
170        .rs-testimonial-content {
171            text-align: center;
172            display: flex;
173            flex-direction: column;
174            gap: clamp(0.75rem, 2vh, 1rem);
175        }
176        
177        .rs-quote-text {
178            font-size: clamp(0.95rem, 2.5vw, 1.7rem);
179            line-height: 1.6;
180            color: #333;
181            font-style: italic;
182            position: relative;
183            padding-inline: clamp(1.5rem, 6vw, 2rem);
184            transition: all 0.3s ease;
185            hyphens: auto;
186            text-wrap: pretty;
187        }
188        
189        /* Tablet and up */
190        @media (min-width: 48rem) {
191            .rs-quote-text {
192                font-size: clamp(1.1rem, 2.2vw, 1.7rem);
193                line-height: 1.7;
194                padding-inline: clamp(2rem, 5vw, 3rem);
195            }
196        }
197        
198        .rs-quote-text.truncated {
199            display: -webkit-box;
200            -webkit-box-orient: vertical;
201            -webkit-line-clamp: 4;
202            overflow: hidden;
203        }
204        
205        /* Tablet and up: Show more lines */
206        @media (min-width: 48rem) {
207            .rs-quote-text.truncated {
208                -webkit-line-clamp: 5;
209            }
210        }
211        
212        .rs-quote-text.expanded {
213            display: block;
214            overflow: visible;
215        }
216        
217        .rs-quote-text::before,
218        .rs-quote-text::after {
219            content: '"';
220            font-size: clamp(2rem, 6vw, 3rem);
221            color: #beaf87;
222            position: absolute;
223            font-family: Georgia, serif;
224            line-height: 1;
225        }
226        
227        /* Tablet and up */
228        @media (min-width: 48rem) {
229            .rs-quote-text::before,
230            .rs-quote-text::after {
231                font-size: clamp(3rem, 5vw, 4rem);
232            }
233        }
234        
235        .rs-quote-text::before {
236            left: clamp(0.25rem, 2vw, 0.5rem);
237            top: clamp(-0.5rem, -2vw, -1rem);
238        }
239        
240        .rs-quote-text::after {
241            right: clamp(0.25rem, 2vw, 0.5rem);
242            bottom: clamp(-1.5rem, -4vw, -2rem);
243        }
244        
245        .rs-read-more-btn {
246            background: none;
247            border: none;
248            color: #beaf87;
249            font-size: clamp(0.875rem, 2vw, 1rem);
250            font-weight: 600;
251            cursor: pointer;
252            text-decoration: underline;
253            padding: 0.5rem;
254            transition: color 0.3s ease;
255            /* Modern CSS: Touch-friendly sizing */
256            min-height: 44px;
257            min-width: 44px;
258            border-radius: 0.25rem;
259        }
260        
261        .rs-read-more-btn:hover,
262        .rs-read-more-btn:focus-visible {
263            color: #252526;
264            outline: 2px solid #beaf87;
265            outline-offset: 2px;
266        }
267        
268        .rs-client-info {
269            display: flex;
270            flex-direction: column;
271            gap: clamp(0.25rem, 1vh, 0.5rem);
272            align-items: center;
273        }
274        
275        .rs-client-name {
276            font-size: clamp(1.1rem, 3vw, 1.7rem);
277            font-weight: 600;
278            color: #252526;
279            text-wrap: balance;
280        }
281        
282        /* Tablet and up */
283        @media (min-width: 48rem) {
284            .rs-client-name {
285                font-size: clamp(1.25rem, 2.5vw, 1.6rem);
286            }
287        }
288        
289        .rs-client-location {
290            color: #666;
291            font-size: clamp(0.875rem, 2vw, 1.4rem);
292            text-wrap: balance;
293        }
294        
295        .rs-client-type {
296            background: #beaf87;
297            color: #252526;
298            padding: clamp(0.25rem, 1vw, 0.375rem) clamp(0.75rem, 2vw, 1rem);
299            border-radius: 1rem;
300            font-size: clamp(0.9rem, 2vw, 1.2rem);
301            font-weight: 500;
302            display: inline-block;
303        }
304        
305        /* Tablet and up */
306        @media (min-width: 48rem) {
307            .rs-client-type {
308                font-size: clamp(1rem, 2vw, 1.1rem);
309            }
310        }
311        
312        .rs-testimonial-date {
313            color: #888;
314            font-size: clamp(0.8rem, 1.8vw, 1rem);
315        }
316        
317        .rs-slider-controls {
318            display: flex;
319            justify-content: center;
320            align-items: center;
321            gap: clamp(1rem, 4vw, 1.5rem);
322            flex-wrap: wrap;
323        }
324        
325        .rs-nav-button {
326            background: #beaf87;
327            color: white;
328            border: none;
329            width: clamp(40px, 8vw, 50px);
330            height: clamp(40px, 8vw, 50px);
331            border-radius: 50%;
332            cursor: pointer;
333            font-size: clamp(1.25rem, 3vw, 2rem);
334            display: flex;
335            align-items: center;
336            justify-content: center;
337            transition: all 0.3s ease;
338            line-height: 0.2;
339            outline-offset: 2px;
340            min-width: 44px;
341            min-height: 44px;
342        }
343        
344        .rs-nav-button:hover,
345        .rs-nav-button:focus-visible {
346            background: #252526;
347            color: #ffffff;
348            transform: scale(1.05);
349            outline: 0px solid #beaf87;
350        }
351        
352        .rs-nav-button:disabled {
353            opacity: 0.3;
354            cursor: not-allowed;
355            transform: none;
356        }
357        
358        .rs-dots-container {
359            display: flex;
360            gap: clamp(0.5rem, 1.5vw, 0.75rem);
361            align-items: center;
362            max-width: min(200px, 40vw);
363            overflow: hidden;
364            position: relative;
365        }
366        
367        .rs-dots-wrapper {
368            display: flex;
369            gap: clamp(0.5rem, 1.5vw, 0.75rem);
370            transition: transform 0.3s ease;
371        }
372        
373        .rs-dot {
374            width: clamp(8px, 2vw, 10px);
375            height: clamp(8px, 2vw, 10px);
376            border-radius: 50%;
377            background: #ddd;
378            cursor: pointer;
379            transition: all 0.3s ease;
380            flex-shrink: 0;
381            min-width: 8px;
382            min-height: 8px;
383            position: relative;
384        }
385        
386        .rs-dot::after {
387            content: '';
388            position: absolute;
389            top: 50%;
390            left: 50%;
391            transform: translate(-50%, -50%);
392            width: clamp(8px, 2vw, 10px);
393            height: clamp(8px, 2vw, 10px);
394            border-radius: 50%;
395            background: inherit;
396        }
397        
398        .rs-dot.active::after {
399            background: #beaf87;
400            transform: translate(-50%, -50%) scale(1.3);
401        }
402        
403        .rs-dot:hover::after,
404        .rs-dot:focus-visible::after {
405            transform: translate(-50%, -50%) scale(1.2);
406        }
407        
408        .rs-dots-indicator {
409            color: #666;
410            font-size: clamp(0.80rem, 2vw, 1rem);
411            margin-inline: 0.5rem;
412            white-space: nowrap;
413        }
414        
415        .rs-branding {
416            text-align: center;
417            padding: clamp(1rem, 3vw, 1.5rem);
418            background: #f8f9fa;
419            color: #666;
420            font-size: clamp(0.75rem, 2vw, 0.9rem);
421        }
422        
423        .rs-branding a {
424            color: #beaf87;
425            text-decoration: none;
426            font-weight: 500;
427            transition: color 0.3s ease;
428            outline-offset: 2px;
429            border-radius: 0.25rem;
430            padding: 0.125rem 0.25rem;
431        }
432        
433        .rs-branding a:hover,
434        .rs-branding a:focus-visible {
435            color: #252526;
436            text-decoration: underline;
437            outline: 2px solid #beaf87;
438        }
439        
440        .rs-branding p {
441            margin: 0 !important;
442            font-size: 1rem;
443        }
444        
445        .rs-loading,
446        .rs-error {
447            text-align: center;
448            padding: clamp(2rem, 8vh, 4rem) clamp(1rem, 4vw, 2rem);
449            font-size: clamp(0.9rem, 2.5vw, 1.1rem);
450            display: flex;
451            align-items: center;
452            justify-content: center;
453            min-height: 200px;
454        }
455        
456        .rs-loading {
457            color: #666;
458        }
459        
460        .rs-error {
461            color: #d32f2f;
462        }
463        
464        /* Modern CSS: Container queries for fine-tuned responsive design */
465        @container (min-width: 400px) {
466            .rs-testimonial-slide {
467                padding-inline: 1rem;
468            }
469            
470            .rs-quote-text {
471                padding-inline: 2.5rem;
472            }
473        }
474        
475        @container (min-width: 600px) {
476            .rs-rating-stats {
477                grid-template-columns: repeat(3, 1fr);
478            }
479        }
480        
481        /* Modern CSS: Preference-based media queries */
482        @media (prefers-reduced-motion: reduce) {
483            .rs-testimonial-track,
484            .rs-nav-button,
485            .rs-dot,
486            .rs-dots-wrapper,
487            .rs-read-more-btn {
488                transition: none;
489            }
490        }
491        
492        @media (prefers-color-scheme: dark) {
493            .rs-testimonial-container {
494                background: #1a1a1a;
495                color: #e0e0e0;
496            }
497            
498            .rs-quote-text {
499                color: #e0e0e0;
500            }
501            
502            .rs-client-name {
503                color: #f0f0f0;
504            }
505            
506            .rs-client-location {
507                color: #b0b0b0;
508            }
509            
510            .rs-testimonial-date {
511                color: #999;
512            }
513            
514            .rs-branding {
515                background: #2a2a2a;
516                color: #b0b0b0;
517            }
518        }
519        
520        /* Modern CSS: High contrast mode support */
521        @media (prefers-contrast: high) {
522            .rs-testimonial-container {
523                border: 2px solid;
524            }
525            
526            .rs-nav-button,
527            .rs-client-type {
528                border: 1px solid;
529            }
530        }
531    `;
532    
533    function addStyles() {
534        if (!document.getElementById('rs-testimonial-styles')) {
535            const styleSheet = document.createElement('style');
536            styleSheet.id = 'rs-testimonial-styles';
537            styleSheet.textContent = styles;
538            document.head.appendChild(styleSheet);
539        }
540    }
541
542    function parseRSSFeed(xmlText) {
543        const parser = new DOMParser();
544        const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
545        
546        const parserError = xmlDoc.querySelector('parsererror');
547        if (parserError) {
548            throw new Error('Failed to parse RSS feed');
549        }
550        
551        const items = xmlDoc.querySelectorAll('item');
552        const testimonials = [];
553        
554        items.forEach(item => {
555            const title = item.querySelector('title')?.textContent || '';
556            const description = item.querySelector('description')?.textContent || '';
557            const pubDate = item.querySelector('pubDate')?.textContent || '';
558            
559            // Parse name and location from title
560            // Expected format: "Name, City, State" or "Name - City, State" or just "Name"
561            let name = 'Anonymous';
562            let location = '';
563            
564            if (title.trim()) {
565                // First try comma separation (most common format)
566                if (title.includes(',')) {
567                    const parts = title.split(',').map(part => part.trim());
568                    name = parts[0] || 'Anonymous';
569                    
570                    // If we have 2 parts: "Name, Location"
571                    if (parts.length === 2) {
572                        location = parts[1];
573                    }
574                    // If we have 3 parts: "Name, City, State"
575                    else if (parts.length >= 3) {
576                        location = parts.slice(1).join(', '); // Join city and state
577                    }
578                }
579                // Fallback to dash separation
580                else if (title.includes(' - ')) {
581                    const dashParts = title.split(' - ');
582                    name = dashParts[0]?.trim() || 'Anonymous';
583                    location = dashParts[1]?.trim() || '';
584                }
585                // If no separators, assume it's just the name
586                else {
587                    name = title.trim();
588                }
589            }
590
591            let formattedDate = '';
592            if (pubDate) {
593                try {
594                    const date = new Date(pubDate);
595                    formattedDate = date.toLocaleDateString('en-US', {
596                        year: 'numeric',
597                        month: 'long',
598                        day: 'numeric'
599                    });
600                } catch (e) {
601                    formattedDate = pubDate;
602                }
603            }
604
605            // Determine client type based on description content
606            let type = 'Buyer'; 
607            const descLower = description.toLowerCase();
608            if (descLower.includes('sell') || descLower.includes('sold') || descLower.includes('listing') || descLower.includes('sale')) {
609                type = 'Seller';
610            }
611            
612            testimonials.push({
613                name,
614                location,
615                type,
616                date: formattedDate,
617                text: description
618            });
619        });
620        
621        return testimonials;
622    }
623
624    function parseAgentInfo(xmlText) {
625        const parser = new DOMParser();
626        const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
627        
628        const channel = xmlDoc.querySelector('channel');
629        const title = channel?.querySelector('title')?.textContent || '';
630        const description = channel?.querySelector('description')?.textContent || '';
631
632        let agentName = agentSlug.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
633        if (title.includes(' - ')) {
634            agentName = title.split(' - ')[0];
635        }
636        
637        return {
638            name: agentName,
639            company: description || 'Real Estate Professional'
640        };
641    }
642
643    class TestimonialSlider {
644        constructor(container, testimonials, agentInfo) {
645            this.container = container;
646            this.testimonials = testimonials;
647            this.agentInfo = agentInfo;
648            this.currentSlide = 0;
649            this.autoSlideInterval = null;
650            this.expandedStates = {}; 
651            
652            this.render();
653            this.bindEvents();
654            this.startAutoSlide();
655        }
656        
657        needsTruncation(text) {
658            const wordsLimit = 120;
659            return text.split(' ').length > wordsLimit;
660        }
661  
662        getTruncatedText(text) {
663            const wordsLimit = 120;
664            const words = text.split(' ');
665            if (words.length <= wordsLimit) return text;
666            return words.slice(0, wordsLimit).join(' ') + '...';
667        }
668        
669        render() {
670            this.container.innerHTML = `
671                <div class="rs-testimonial-container">
672                    <div class="rs-testimonial-header">
673                        <div class="rs-agent-info">
674                            <h1>${this.agentInfo.name}</h1>
675                            <p>${this.agentInfo.company}</p>
676                            <div class="rs-rating-stats">
677                                <div class="rs-stat">
678                                    <span class="rs-stat-number">94%</span>
679                                    <span class="rs-stat-label">Satisfaction</span>
680                                </div>
681                                <div class="rs-stat">
682                                    <span class="rs-stat-number">97%</span>
683                                    <span class="rs-stat-label">Recommendation</span>
684                                </div>
685                                <div class="rs-stat">
686                                    <span class="rs-stat-number">99%</span>
687                                    <span class="rs-stat-label">Performance</span>
688                                </div>
689                            </div>
690                        </div>
691                    </div>
692                    <div class="rs-slider-wrapper">
693                        <div class="rs-testimonial-slider">
694                            <div class="rs-testimonial-track" id="rs-testimonial-track">
695                                ${this.createTestimonialSlides()}
696                            </div>
697                        </div>
698                        <div class="rs-slider-controls">
699                            <button class="rs-nav-button" id="rs-prevBtn" aria-label="Previous testimonial">‹</button>
700                            <div class="rs-dots-container" id="rs-dots-container">
701                                ${this.createDots()}
702                            </div>
703                            <button class="rs-nav-button" id="rs-nextBtn" aria-label="Next testimonial">›</button>
704                        </div>
705                    </div>
706                    <div class="rs-branding">
707                        <p>Testimonials powered by <a href="${REALSATISFIED_BASE_URL}${agentSlug}" target="_blank" rel="noopener">RealSatisfied</a></p>
708                    </div>
709                </div>
710            `;
711            
712            this.updateSlider();
713        }
714        
715        createTestimonialSlides() {
716            return this.testimonials.map((testimonial, index) => {
717                const needsReadMore = this.needsTruncation(testimonial.text);
718                const isExpanded = this.expandedStates[index] || false;
719                const displayText = needsReadMore && !isExpanded ? 
720                    this.getTruncatedText(testimonial.text) : 
721                    testimonial.text;
722                
723                return `
724                    <div class="rs-testimonial-slide">
725                        <div class="rs-testimonial-content">
726                            <div class="rs-quote-text ${needsReadMore && !isExpanded ? 'truncated' : 'expanded'}" data-index="${index}">
727                                ${displayText}
728                            </div>
729                            ${needsReadMore ? `
730                                <button class="rs-read-more-btn" data-index="${index}" aria-expanded="${isExpanded}">
731                                    ${isExpanded ? 'Read Less' : 'Read More'}
732                                </button>
733                            ` : ''}
734                            <div class="rs-client-info">
735                                <div class="rs-client-name">${testimonial.name}</div>
736                                <div class="rs-client-location">${testimonial.location}</div>
737                                <div class="rs-client-type">${testimonial.type}</div>
738                                ${testimonial.date ? `<div class="rs-testimonial-date">${testimonial.date}</div>` : ''}
739                            </div>
740                        </div>
741                    </div>
742                `;
743            }).join('');
744        }
745        
746        createDots() {
747            const totalDots = this.testimonials.length;
748            
749            if (totalDots <= MAX_VISIBLE_DOTS) {
750                // Show all dots normally
751                return `<div class="rs-dots-wrapper" role="tablist" aria-label="Testimonial navigation">` +
752                    this.testimonials.map((_, index) => 
753                        `<div class="rs-dot ${index === 0 ? 'active' : ''}
753" data-slide="${index}" role="tab" tabindex="${index === 0 ? '0' : '-1'}" aria-label="Go to testimonial ${index + 1}"></div>`
754                    ).join('') +
755                    `</div>`;
756            } else {
757                // Show dynamic dots with indicator
758                const dots = Array.from({ length: MAX_VISIBLE_DOTS }, (_, index) => 
759                    `<div class="rs-dot ${index === 0 ? 'active' : ''}" data-slide="${index}" role="tab" tabindex="${index === 0 ? '0' : '-1'}" aria-label="Go to testimonial ${index + 1}"></div>`
760                ).join('');
761                
762                return `
763                    <div class="rs-dots-wrapper" id="rs-dots-wrapper" role="tablist" aria-label="Testimonial navigation">
764                        ${dots}
765                    </div>
766                    <div class="rs-dots-indicator" id="rs-dots-indicator" aria-live="polite">1 / ${totalDots}</div>
767                `;
768            }
769        }
770        
771        toggleReadMore(index) {
772            this.expandedStates[index] = !this.expandedStates[index];
773            const testimonial = this.testimonials[index];
774            const isExpanded = this.expandedStates[index];
775            
776            // Find the current slide
777            const slide = this.container.querySelector(`[data-index="${index}"]`).closest('.rs-testimonial-slide');
778            const quoteText = slide.querySelector('.rs-quote-text');
779            const readMoreBtn = slide.querySelector('.rs-read-more-btn');
780            
781            // Update text content
782            if (isExpanded) {
783                quoteText.textContent = testimonial.text;
784                quoteText.className = 'rs-quote-text expanded';
785                readMoreBtn.textContent = 'Read Less';
786                readMoreBtn.setAttribute('aria-expanded', 'true');
787            } else {
788                quoteText.textContent = this.getTruncatedText(testimonial.text);
789                quoteText.className = 'rs-quote-text truncated';
790                readMoreBtn.textContent = 'Read More';
791                readMoreBtn.setAttribute('aria-expanded', 'false');
792            }
793            
794            // Pause auto-slide when expanding
795            if (isExpanded) {
796                this.stopAutoSlide();
797            } else {
798                this.startAutoSlide();
799            }
800        }
801        
802        bindEvents() {
803            // Navigation buttons
804            this.container.querySelector('#rs-prevBtn').addEventListener('click', () => this.prevSlide());
805            this.container.querySelector('#rs-nextBtn').addEventListener('click', () => this.nextSlide());
806            
807            // Keyboard navigation for nav buttons
808            [this.container.querySelector('#rs-prevBtn'), this.container.querySelector('#rs-nextBtn')].forEach(btn => {
809                btn.addEventListener('keydown', (e) => {
810                    if (e.key === 'Enter' || e.key === ' ') {
811                        e.preventDefault();
812                        btn.click();
813                    }
814                });
815            });
816            
817            // Dots
818            this.container.querySelectorAll('.rs-dot').forEach((dot, index) => {
819                dot.addEventListener('click', () => this.goToSlide(index));
820                dot.addEventListener('keydown', (e) => {
821                    if (e.key === 'Enter' || e.key === ' ') {
822                        e.preventDefault();
823                        this.goToSlide(index);
824                    }
825                    // Arrow key navigation
826                    else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
827                        e.preventDefault();
828                        const nextDot = this.container.querySelector(`.rs-dot[data-slide="${index + 1}"]`);
829                        if (nextDot) {
830                            nextDot.focus();
831                            this.goToSlide(index + 1);
832                        }
833                    }
834                    else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
835                        e.preventDefault();
836                        const prevDot = this.container.querySelector(`.rs-dot[data-sli
836de="${index - 1}"]`);
837                        if (prevDot) {
838                            prevDot.focus();
839                            this.goToSlide(index - 1);
840                        }
841                    }
842                });
843            });
844            
845            // Read more buttons
846            this.container.addEventListener('click', (e) => {
847                if (e.target.classList.contains('rs-read-more-btn')) {
848                    e.stopPropagation();
849                    const index = parseInt(e.target.dataset.index);
850                    this.toggleReadMore(index);
851                }
852            });
853            
854            // Touch/swipe support with improved touch handling
855            let startX = 0;
856            let endX = 0;
857            let startY = 0;
858            let endY = 0;
859            let isDragging = false;
860            const slider = this.container.querySelector('.rs-testimonial-slider');
861            
862            slider.addEventListener('touchstart', (e) => {
863                startX = e.touches[0].clientX;
864                startY = e.touches[0].clientY;
865                isDragging = true;
866            }, { passive: true });
867            
868            slider.addEventListener('touchmove', (e) => {
869                if (!isDragging) return;
870                
871                const currentX = e.touches[0].clientX;
872                const currentY = e.touches[0].clientY;
873                const diffX = Math.abs(currentX - startX);
874                const diffY = Math.abs(currentY - startY);
875                
876                // If horizontal movement is greater than vertical, prevent scroll
877                if (diffX > diffY && diffX > 10) {
878                    e.preventDefault();
879                }
880            });
881            
882            slider.addEventListener('touchend', (e) => {
883                if (!isDragging) return;
884                
885                endX = e.changedTouches[0].clientX;
886                endY = e.changedTouches[0].clientY;
887                isDragging = false;
888                
889                this.handleSwipe(startX, endX, startY, endY);
890            }, { passive: true });
891            
892            // Mouse drag support for desktop
893            let isMouseDown = false;
894            let mouseStartX = 0;
895            
896            slider.addEventListener('mousedown', (e) => {
897                isMouseDown = true;
898                mouseStartX = e.clientX;
899                slider.style.cursor = 'grabbing';
900                e.preventDefault();
901            });
902            
903            slider.addEventListener('mousemove', (e) => {
904                if (!isMouseDown) return;
905                e.preventDefault();
906            });
907            
908            slider.addEventListener('mouseup', (e) => {
909                if (!isMouseDown) return;
910                isMouseDown = false;
911                slider.style.cursor = '';
912                
913                const mouseEndX = e.clientX;
914                this.handleSwipe(mouseStartX, mouseEndX, 0, 0);
915            });
916            
917            slider.addEventListener('mouseleave', () => {
918                if (isMouseDown) {
919                    isMouseDown = false;
920                    slider.style.cursor = '';
921                }
922            });
923            
924            // Pause auto-slide on hover/focus
925            this.container.addEventListener('mouseenter', () => this.stopAutoSlide());
926            this.container.addEventListener('mouseleave', () => {
927                // Only restart if no testimonial is currently expanded
928                const hasExpanded = Object.values(this.expandedStates).some(state => state);
929                if (!hasExpanded) {
930                    this.startAutoSlide();
931                }
932            });
933            
934            // Pause on focus
935            this.container.addEventListener('focusin', () => this.stopAutoSlide());
936            this.container.addEventListener('focusout', (e) => {
937                // Check if focus is moving outside the container
938                if (!this.container.contains(e.relatedTarget)) {
939                    const hasExpanded = Object.values(this.expandedStates).some(state => state);
940                    if (!hasExpanded) {
941                        this.startAutoSlide();
942                    }
943                }
944            });
945            
946            // Keyboard navigation for the entire slider
947            this.container.addEventListener('keydown', (e) => {
948                if (e.target.closest('.rs-dot') || e.target.closest('.rs-nav-button') || e.target.closest('.rs-read-more-btn')) {
949                    return; // Let specific elements handle their own keyboard events
950                }
951                
952                switch (e.key) {
953                    case 'ArrowLeft':
954                    case 'ArrowUp':
955                        e.preventDefault();
956                        this.prevSlide();
957                        break;
958                    case 'ArrowRight':
959                    case 'ArrowDown':
960                        e.preventDefault();
961                        this.nextSlide();
962                        break;
963                    case 'Home':
964                        e.preventDefault();
965                        this.goToSlide(0);
966                        break;
967                    case 'End':
968                        e.preventDefault();
969                        this.goToSlide(this.testimonials.length - 1);
970                        break;
971                }
972            });
973            
974            // Handle visibility changes to pause/resume auto-slide
975            document.addEventListener('visibilitychange', () => {
976                if (document.hidden) {
977                    this.stopAutoSlide();
978                } else {
979                    const hasExpanded = Object.values(this.expandedStates).some(state => state);
980                    if (!hasExpanded) {
981                        this.startAutoSlide();
982                    }
983                }
984            });
985            
986            // Handle resize events for responsive adjustments
987            let resizeTimeout;
988            window.addEventListener('resize', () => {
989                clearTimeout(resizeTimeout);
990                resizeTimeout = setTimeout(() => {
991                    this.updateSlider();
992                }, 150);
993            });
994        }
995        
996        updateSlider() {
997            const track = this.container.querySelector('#rs-testimonial-track');
998            const dots = this.container.querySelectorAll('.rs-dot');
999            const prevBtn = this.container.querySelector('#rs-prevBtn');
1000            const nextBtn = this.container.querySelector('#rs-nextBtn');
1001            const dotsIndicator = this.container.querySelector('#rs-dots-indicator');
1002            
1003            // Move slider
1004            track.style.transform = `translateX(-${this.currentSlide * 100}%)`;
1005            
1006            // Update ARIA attributes
1007            track.setAttribute('aria-live', 'polite');
1008            
1009            // Update dots based on whether we have dynamic dots or not
1010            if (this.testimonials.length <= MAX_VISIBLE_DOTS) {
1011                // Normal dots behavior
1012                dots.forEach((dot, index) => {
1013                    const isActive = index === this.currentSlide;
1014                    dot.classList.toggle('active', isActive);
1015                    dot.setAttribute('tabindex', isActive ? '0' : '-1');
1016                    dot.setAttribute('aria-selected', isActive.toString());
1017                });
1018            } else {
1019                // Dynamic dots behavior
1020                this.updateDynamicDots();
1021                
1022                // Update indicator
1023                if (dotsIndicator) {
1024                    dotsIndicator.textContent = `${this.currentSlide + 1} / ${this.testimonials.length}`;
1025                }
1026            }
1027            
1028            // Update navigation buttons
1029            prevBtn.disabled = this.currentSlide === 0;
1030            nextBtn.disabled = this.currentSlide === this.testimonials.length - 1;
1031            
1032            // Update ARIA labels
1033            prevBtn.setAttribute('aria-label', `Previous testimonial (${this.currentSlide} of ${this.testimonials.length})`);
1034            nextBtn.setAttribute('aria-label', `Next testimonial (${this.currentSlide + 2} of ${this.testimonials.length})`);
1035            
1036            // Announce slide change to screen readers
1037            const currentTestimonial = this.testimonials[this.currentSlide];
1038            if (currentTestimonial) {
1039                const announcement = `Testimonial ${this.currentSlide + 1} of ${this.testimonials.length}: ${currentTestimonial.name}`;
1040                this.announceToScreenReader(announcement);
1041            }
1042        }
1043        
1044        updateDynamicDots() {
1045            const dots = this.container.querySelectorAll('.rs-dot');
1046            const totalSlides = this.testimonials.length;
1047            const maxVisible = MAX_VISIBLE_DOTS;
1048            
1049            // Calculate which slides the visible dots should represent
1050            let startSlide, endSlide;
1051            
1052            if (this.currentSlide < Math.floor(maxVisible / 2)) {
1053                // Near the beginning
1054                startSlide = 0;
1055                endSlide = maxVisible - 1;
1056            } else if (this.currentSlide >= totalSlides - Math.ceil(maxVisible / 2)) {
1057                // Near the end
1058                startSlide = totalSlides - maxVisible;
1059                endSlide = totalSlides - 1;
1060            } else {
1061                // In the middle
1062                const halfVisible = Math.floor(maxVisible / 2);
1063                startSlide = this.currentSlide - halfVisible;
1064                endSlide = this.currentSlide + halfVisible;
1065            }
1066            
1067            // Update each dot
1068            dots.forEach((dot, index) => {
1069                const slideIndex = startSlide + index;
1070                const isActive = slideIndex === this.currentSlide;
1071                
1072                dot.dataset.slide = slideIndex;
1073                dot.classList.toggle('active', isActive);
1074                dot.setAttribute('tabindex', isActive ? '0' : '-1');
1075                dot.setAttribute('aria-selected', isActive.toString());
1076                dot.setAttribute('aria-label', `Go to testimonial ${slideIndex + 1}`);
1077                
1078                // Update click handler
1079                dot.onclick = () => this.goToSlide(slideIndex);
1080            });
1081        }
1082        
1083        goToSlide(index) {
1084            if (index < 0 || index >= this.testimonials.length) return;
1085            
1086            this.currentSlide = index;
1087            this.updateSlider();
1088            
1089            // Focus management for accessibility
1090            const activeDot = this.container.querySelector('.rs-dot.active');
1091            if (activeDot && document.activeElement === activeDot) {
1092                activeDot.focus();
1093            }
1094        }
1095        
1096        nextSlide() {
1097            if (this.currentSlide < this.testimonials.length - 1) {
1098                this.currentSlide++;
1099                this.updateSlider();
1100            }
1101        }
1102        
1103        prevSlide() {
1104            if (this.currentSlide > 0) {
1105                this.currentSlide--;
1106                this.updateSlider();
1107            }
1108        }
1109        
1110        startAutoSlide() {
1111            this.stopAutoSlide();
1112            this.autoSlideInterval = setInterval(() => {
1113                if (this.currentSlide === this.testimonials.length - 1) {
1114                    this.currentSlide = 0;
1115                } else {
1116                    this.currentSlide++;
1117                }
1118                this.updateSlider();
1119            }, 8000);
1120        }
1121        
1122        stopAutoSlide() {
1123            if (this.autoSlideInterval) {
1124                clearInterval(this.autoSlideInterval);
1125                this.autoSlideInterval = null;
1126            }
1127        }
1128        
1129        handleSwipe(startX, endX, startY = 0, endY = 0) {
1130            const swipeThreshold = 50;
1131            const diffX = startX - endX;
1132            const diffY = Math.abs(startY - endY);
1133            
1134            // Only process horizontal swipes
1135            if (Math.abs(diffX) > swipeThreshold && Math.abs(diffX) > diffY) {
1136                if (diffX > 0) {
1137                    this.nextSlide();
1138                } else {
1139                    this.prevSlide();
1140                }
1141            }
1142        }
1143        
1144        // Accessibility helper for screen reader announcements
1145        announceToScreenReader(message) {
1146            const announcement = document.createElement('div');
1147            announcement.setAttribute('aria-live', 'polite');
1148            announcement.setAttribute('aria-atomic', 'true');
1149            announcement.className = 'sr-only';
1150            announcement.style.cssText = 'position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden;';
1151            announcement.textContent = message;
1152            
1153            document.body.appendChild(announcement);
1154            
1155            setTimeout(() => {
1156                document.body.removeChild(announcement);
1157            }, 1000);
1158        }
1159    }
1160    
1161    // Fetch and initialize testimonial slider
1162    async function initializeTestimonialSlider() {
1163        const containers = document.querySelectorAll('#testimonial-slider');
1164        
1165        if (containers.length === 0) {
1166            console.warn('No testimonial slider containers found');
1167            return;
1168        }
1169        
1170        // Add styles
1171        addStyles();
1172        
1173        try {
1174            // Show loading state
1175            containers.forEach(container => {
1176                container.innerHTML = '<div class="rs-loading" role="status" aria-live="polite">Loading testimonials...</div>';
1177            });
1178            
1179            // Fetch RSS feed
1180            const rssUrl = `${RSS_BASE_URL}${agentSlug}`;
1181            const response = await fetch(`https://api.allorigins.win/raw?url=${encodeURIComponent(rssUrl)}`);
1182            
1183            if (!response.ok) {
1184                throw new Error(`HTTP error! status: ${response.status}`);
1185            }
1186            
1187            const xmlText = await response.text();
1188            const testimonials = parseRSSFeed(xmlText);
1189            const agentInfo = parseAgentInfo(xmlText);
1190            
1191            if (testimonials.length === 0) {
1192                throw new Error('No testimonials found');
1193            }
1194            
1195            // Initialize slider for each container
1196            containers.forEach(container => {
1197                new TestimonialSlider(container, testimonials, agentInfo);
1198            });
1199            
1200        } catch (error) {
1201            console.error('Error loading testimonials:', error);
1202            containers.forEach(container => {
1203                container.innerHTML = '<div class="rs-error" role="alert">Unable to load testimonials. Please try again later.</div>';
1204            });
1205        }
1206    }
1207    
1208    // Initialize when DOM is ready
1209    if (document.readyState === 'loading') {
1210        document.addEventListener('DOMContentLoaded', initializeTestimonialSlider);
1211    } else {
1212        initializeTestimonialSlider();
1213    }
1214    
1215})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.