site.js 4.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. (() => {
  2. const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
  3. document.getElementById('year').textContent = new Date().getFullYear();
  4. document.getElementById('ticket-number').textContent = `TKT-${String(Math.floor(1000 + Math.random() * 9000))}`;
  5. document.getElementById('started-at').value = Math.floor(Date.now() / 1000);
  6. const reveals = document.querySelectorAll('.reveal');
  7. if (reducedMotion || !('IntersectionObserver' in window)) {
  8. reveals.forEach((item) => item.classList.add('visible'));
  9. } else {
  10. const observer = new IntersectionObserver((entries) => {
  11. entries.forEach((entry) => {
  12. if (entry.isIntersecting) {
  13. entry.target.classList.add('visible');
  14. observer.unobserve(entry.target);
  15. }
  16. });
  17. }, { threshold: 0.12 });
  18. reveals.forEach((item) => observer.observe(item));
  19. }
  20. const form = document.getElementById('contact-form');
  21. const status = document.getElementById('form-status');
  22. form.addEventListener('submit', async (event) => {
  23. event.preventDefault();
  24. if (!form.reportValidity()) return;
  25. form.classList.remove('error', 'success');
  26. form.classList.add('sending');
  27. status.textContent = 'Routing your ticket…';
  28. form.querySelector('.button-label').textContent = 'Sending';
  29. try {
  30. const response = await fetch(form.action, { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } });
  31. const result = await response.json();
  32. if (!response.ok || !result.ok) throw new Error(result.message || 'Message could not be sent.');
  33. form.classList.add('success');
  34. status.textContent = result.message;
  35. form.reset();
  36. document.getElementById('started-at').value = Math.floor(Date.now() / 1000);
  37. form.querySelector('.button-label').textContent = 'Ticket received ✓';
  38. } catch (error) {
  39. form.classList.add('error');
  40. status.textContent = error.message;
  41. form.querySelector('.button-label').textContent = 'Try again';
  42. } finally {
  43. form.classList.remove('sending');
  44. }
  45. });
  46. if (reducedMotion) return;
  47. const canvas = document.getElementById('particle-field');
  48. const context = canvas.getContext('2d');
  49. const pointer = { x: -1000, y: -1000 };
  50. let particles = [];
  51. let width = 0;
  52. let height = 0;
  53. const resize = () => {
  54. const ratio = Math.min(devicePixelRatio || 1, 2);
  55. width = innerWidth;
  56. height = innerHeight;
  57. canvas.width = width * ratio;
  58. canvas.height = height * ratio;
  59. canvas.style.width = `${width}px`;
  60. canvas.style.height = `${height}px`;
  61. context.setTransform(ratio, 0, 0, ratio, 0, 0);
  62. particles = Array.from({ length: Math.min(55, Math.floor(width / 24)) }, () => ({
  63. x: Math.random() * width, y: Math.random() * height, vx: (Math.random() - .5) * .18, vy: (Math.random() - .5) * .18, r: Math.random() * 1.4 + .4
  64. }));
  65. };
  66. addEventListener('resize', resize, { passive: true });
  67. addEventListener('pointermove', (event) => { pointer.x = event.clientX; pointer.y = event.clientY; }, { passive: true });
  68. addEventListener('pointerleave', () => { pointer.x = pointer.y = -1000; });
  69. resize();
  70. const draw = () => {
  71. context.clearRect(0, 0, width, height);
  72. particles.forEach((particle, index) => {
  73. particle.x += particle.vx; particle.y += particle.vy;
  74. if (particle.x < 0 || particle.x > width) particle.vx *= -1;
  75. if (particle.y < 0 || particle.y > height) particle.vy *= -1;
  76. const pdx = pointer.x - particle.x, pdy = pointer.y - particle.y, pointerDistance = Math.hypot(pdx, pdy);
  77. if (pointerDistance < 130) { particle.x -= pdx * .0008; particle.y -= pdy * .0008; }
  78. context.beginPath(); context.arc(particle.x, particle.y, particle.r, 0, Math.PI * 2); context.fillStyle = '#6fc9ff55'; context.fill();
  79. for (let j = index + 1; j < particles.length; j++) {
  80. const other = particles[j], distance = Math.hypot(particle.x - other.x, particle.y - other.y);
  81. if (distance < 115) { context.beginPath(); context.moveTo(particle.x, particle.y); context.lineTo(other.x, other.y); context.strokeStyle = `rgba(70,185,255,${(1 - distance / 115) * .09})`; context.stroke(); }
  82. }
  83. });
  84. requestAnimationFrame(draw);
  85. };
  86. draw();
  87. })();