PageSourceSearch

https://trangotech.com/wp-content/themes/trangoMain/assets/js/contact-us-new.js?ver=1787212852

js trangotech.com collected 2026-10-02 14:42:13 UTC 17,316 bytes, 343 lines download raw bytes

1/* Scroll reveal (single elements + stagger grids) */
2const io = new IntersectionObserver(entries => {
3  entries.forEach(e => {
4    if (e.isIntersecting) { e.target.classList.add('vis'); io.unobserve(e.target); }
5  });
6}, {threshold: .12, rootMargin: '0px 0px -40px 0px'});
7document.querySelectorAll('[data-r], [data-r-stagger]').forEach(el => io.observe(el));
8
9/* Hero H1 underline + badge entrance trigger */
10requestAnimationFrame(() => requestAnimationFrame(() => {
11  const h1 = document.querySelector('.hero-h1');
12  if (h1) h1.classList.add('shown');
13}));
14
15/* Stat counter */
16const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
17function animateCounter(el) {
18  if (reducedMotion) return;
19  const target = el.getAttribute('data-count');
20  if (!target) return;
21  const suffix = el.getAttribute('data-suffix') || '';
22  const isFloat = target.includes('.');
23  const end = parseFloat(target);
24  const duration = 1400;
25  const startTime = performance.now();
26  function tick(now) {
27    const p = Math.min((now - startTime) / duration, 1);
28    const eased = 1 - Math.pow(1 - p, 3);
29    const val = end * eased;
30    el.textContent = (isFloat ? val.toFixed(1) : Math.round(val)) + suffix;
31    if (p < 1) requestAnimationFrame(tick);
32    else el.textContent = (isFloat ? end.toFixed(1) : end) + suffix;
33  }
34  requestAnimationFrame(tick);
35}
36const statIO = new IntersectionObserver(entries => {
37  entries.forEach(e => {
38    if (e.isIntersecting) { animateCounter(e.target); statIO.unobserve(e.target); }
39  });
40}, {threshold: .5});
41document.querySelectorAll('.stat-n[data-count]').forEach(el => statIO.observe(el));
42
43/* Calculator single-select options */
44document.querySelectorAll('.calc-opt').forEach(btn => {
45  btn.addEventListener('click', () => {
46    const group = btn.dataset.group;
47    document.querySelectorAll('.calc-opt[data-group="'+group+'"]').forEach(b => b.classList.remove('active'));
48    btn.classList.add('active');
49  });
50});
51/* Calculator multi-select checks */
52document.querySelectorAll('.calc-check-item').forEach(btn => {
53  btn.addEventListener('click', () => btn.classList.toggle('active'));
54});
55
56/* Tech stack: inject brand-colored logos for each tag */
57const techLogos = {
58  /* Languages */
59  'TypeScript':{bg:'#3178C6',letter:'TS'},
60  'JavaScript':{bg:'#F7DF1E',letter:'JS'},
61  'Swift':{bg:'#F05138',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M19 6c-1 5-5 11-12 13 2-3 1-4-2-5l1-1c-2-1-3-3-4-5h2c2 3 5 5 9 5-2-3-5-5-7-8 2 2 6 4 9 4-1-2-3-5-6-7 3 1 7 4 10 4z"/></svg>'},
62  'Kotlin':{bg:'#7F52FF',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M3 3h9l-9 9V3zm0 18V11l9 10H3zm9-18h9L12 12V3z"/></svg>'},
63  'Objective-C':{bg:'#A2B9CB',letter:'Obj'},
64  'C++ (JSI)':{bg:'#00599C',letter:'C++'},
65
66  /* RN Core & UI */
67  'React Native 0.76':{bg:'#61DAFB',svg:'<svg viewBox="0 0 24 24" fill="#fff"><circle cx="12" cy="12" r="2"/><ellipse cx="12" cy="12" rx="11" ry="4.2" fill="none" stroke="#fff" stroke-width="1.4"/><ellipse cx="12" cy="12" rx="11" ry="4.2" fill="none" stroke="#fff" stroke-width="1.4" transform="rotate(60 12 12)"/><ellipse cx="12" cy="12" rx="11" ry="4.2" fill="none" stroke="#fff" stroke-width="1.4" transform="rotate(120 12 12)"/></svg>'},
68  'Expo SDK 52':{bg:'#000020',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 3l9 15.5h-3.5L12 9 6.5 18.5H3z"/></svg>'},
69  'Expo Router':{bg:'#000020',letter:'ER'},
70  'Reanimated 3':{bg:'#F7672E',letter:'Re'},
71  'React Native Skia':{bg:'#FE3C45',letter:'Sk'},
72  'Gesture Handler':{bg:'#61DAFB',letter:'GH'},
73  'Lottie':{bg:'#00DDB3',svg:'<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2"><path d="M3 12c2-6 6-9 9-9s7 3 9 9-2 9-9 9-11-3-9-9z"/></svg>'},
74  'React Native Web':{bg:'#61DAFB',letter:'RNW'},
75  'React Navigation':{bg:'#6B52AE',letter:'RN'},
76
77  /* Architecture & State */
78  'Fabric':{bg:'#61DAFB',letter:'Fb'},
79  'JSI':{bg:'#0F4C81',letter:'JSI'},
80  'TurboModules':{bg:'#7F52FF',letter:'Tm'},
81  'Hermes':{bg:'#1C1E21',letter:'Hr'},
82  'React Query':{bg:'#FF4154',letter:'RQ'},
83  'Zustand':{bg:'#443E38',letter:'Zu'},
84  'Redux Toolkit':{bg:'#764ABC',letter:'RT'},
85  'MMKV':{bg:'#3DDC84',letter:'MM'},
86  'Axios':{bg:'#5A29E4',letter:'Ax'},
87
88  /* Backend */
89  'Node.js':{bg:'#339933',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2L3 7v10l9 5 9-5V7L12 2zm0 2.3l7 3.9v7.6l-7 3.9-7-3.9V8.2l7-3.9z"/></svg>'},
90  'NestJS':{bg:'#E0234E',letter:'N'},
91  'Python (Django, FastAPI)':{bg:'#3776AB',svg:'<svg viewBox="0 0 24 24"><path fill="#FFD43B" d="M12 2c-3 0-3 1.5-3 3v2h6v1H6c-2 0-3 1.5-3 4s1 4 3 4h2v-2c0-2 1.5-3 4-3h3c1.5 0 3-.5 3-2V5c0-1.5-1.5-3-6-3zm-2 2c.5 0 1 .5 1 1s-.5 1-1 1-1-.5-1-1 .5-1 1-1z"/><path fill="#fff" d="M12 22c3 0 3-1.5 3-3v-2H9v-1h9c2 0 3-1.5 3-4s-1-4-3-4h-2v2c0 2-1.5 3-4 3H9c-1.5 0-3 .5-3 2v3c0 1.5 1.5 4 6 4zm2-2c-.5 0-1-.5-1-1s.5-1 1-1 1 .5 1 1-.5 1-1 1z"/></svg>'},
92  'Ruby on Rails':{bg:'#CC0000',letter:'R'},
93  '.NET 8':{bg:'#512BD4',letter:'.N'},
94  'Go':{bg:'#00ADD8',letter:'Go'},
95  'GraphQL':{bg:'#E10098',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2l9 5.2v9.6L12 22l-9-5.2V7.2L12 2z" stroke="#fff" stroke-width="1.2" fill="none"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="3" r="1.5"/><circle cx="12" cy="21" r="1.5"/><circle cx="4" cy="7.5" r="1.5"/><circle cx="20" cy="7.5" r="1.5"/><circle cx="4" cy="16.5" r="1.5"/><circle cx="20" cy="16.5" r="1.5"/></svg>'},
96  'REST':{bg:'#0E2F44',letter:'API'},
97  'gRPC':{bg:'#247DAB',letter:'g'},
98  'WebSockets':{bg:'#010101',letter:'WS'},
99
100  /* Cloud & DevOps */
101  'AWS':{bg:'#FF9900',svg:'<svg viewBox="0 0 24 24" fill="#232F3E"><path d="M7 11c-.6 0-1 .4-1 1.5 0 .6.2 1 .5 1.5l-.5 1c-.7-.5-1-1.5-1-2.5C5 11 5.7 10 7 10s2 .8 2 2.2v2c0 .3.1.5.2.7l-1 .5c-.1-.1-.2-.3-.3-.5-.5.4-1.1.6-1.7.6-.7 0-1.2-.4-1.2-1 0-.8.7-1.3 2-1.3h.7v-.6c0-.5-.2-.6-.7-.6zm.7 1.7v-.5h-.5c-.5 0-.7.2-.7.5s.2.4.4.4c.3 0 .6-.2.8-.4zM3 17c4 2 9 2 14 0l-.5-1c-4 2-9 2-13 0z"/></svg>'},
102  'Google Cloud (GCP)':{bg:'#4285F4',svg:'<svg viewBox="0 0 24 24"><path fill="#EA4335" d="M14 7l1.5-1.5C14 4 12.5 3.5 11 3.5c-3 0-5.5 1.5-7 4l2 1.5c.8-1.5 2.5-2.5 5-2.5 1 0 2 .3 3 1z"/><path fill="#4285F4" d="M11.5 11v3h4.5c-.5 1.5-2 2.5-4.5 2.5-2 0-4-1.5-4.5-3l-2 1.5c1 2.5 3.5 4 6.5 4 4 0 7-3 7-7 0-.5 0-1-.2-1.5z"/><path fill="#FBBC04" d="M4 13c-.3-.7-.5-1.5-.5-2.5s.2-1.8.5-2.5l-2-1.5c-.7 1-1 2.5-1 4s.3 3 1 4z"/><path fill="#34A853" d="M11 7c1 0 1.8.3 2.5.8L15.5 6c-1-1-2.5-2-4.5-2-2.5 0-4.5 1-6 2.5L7 8.5c.8-1.2 2-1.5 4-1.5z"/></svg>'},
103  'Azure':{bg:'#0078D4',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M9 3L2 17l5 4h13L12 7zm1 5l6 11h-9z"/></svg>'},
104  'Firebase':{bg:'#FFCA28',svg:'<svg viewBox="0 0 24 24"><path fill="#FFA000" d="M4 18l4-15 4 7z"/><path fill="#F57C00" d="M4 18l11-6 5 6-9 4z"/><path fill="#FFCA28" d="M8 3l4 7-3 2z"/></svg>'},
105  'EAS Build':{bg:'#000020',letter:'EB'},
106  'EAS Update':{bg:'#000020',letter:'EU'},
107  'CodePush':{bg:'#0078D4',letter:'CP'},
108  'Crashlytics':{bg:'#FFA000',letter:'CR'},
109  'Fastlane':{bg:'#84BD00',letter:'F'},
110  'GitHub Actions':{bg:'#2088FF',svg:'<svg viewBox="0 0 24 24" fill="#fff"><circle cx="12" cy="12" r="10" fill-opacity=".15"/><path d="M16 12l-5-4v3H7v2h4v3z"/></svg>'},
111  'CircleCI':{bg:'#161616',letter:'CI'},
112  'Sentry':{bg:'#362D59',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2l10 18H14l-2-4-2 4H2L12 2zm0 5l5 10h-3l-2-4-2 4H9z" opacity=".95"/></svg>'},
113
114  /* Databases */
115  'WatermelonDB':{bg:'#FE3C45',letter:'Wm'},
116  'SQLite':{bg:'#003B57',letter:'SQ'},
117  'Firestore':{bg:'#FFCA28',letter:'FS'},
118  'Realm':{bg:'#39477F',letter:'R'},
119  'PostgreSQL':{bg:'#336791',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M16 4c-2 0-3 .5-4 1-1-.5-2-1-4-1-3 0-5 2-5 5 0 4 2 13 5 13 1 0 2-1 3-3 0-1 0-2 1-2s1 1 1 2c1 2 2 3 3 3 3 0 5-9 5-13 0-3-2-5-5-5zM7 7c1 0 1 1 1 1s-1 0-1-1zm9 0c0-1 1-1 1 0s-1 1-1 1z"/></svg>'},
120  'MongoDB':{bg:'#47A248',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2C8 8 8 14 12 22c4-8 4-14 0-20z"/></svg>'},
121  'Redis':{bg:'#DC382D',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M3 8l9 4 9-4-9-4zm0 4l9 4 9-4M3 16l9 4 9-4" stroke="#fff" stroke-width="1.5" fill="none"/></svg>'},
122  'DynamoDB':{bg:'#4053D6',letter:'Dy'},
123  'AsyncStorage':{bg:'#61DAFB',letter:'AS'},
124
125  /* AI / ML */
126  'OpenAI API':{bg:'#000',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M22 11c0-3-2-5-5-5-.5-2-2.5-3-5-3s-4.5 1.5-5 3.5C4.5 7 2.5 9 2.5 12c0 1.5 1 3 2 4-1 1.5-.5 4 1 5.5 1.5 1 4 1 5.5 0 .5 2 2.5 3.5 5 3.5s4.5-1.5 5-3.5c2.5-.5 4.5-2.5 4.5-5 0-1.5-1-3-2-4 1-1.5.5-1 .5-1.5z" opacity=".95"/></svg>'},
127  'Anthropic Claude':{bg:'#D97706',letter:'An'},
128  'Gemini':{bg:'#1E88E5',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2L8 8 2 12l6 4 4 6 4-6 6-4-6-4z"/></svg>'},
129  'ML Kit':{bg:'#4285F4',letter:'ML'},
130  'TensorFlow Lite':{bg:'#FF6F00',letter:'TF'},
131  'Vertex AI':{bg:'#4285F4',letter:'Vx'},
132  'AWS Bedrock':{bg:'#FF9900',letter:'Br'},
133  'ARKit / ARCore':{bg:'#4285F4',letter:'AR'},
134  'LangChain':{bg:'#1C3C3C',letter:'Lc'},
135
136  /* Testing & QA */
137  'Jest':{bg:'#C21325',letter:'J'},
138  'React Testing Library':{bg:'#E33332',letter:'RTL'},
139  'Detox':{bg:'#0066B8',letter:'Dx'},
140  'Maestro':{bg:'#9333EA',letter:'Ma'},
141  'Appium':{bg:'#662D91',letter:'Ap'},
142  'BrowserStack App Live':{bg:'#FF6B00',letter:'BS'},
143  'Firebase Test Lab':{bg:'#FFCA28',letter:'FTL'},
144  'MSW (Mock Service Worker)':{bg:'#FF6A33',letter:'MSW'},
145  'ESLint':{bg:'#4B32C3',letter:'ES'},
146  'Prettier':{bg:'#F7B93E',letter:'Pr'}
147};
148function buildTechLogo(name) {
149  const t = techLogos[name];
150  const fallbackBg = '#5a6068';
151  const fallbackLetter = name.replace(/[^A-Za-z0-9]/g,'').slice(0,2).toUpperCase() || '·';
152  if (!t) return '<span class="stack-chip-logo" style="background:'+fallbackBg+'" aria-hidden="true">'+fallbackLetter+'</span>';
153  const style = 'style="background:'+t.bg+'"';
154  if (t.svg) return '<span class="stack-chip-logo" '+style+' aria-hidden="true">'+t.svg+'</span>';
155  if (t.letter) return '<span class="stack-chip-logo" '+style+' aria-hidden="true">'+t.letter+'</span>';
156  return '<span class="stack-chip-logo" '+style+' aria-hidden="true">'+fallbackLetter+'</span>';
157}
158document.querySelectorAll('.stack-chip[data-tech]').forEach(chip => {
159  const name = chip.dataset.tech;
160  chip.insertAdjacentHTML('afterbegin', buildTechLogo(name));
161});
162
163/* Testimonials rotate */
164let ti = 0;
165const tCount = 5;
166function showTest(n) {
167  ti = n;
168  document.querySelectorAll('.test-slide').forEach((s, i) => s.classList.toggle('active', i === n));
169  document.querySelectorAll('.test-dot').forEach((d, i) => d.classList.toggle('active', i === n));
170}
171setInterval(() => showTest((ti + 1) % tCount), 5500);
172
173/* FAQ accordion */
174function toggleFAQ(btn) {
175  const item = btn.parentElement;
176  const isOpen = item.classList.contains('open');
177  item.classList.toggle('open');
178  btn.setAttribute('aria-expanded', !isOpen);
179}
180
181
182/* Main contact form */
183function onMainContactSubmit() {
184  const form = document.getElementById('contact-main-form');
185  alert('Thanks — a Senior Engineering Lead will reply within one business hour during US working hours (Mon–Fri, 9am–7pm ET). You\'ll get an auto-acknowledgement email with a reference number in the next few seconds.');
186  form.reset();
187}
188
189/* ═══════════ HERO 3D DEPTH (#v2) — entrance reveal, mouse parallax, app-screen carousel ═══════════ */
190(function(){
191  const v2 = document.getElementById('v2');
192  if(!v2) return;
193  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
194
195  /* entrance reveal via Web Animations API */
196  if(!reduce && !document.hidden){
197    v2.querySelectorAll('.reveal').forEach(it => {
198      const d = parseFloat(getComputedStyle(it).getPropertyValue('--d')) || 0;
199      it.animate(
200        [{opacity:0, transform:'translateY(18px)'},{opacity:1, transform:'translateY(0)'}],
201        {duration:750, delay:d*1000, easing:'cubic-bezier(.2,.75,.25,1)', fill:'backwards'}
202      );
203    });
204  }
205
206  /* mouse parallax */
207  const stage = document.getElementById('v2-stage');
208  const layers = document.querySelectorAll('#v2-scene [data-depth]');
209  if(stage && !reduce){
210    stage.addEventListener('mousemove', e => {
211      const r = stage.getBoundingClientRect();
212      const x = (e.clientX - r.left)/r.width - .5;
213      const y = (e.clientY - r.top)/r.height - .5;
214      layers.forEach(l => {
215        const d = parseFloat(l.dataset.depth);
216        l.style.transform = `translate3d(${-x*d}px, ${-y*d}px, 0)`;
217      });
218    });
219    stage.addEventListener('mouseleave', () => {
220      layers.forEach(l => l.style.transform = 'translate3d(0,0,0)');
221    });
222  }
223
224  /* Industry app-screen carousel — auto-rotates inside the phone */
225  (function buildCarousel(){
226    const car = document.getElementById('v2-car');
227    const template = document.getElementById('app-screens');
228    if(!car || !template) return;
229    const track = car.querySelector('.appcar-track');
230    track.appendChild(template.content.cloneNode(true));
231    const n = track.children.length;
232    let idx = 0, timer = null;
233    const go = i => { idx = i; track.style.transform = `translateX(-${i*100}%)`; };
234    const start = () => { if(reduce || timer) return; timer = setInterval(() => go((idx+1)%n), 3400); };
235    const stop = () => { clearInterval(timer); timer = null; };
236    car.addEventListener('mouseenter', stop);
237    car.addEventListener('mouseleave', start);
238    start();
239  })();
240})();
241
242/* ═══════════ UNIFIED CAROUSEL ═══════════ */
243(function(){
244  const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
245
246  function initCarousel(root){
247    const viewport = root.querySelector('.tt-car-viewport');
248    const track = root.querySelector('.tt-car-track');
249    if(!viewport || !track) return;
250    const slides = Array.from(track.children).filter(el => el.classList.contains('tt-car-slide'));
251    if(slides.length === 0) return;
252
253    const prevBtn = root.querySelector('.tt-car-prev');
254    const nextBtn = root.querySelector('.tt-car-next');
255    const dotsHost = root.querySelector('.tt-car-dots');
256    const counter = root.querySelector('.tt-car-counter');
257    const baseLabel = root.dataset.label || 'item';
258
259    function currentPerView(){
260      const base = parseInt(root.dataset.per || '3', 10);
261      if(matchMedia('(max-width: 640px)').matches) return 1;
262      if(matchMedia('(max-width: 980px)').matches) return Math.min(base, 2);
263      return base;
264    }
265
266    let perView = currentPerView();
267    let pages = Math.max(1, Math.ceil(slides.length / perView));
268    let idx = 0;
269
270    function rebuildDots(){
271      if(!dotsHost) return;
272      dotsHost.innerHTML = '';
273      for(let i = 0; i < pages; i++){
274        const b = document.createElement('button');
275        b.type = 'button';
276        b.className = 'tt-car-dot' + (i === idx ? ' active' : '');
277        b.setAttribute('aria-label', `Page ${i+1} of ${pages}`);
278        b.addEventListener('click', () => go(i));
279        dotsHost.appendChild(b);
280      }
281    }
282
283    function go(i){
284      idx = Math.max(0, Math.min(pages - 1, i));
285      const slideW = slides[0] ? slides[0].getBoundingClientRect().width : 0;
286      const gap = parseFloat(getComputedStyle(track).gap) || 0;
287      const offset = (slideW + gap) * (idx * perView);
288      track.style.transform = `translateX(-${offset}px)`;
289      if(dotsHost){
290        [...dotsHost.children].forEach((d, k) => d.classList.toggle('active', k === idx));
291      }
292      if(prevBtn) prevBtn.disabled = idx === 0;
293      if(nextBtn) nextBtn.disabled = idx === pages - 1;
294      if(counter) counter.textContent = `${idx+1} / ${pages}`;
295    }
296
297    if(prevBtn) prevBtn.addEventListener('click', () => go(idx - 1));
298    if(nextBtn) nextBtn.addEventListener('click', () => go(idx + 1));
299
300    /* touch swipe */
301    let startX = 0, dx = 0, touching = false;
302    viewport.addEventListener('touchstart', e => {
303      startX = e.touches[0].clientX; dx = 0; touching = true;
304    }, {passive: true});
305    viewport.addEventListener('touchmove', e => {
306      if(!touching) return;
307      dx = e.touches[0].clientX - startX;
308    }, {passive: true});
309    viewport.addEventListener('touchend', () => {
310      if(!touching) return;
311      touching = false;
312      if(Math.abs(dx) > 50) go(idx + (dx < 0 ? 1 : -1));
313    });
314
315    /* keyboard support on focused viewport */
316    viewport.setAttribute('tabindex', '0');
317    viewport.setAttribute('role', 'region');
318    viewport.setAttribute('aria-roledescription', 'carousel');
319    viewport.setAttribute('aria-label', `${baseLabel} carousel`);
320    viewport.addEventListener('keydown', e => {
321      if(e.key === 'ArrowLeft'){ e.preventDefault(); go(idx - 1); }
322      if(e.key === 'ArrowRight'){ e.preventDefault(); go(idx + 1); }
323    });
324
325    /* resize handler */
326    let lastW = window.innerWidth;
327    window.addEventListener('resize', () => {
328      if(Math.abs(window.innerWidth - lastW) < 40) return;
329      lastW = window.innerWidth;
330      perView = currentPerView();
331      pages = Math.max(1, Math.ceil(slides.length / perView));
332      idx = Math.min(idx, pages - 1);
333      rebuildDots();
334      requestAnimationFrame(() => go(idx));
335    });
336
337    rebuildDots();
338    /* defer first layout so flex widths settle */
339    requestAnimationFrame(() => requestAnimationFrame(() => go(0)));
340  }
341
342  document.querySelectorAll('[data-carousel]').forEach(initCarousel);
343})();

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.