website refresh: true numbers (56 apps), claims checked against the capability map, ten missing apps, an English twin of every public page (public, choice B), 404 page; site gates 13-18 + 11 decoys; R-902 opened
gates / gates (push) Failing after 13m1s
gates / gates (push) Failing after 13m1s
Part A: claim table documentation/audits/website-refresh-2026-10-08/claims.md; "100% open source" corrected from the licence read; unbacked claims cut (firewall, RAID, snapshots, new-machine restore, self-managed mode, household VPN, "never lost"). Part B: dawarich, docmost, grimmory, homebox, karakeep, mealie, metube, radicale, recipe-importer, sparkyfitness cards; plant-it and wger cut (not offered). Part C: /en/ twins, nav language switch, hreflang both ways, sitemap with xhtml:link, og-image-en.png. Part D: site_gates.py twins/lang/same-apps/no-Hungarian/FAQ-JSON-LD/tail. Operator ruling 9 + choice B in 10-localisation.md §11, §10.7. Register: R-902 (contact mailer source in no repo); R-813, R-784, R-559 annotated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
@@ -0,0 +1,601 @@
|
||||
<!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=2">
|
||||
<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><a href="/kapcsolat" lang="hu" hreflang="hu" class="lang-switch">Magyar</a></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>
|
||||
Reference in New Issue
Block a user