79c53b5f1a
gates / gates (push) Successful in 4m25s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
602 lines
29 KiB
HTML
602 lines
29 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Contact — ask for a personal offer for your home server | Felhom.eu</title>
|
|
<meta name="description" content="Write to us and we will make a personal offer for your home server in Hungary. A question? We help! E-mail: info@felhom.eu">
|
|
|
|
<meta name="keywords" content="contact, request an offer, home server offer, Felhom contact">
|
|
<meta name="robots" content="index, follow">
|
|
<link rel="canonical" href="https://felhom.eu/en/contact">
|
|
<link rel="alternate" hreflang="hu" href="https://felhom.eu/kapcsolat">
|
|
<link rel="alternate" hreflang="en" href="https://felhom.eu/en/contact">
|
|
<link rel="alternate" hreflang="x-default" href="https://felhom.eu/kapcsolat">
|
|
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://felhom.eu/en/contact">
|
|
<meta property="og:title" content="Contact — Felhom.eu">
|
|
<meta property="og:description" content="Write to us and we will make a personal offer for your home server.">
|
|
<meta property="og:image" content="https://felhom.eu/assets/og-image-en.png">
|
|
<meta property="og:locale" content="en_GB">
|
|
<meta property="og:site_name" content="Felhom.eu">
|
|
|
|
<meta name="twitter:card" content="summary">
|
|
<meta name="twitter:title" content="Contact — Felhom.eu">
|
|
<meta name="twitter:description" content="A question? Write to us: info@felhom.eu">
|
|
<meta name="twitter:image" content="https://felhom.eu/assets/og-image-en.png">
|
|
<!-- shared assets: bump ?v= together on any site.css/icons.svg change -->
|
|
<link rel="stylesheet" href="/assets/site.css?v=5">
|
|
<link rel="icon" type="image/png" href="/assets/logo_favicon_2.svg">
|
|
<script defer src="https://stats.felhom.eu/script.js" data-website-id="d419db57-5e1c-48df-bdb3-c29f25fb1661"></script>
|
|
</head>
|
|
<body class="page-kapcsolat">
|
|
<nav>
|
|
<div class="container">
|
|
<a href="/en/" class="logo">
|
|
<img src="/assets/logo.png" alt="Felhom.eu logo">
|
|
<span class="logo-text">felhom.eu</span>
|
|
</a>
|
|
<ul class="nav-links">
|
|
<li><a href="/en/#services">Services</a></li>
|
|
<li><a href="/en/apps">Apps</a></li>
|
|
<li><a href="/en/technology">Technology</a></li>
|
|
<li><a href="/en/backups">Backups</a></li>
|
|
<li><a href="/en/faq">FAQ</a></li>
|
|
<li><a href="/en/contact" class="active">Contact</a></li>
|
|
<li class="lang-globe-li"><details class="lang-globe"><summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-globe"/></svg></summary><ul class="lang-globe-menu"><li><a href="/kapcsolat" lang="hu" hreflang="hu" class="lang-globe-item">Magyar</a></li><li><a href="/en/contact" lang="en" hreflang="en" class="lang-globe-item current" aria-current="true">English</a></li></ul></details></li>
|
|
</ul>
|
|
<button class="hamburger" id="hamburger" aria-label="Open menu" aria-expanded="false">
|
|
<span class="hamburger-box">
|
|
<span class="hamburger-line"></span>
|
|
<span class="hamburger-line"></span>
|
|
<span class="hamburger-line"></span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
<div class="mobile-overlay" id="mobileOverlay"></div>
|
|
|
|
<section class="page-header">
|
|
<div class="container">
|
|
<h1>Write to <span>us!</span></h1>
|
|
<p>A question, or want to talk about what you need? Fill in the form and we will answer soon. We answer in English or Hungarian.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<main>
|
|
<section class="content-section" style="padding-top: 0;">
|
|
<div class="container">
|
|
<div class="contact-layout">
|
|
|
|
<!-- Form -->
|
|
<div class="contact-form-card">
|
|
<div id="formStatus" class="form-status"></div>
|
|
|
|
<form id="contactForm" novalidate>
|
|
<div style="position:absolute;left:-9999px;" aria-hidden="true">
|
|
<input type="text" name="website" tabindex="-1" autocomplete="off">
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="name">Name <span class="required">*</span></label>
|
|
<input type="text" id="name" name="name" placeholder="Your full name" required>
|
|
<div class="error-message" id="nameError">Enter your name.</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="email">E-mail <span class="required">*</span></label>
|
|
<input type="email" id="email" name="email" placeholder="you@example.com" required>
|
|
<div class="error-message" id="emailError">Enter a valid e-mail address.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="subject">Subject <span class="required">*</span></label>
|
|
<select id="subject" name="subject" required>
|
|
<option value="" disabled selected>Choose a topic...</option>
|
|
<option value="zart-teszt">Closed test — I want to be a tester</option>
|
|
<option value="erdeklodes">Interest — I would like to learn about the service</option>
|
|
<option value="arajanlat">Request a quote — I have specific needs</option>
|
|
<option value="tamogatas">Support — a question about my existing server</option>
|
|
<option value="egyeb">Other</option>
|
|
</select>
|
|
<div class="error-message" id="subjectError">Choose a topic.</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="message">Message <span class="required">*</span></label>
|
|
<textarea id="message" name="message" placeholder="Tell us how we can help..." required></textarea>
|
|
<div class="error-message" id="messageError">Write your message.</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Attach files <span style="color: var(--text-3); font-weight: 400;">(optional)</span></label>
|
|
<div class="file-upload-zone" id="dropZone">
|
|
<input type="file" id="fileInput" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.gif,.webp,.txt,.csv,.zip,.7z">
|
|
<div class="upload-icon"><svg class="ico-tile" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-paperclip"/></svg></div>
|
|
<div class="upload-text">
|
|
<strong>Click here</strong> or drag your files here
|
|
</div>
|
|
<div class="upload-hint">PDF, images, documents · max. 20 MB in total</div>
|
|
</div>
|
|
<div class="file-list" id="fileList"></div>
|
|
<div class="file-total-size" id="fileTotalSize"></div>
|
|
</div>
|
|
|
|
<div class="checkbox-group">
|
|
<input type="checkbox" id="privacy" name="privacy" required>
|
|
<label for="privacy">
|
|
I agree that Felhom.eu uses the details I give in this form to answer my enquiry. The details are not passed on to third parties.
|
|
</label>
|
|
</div>
|
|
<div class="error-message" id="privacyError" style="margin-top: -16px; margin-bottom: 16px;">Your consent to the use of your details is needed.</div>
|
|
|
|
<button type="submit" class="submit-button" id="submitBtn">
|
|
<span class="btn-text">Send message →</span>
|
|
<span class="spinner"></span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="contact-sidebar">
|
|
<div class="sidebar-card">
|
|
<h3><span class="icon"><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-mail"/></svg></span> E-mail us directly</h3>
|
|
<p>If you would rather send an e-mail:</p>
|
|
<a href="mailto:info@felhom.eu" class="direct-email">
|
|
<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-mail"/></svg> info@felhom.eu
|
|
</a>
|
|
</div>
|
|
|
|
<div class="sidebar-card">
|
|
<h3><span class="icon"><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-clock"/></svg></span> Reply time</h3>
|
|
<p>We usually answer <strong style="color: var(--blue-bright);">within 24 hours</strong> on working days.</p>
|
|
<p>If a support question is urgent, say so in the subject.</p>
|
|
</div>
|
|
|
|
<div class="sidebar-card">
|
|
<h3><span class="icon"><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-lightbulb"/></svg></span> How can we help?</h3>
|
|
<ul class="sidebar-list">
|
|
<li><span class="li-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-monitor"/></svg></span> Advice on a server set-up</li>
|
|
<li><span class="li-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg></span> Installing and setting up apps</li>
|
|
<li><span class="li-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-lock"/></svg></span> Setting up backups</li>
|
|
<li><span class="li-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-globe"/></svg></span> Setting up remote access</li>
|
|
<li><span class="li-icon"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-wrench"/></svg></span> Maintaining an existing server</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<footer>
|
|
<div class="container">
|
|
<p>© 2026 felhom.eu — Your own cloud, your own rules. Felhom serves households in Hungary.</p>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
(() => {
|
|
'use strict';
|
|
|
|
// ==============================================
|
|
// CONFIGURATION
|
|
// ==============================================
|
|
const CONFIG = {
|
|
// TODO: Replace with your actual form backend endpoint.
|
|
// Options:
|
|
// 1. Self-hosted: Deploy a small container (e.g. Go/Python) that receives POST and sends via SMTP
|
|
// 2. Formspree: 'https://formspree.io/f/YOUR_FORM_ID'
|
|
// 3. Cloudflare Workers: Deploy a worker that forwards to email
|
|
//
|
|
// For now, falls back to mailto: if no endpoint is configured.
|
|
formEndpoint: '/api/contact',
|
|
|
|
maxTotalFileSize: 20 * 1024 * 1024, // 20 MB
|
|
maxSingleFileSize: 10 * 1024 * 1024, // 10 MB per file
|
|
maxFiles: 5,
|
|
allowedExtensions: ['pdf','doc','docx','xls','xlsx','png','jpg','jpeg','gif','webp','txt','csv','zip','7z'],
|
|
};
|
|
|
|
// ==============================================
|
|
// DOM REFS
|
|
// ==============================================
|
|
const form = document.getElementById('contactForm');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const formStatus = document.getElementById('formStatus');
|
|
const dropZone = document.getElementById('dropZone');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const fileListEl = document.getElementById('fileList');
|
|
const fileTotalSizeEl = document.getElementById('fileTotalSize');
|
|
|
|
let attachedFiles = [];
|
|
|
|
// Preselect the subject from ?tema= (the closed-test CTA on the landing page
|
|
// links here as /en/contact?tema=zart-teszt — the same values as the Hungarian page). Unknown values are ignored.
|
|
(function preselectSubject() {
|
|
const wanted = new URLSearchParams(window.location.search).get('tema');
|
|
if (!wanted) return;
|
|
const select = document.getElementById('subject');
|
|
if (!select) return;
|
|
const match = Array.from(select.options).some(o => o.value === wanted);
|
|
if (match) {
|
|
select.value = wanted;
|
|
} else {
|
|
console.warn('[contact] unknown ?tema= value, ignoring:', wanted);
|
|
}
|
|
})();
|
|
|
|
// ==============================================
|
|
// VALIDATION
|
|
// ==============================================
|
|
function validateField(field) {
|
|
const errorEl = document.getElementById(field.id + 'Error');
|
|
let valid = true;
|
|
|
|
if (field.type === 'checkbox') {
|
|
valid = field.checked;
|
|
} else if (field.required && !field.value.trim()) {
|
|
valid = false;
|
|
} else if (field.type === 'email' && field.value.trim()) {
|
|
valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(field.value.trim());
|
|
}
|
|
|
|
if (valid) {
|
|
field.classList.remove('error');
|
|
if (errorEl) errorEl.classList.remove('visible');
|
|
} else {
|
|
field.classList.add('error');
|
|
if (errorEl) errorEl.classList.add('visible');
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
function validateForm() {
|
|
const fields = ['name', 'email', 'subject', 'message', 'privacy'];
|
|
let allValid = true;
|
|
|
|
fields.forEach(id => {
|
|
const field = document.getElementById(id);
|
|
if (!validateField(field)) allValid = false;
|
|
});
|
|
|
|
return allValid;
|
|
}
|
|
|
|
// Live validation on blur
|
|
['name', 'email', 'subject', 'message', 'privacy'].forEach(id => {
|
|
const el = document.getElementById(id);
|
|
const event = el.type === 'checkbox' ? 'change' : 'blur';
|
|
el.addEventListener(event, () => validateField(el));
|
|
// Clear error on input
|
|
el.addEventListener('input', () => {
|
|
if (el.classList.contains('error')) validateField(el);
|
|
});
|
|
});
|
|
|
|
// ==============================================
|
|
// FILE HANDLING
|
|
// ==============================================
|
|
function formatFileSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
}
|
|
|
|
function getFileIcon(name) {
|
|
const ext = name.split('.').pop().toLowerCase();
|
|
const icons = {
|
|
pdf: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-file-text"/></svg>', doc: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-file-text"/></svg>', docx: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-file-text"/></svg>', xls: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chart-column"/></svg>', xlsx: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chart-column"/></svg>',
|
|
png: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-image"/></svg>', jpg: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-image"/></svg>', jpeg: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-image"/></svg>', gif: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-image"/></svg>', webp: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-image"/></svg>',
|
|
txt: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-file-text"/></svg>', csv: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chart-column"/></svg>', zip: '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg>', '7z': '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg>',
|
|
};
|
|
return icons[ext] || '<svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-paperclip"/></svg>';
|
|
}
|
|
|
|
function getTotalFileSize() {
|
|
return attachedFiles.reduce((sum, f) => sum + f.size, 0);
|
|
}
|
|
|
|
function addFiles(fileArray) {
|
|
for (const file of fileArray) {
|
|
// Check extension
|
|
const ext = file.name.split('.').pop().toLowerCase();
|
|
if (!CONFIG.allowedExtensions.includes(ext)) {
|
|
showStatus(`The file type of "${file.name}" is not allowed.`, 'error-state');
|
|
continue;
|
|
}
|
|
|
|
// Check single file size
|
|
if (file.size > CONFIG.maxSingleFileSize) {
|
|
showStatus(`"${file.name}" is too large (max. ${formatFileSize(CONFIG.maxSingleFileSize)} per file).`, 'error-state');
|
|
continue;
|
|
}
|
|
|
|
// Check max file count
|
|
if (attachedFiles.length >= CONFIG.maxFiles) {
|
|
showStatus(`You can attach at most ${CONFIG.maxFiles} files.`, 'error-state');
|
|
break;
|
|
}
|
|
|
|
// Check total size
|
|
if (getTotalFileSize() + file.size > CONFIG.maxTotalFileSize) {
|
|
showStatus(`The files together are over the ${formatFileSize(CONFIG.maxTotalFileSize)} limit.`, 'error-state');
|
|
break;
|
|
}
|
|
|
|
// Check duplicate
|
|
if (attachedFiles.some(f => f.name === file.name && f.size === file.size)) {
|
|
continue;
|
|
}
|
|
|
|
attachedFiles.push(file);
|
|
}
|
|
|
|
renderFileList();
|
|
}
|
|
|
|
function removeFile(index) {
|
|
attachedFiles.splice(index, 1);
|
|
renderFileList();
|
|
// Reset file input so same file can be re-added
|
|
fileInput.value = '';
|
|
}
|
|
|
|
function renderFileList() {
|
|
if (attachedFiles.length === 0) {
|
|
fileListEl.innerHTML = '';
|
|
fileTotalSizeEl.textContent = '';
|
|
return;
|
|
}
|
|
|
|
fileListEl.innerHTML = attachedFiles.map((file, i) => `
|
|
<div class="file-item">
|
|
<span class="file-icon">${getFileIcon(file.name)}</span>
|
|
<div class="file-info">
|
|
<div class="file-name" title="${file.name}">${file.name}</div>
|
|
<div class="file-size">${formatFileSize(file.size)}</div>
|
|
</div>
|
|
<button type="button" class="file-remove" data-index="${i}" title="Remove">×</button>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Bind remove buttons
|
|
fileListEl.querySelectorAll('.file-remove').forEach(btn => {
|
|
btn.addEventListener('click', () => removeFile(parseInt(btn.dataset.index)));
|
|
});
|
|
|
|
// Total size indicator
|
|
const total = getTotalFileSize();
|
|
const pct = (total / CONFIG.maxTotalFileSize) * 100;
|
|
fileTotalSizeEl.textContent = `${formatFileSize(total)} / ${formatFileSize(CONFIG.maxTotalFileSize)}`;
|
|
fileTotalSizeEl.className = 'file-total-size';
|
|
if (pct > 90) fileTotalSizeEl.classList.add('error-text');
|
|
else if (pct > 70) fileTotalSizeEl.classList.add('warning');
|
|
}
|
|
|
|
// File input change
|
|
fileInput.addEventListener('change', () => {
|
|
if (fileInput.files.length) {
|
|
addFiles(Array.from(fileInput.files));
|
|
fileInput.value = '';
|
|
}
|
|
});
|
|
|
|
// Drag & drop
|
|
['dragenter', 'dragover'].forEach(evt => {
|
|
dropZone.addEventListener(evt, e => {
|
|
e.preventDefault();
|
|
dropZone.classList.add('dragover');
|
|
});
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(evt => {
|
|
dropZone.addEventListener(evt, e => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('dragover');
|
|
});
|
|
});
|
|
|
|
dropZone.addEventListener('drop', e => {
|
|
if (e.dataTransfer.files.length) {
|
|
addFiles(Array.from(e.dataTransfer.files));
|
|
}
|
|
});
|
|
|
|
// ==============================================
|
|
// STATUS MESSAGES
|
|
// ==============================================
|
|
function showStatus(message, type) {
|
|
formStatus.textContent = message;
|
|
formStatus.className = 'form-status ' + type;
|
|
formStatus.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
|
|
if (type === 'error-state') {
|
|
setTimeout(() => {
|
|
formStatus.className = 'form-status';
|
|
}, 6000);
|
|
}
|
|
}
|
|
|
|
// ==============================================
|
|
// FORM SUBMISSION
|
|
// ==============================================
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
formStatus.className = 'form-status';
|
|
|
|
if (!validateForm()) {
|
|
// Build specific error message
|
|
const missing = [];
|
|
if (!document.getElementById('name').value.trim()) missing.push('name');
|
|
const emailVal = document.getElementById('email').value.trim();
|
|
if (!emailVal || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal)) missing.push('email');
|
|
if (!document.getElementById('subject').value) missing.push('subject');
|
|
if (!document.getElementById('message').value.trim()) missing.push('message');
|
|
if (!document.getElementById('privacy').checked) missing.push('consent');
|
|
|
|
if (missing.length === 1 && missing[0] === 'consent') {
|
|
showStatus('Tick the consent box to send the form.', 'error-state');
|
|
} else {
|
|
showStatus('Fill in every required field.', 'error-state');
|
|
}
|
|
return;
|
|
}
|
|
|
|
submitBtn.classList.add('loading');
|
|
submitBtn.disabled = true;
|
|
|
|
const formData = {
|
|
name: document.getElementById('name').value.trim(),
|
|
email: document.getElementById('email').value.trim(),
|
|
subject: document.getElementById('subject').value,
|
|
message: document.getElementById('message').value.trim(),
|
|
};
|
|
|
|
// Subject label mapping
|
|
const subjectLabels = {
|
|
'zart-teszt': 'Application — closed test (EN)',
|
|
erdeklodes: 'Interest (EN)',
|
|
arajanlat: 'Quote request (EN)',
|
|
tamogatas: 'Support (EN)',
|
|
egyeb: 'Other (EN)',
|
|
};
|
|
|
|
try {
|
|
if (CONFIG.formEndpoint) {
|
|
// --- Backend submission ---
|
|
const data = new FormData();
|
|
data.append('name', formData.name);
|
|
data.append('email', formData.email);
|
|
data.append('subject', subjectLabels[formData.subject] || formData.subject);
|
|
data.append('message', formData.message);
|
|
|
|
attachedFiles.forEach(file => {
|
|
data.append('attachments', file);
|
|
});
|
|
|
|
const response = await fetch(CONFIG.formEndpoint, {
|
|
method: 'POST',
|
|
body: data,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Server error: ${response.status}`);
|
|
}
|
|
|
|
showStatus('Your message has been sent. We will answer soon.', 'success');
|
|
form.reset();
|
|
attachedFiles = [];
|
|
renderFileList();
|
|
|
|
} else {
|
|
// --- Fallback: mailto ---
|
|
const subjectLine = `[Felhom.eu] ${subjectLabels[formData.subject] || formData.subject}`;
|
|
const body = [
|
|
`Name: ${formData.name}`,
|
|
`Email: ${formData.email}`,
|
|
``,
|
|
formData.message,
|
|
].join('\n');
|
|
|
|
const mailtoUrl = `mailto:info@felhom.eu?subject=${encodeURIComponent(subjectLine)}&body=${encodeURIComponent(body)}`;
|
|
window.location.href = mailtoUrl;
|
|
|
|
if (attachedFiles.length > 0) {
|
|
showStatus('Your e-mail program has opened. Add the attached files to the e-mail by hand: a mailto: link cannot carry attachments.', 'error-state');
|
|
} else {
|
|
showStatus('Your e-mail program has opened with your message. Send the e-mail to get in touch.', 'success');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Form submission error:', err);
|
|
showStatus(`Something went wrong while sending. Try again, or write to info@felhom.eu directly.`, 'error-state');
|
|
} finally {
|
|
submitBtn.classList.remove('loading');
|
|
submitBtn.disabled = false;
|
|
}
|
|
});
|
|
|
|
// ==============================================
|
|
// DEBUG HELPER
|
|
// ==============================================
|
|
// Uncomment to test form states:
|
|
// window.debugForm = {
|
|
// showSuccess: () => showStatus('Your message has been sent.', 'success'),
|
|
// showError: () => showStatus('Something went wrong.', 'error-state'),
|
|
// getFiles: () => console.log(attachedFiles),
|
|
// getFormData: () => {
|
|
// const fd = new FormData(form);
|
|
// for (const [k,v] of fd.entries()) console.log(k, v);
|
|
// },
|
|
// };
|
|
|
|
})();
|
|
</script>
|
|
<script>
|
|
(function() {
|
|
var hamburger = document.getElementById('hamburger');
|
|
var navLinks = document.querySelector('.nav-links');
|
|
var overlay = document.getElementById('mobileOverlay');
|
|
var body = document.body;
|
|
|
|
if (!hamburger || !navLinks || !overlay) {
|
|
console.warn('[mobile-menu] Missing elements:', {
|
|
hamburger: !!hamburger,
|
|
navLinks: !!navLinks,
|
|
overlay: !!overlay
|
|
});
|
|
return;
|
|
}
|
|
|
|
function openMenu() {
|
|
hamburger.classList.add('is-active');
|
|
navLinks.classList.add('is-open');
|
|
overlay.classList.add('is-visible');
|
|
body.classList.add('menu-open');
|
|
hamburger.setAttribute('aria-expanded', 'true');
|
|
hamburger.setAttribute('aria-label', 'Close menu');
|
|
}
|
|
|
|
function closeMenu() {
|
|
hamburger.classList.remove('is-active');
|
|
navLinks.classList.remove('is-open');
|
|
overlay.classList.remove('is-visible');
|
|
body.classList.remove('menu-open');
|
|
hamburger.setAttribute('aria-expanded', 'false');
|
|
hamburger.setAttribute('aria-label', 'Open menu');
|
|
}
|
|
|
|
hamburger.addEventListener('click', function() {
|
|
if (navLinks.classList.contains('is-open')) {
|
|
closeMenu();
|
|
} else {
|
|
openMenu();
|
|
}
|
|
});
|
|
|
|
overlay.addEventListener('click', closeMenu);
|
|
|
|
navLinks.querySelectorAll('a').forEach(function(link) {
|
|
link.addEventListener('click', closeMenu);
|
|
});
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape' && navLinks.classList.contains('is-open')) {
|
|
closeMenu();
|
|
}
|
|
});
|
|
|
|
window.addEventListener('resize', function() {
|
|
if (window.innerWidth > 900 && navLinks.classList.contains('is-open')) {
|
|
closeMenu();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|