Files
felhom.eu/website/en/contact.html
T
admin a2ca6eb413
gates / gates (push) Failing after 6m36s
Website: a page for people who search „felhő tárhely" (R-1033..R-1035)
Search Console (3 months, read 2026-10-14): 48 impressions, 1 click, average
position 73.6; the top query is „felhő tárhely" (19). No page on the site used
the phrase — Google was showing the home page by accident.

New pair /sajat-felho-tarhely + /en/own-cloud-storage: what cloud storage is and
the three ways to have one; what to do when the Google or iCloud storage is full
(the free clean-up first, linked to Google's and Apple's own help; the paid
options second); a five-year cost table; where the data sits; who should NOT
choose this; the closed test. Registered in site_gates.py TWINS/URL/SHELL, both
URLs in sitemap.xml (22 -> 24). FAQPage JSON-LD derived from the visible text.

Prices each read 2026-10-14 on the provider's own Hungarian page, with the URLs
and the date in an HTML comment and a visible „Árak" line: Google AI Plus (2 TB)
— Google One's 2 TB plan — 35 990 Ft/év (behind the „Éves" toggle; a plain fetch
sees only the monthly 3 590 Ft), iCloud+ 2 TB 4 490 Ft/hó, Microsoft 365 Családi
(2 TB shared, no longer 1 TB per person) 46 499 Ft/év.

Felhom's own cost follows what the site already said (operator ruling): machine
and install one-off, power, off-site storage monthly by volume, operation
OPTIONAL, no fee during the closed test. Power is stated as an idle measurement
(nyugalmi állapotban 7-9 W, ~75 kWh/év, from audits/night-power-2026-10-13) and
the page says a year with daytime use is more; no daytime figure and no
electricity tariff are given, because neither is measured or sourced.

FAQ corrected (both languages, JSON-LD re-derived): the cost answer carried
unsourced, undated prices and summed Spotify and a password manager into a
storage comparison; and the page contradicted itself on electricity („havi
néhány ezer forint" vs „évi 5.000–15.000 Ft"). App count 60 -> 62 in the four
places that were stale.

Three defects found on the rendered page and fixed: a .vs-row link fell back to
the browser-default blue (contrast 1.99 -> 7.17); 24px of horizontal scroll at
390px because .method-pros li is a nowrap flex row and the new items contain
links (each item's content wrapped in one span); .info-card and .method-pros
were scoped to the pages that first used them. site.css ?v=31 on all 26 pages.
The shared .content-section padding was NOT changed — it is identical on every
page; the new page only lacked the alt-bg alternation, and now has it.

R-1033 the apps page lags the catalog by two apps (plant-it, wger); R-1034 gate
17 checks the FAQ JSON-LD on two hard-coded pages only; R-1035 the SEO result,
with dated checks on 2026-11-11 and 2026-12-09.
2026-10-14 18:51:42 +02:00

598 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<script>document.documentElement.className=document.documentElement.className.replace('no-js','js');</script>
<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 me and I will make a personal offer for your home server in Hungary. A question? I 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 me and I will make a personal offer for your home server.">
<meta property="og:image" content="https://felhom.eu/assets/og-image-en.png?v=3">
<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 me: info@felhom.eu">
<meta name="twitter:image" content="https://felhom.eu/assets/og-image-en.png?v=3">
<link rel="preload" as="font" type="font/woff2" crossorigin href="/assets/fonts/pjs-latin.woff2">
<link rel="preload" as="font" type="font/woff2" crossorigin href="/assets/fonts/pjs-latin-ext.woff2">
<!-- shared assets: bump ?v= together on any site.css/icons.svg change -->
<link rel="stylesheet" href="/assets/site.css?v=31">
<link rel="stylesheet" href="/assets/shell.css?v=25">
<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="shell page-kapcsolat">
<header class="mobile-topbar">
<a href="/en/" class="mobile-topbar-logo"><img src="/assets/logo_notext_white.svg" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" id="navBurger" aria-expanded="false" aria-controls="sidebar" aria-label="Menu">
<span class="hamburger-box"><span class="hamburger-line"></span><span class="hamburger-line"></span><span class="hamburger-line"></span></span>
</button>
</header>
<div class="nav-backdrop" id="navBackdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<a href="/en/" class="sidebar-logo-link">
<img src="/assets/logo_notext_white.svg" alt="" class="sidebar-logo" aria-hidden="true">
<img src="/assets/logo-wordmark.png" alt="felhom.eu" class="sidebar-word">
</a>
</div>
<ul class="nav-links">
<li><a href="/en/"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-home"/></svg>Home</a></li>
<li><a href="/en/services"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-rocket"/></svg>Services</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-apps"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-package"/></svg>Applications<svg class="ico nav-chevron" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-chevron-down"/></svg></button>
<ul id="nav-group-apps" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/en/apps">About the apps</a></li>
<li><a href="/en/apps#files">Files &amp; Office</a></li>
<li><a href="/en/apps#media">Media &amp; Entertainment</a></li>
<li><a href="/en/apps#home">Home &amp; Lifestyle</a></li>
<li><a href="/en/apps#productivity">Productivity &amp; Knowledge</a></li>
<li><a href="/en/apps#system">System &amp; Development</a></li>
</ul>
</li>
<li><a href="/en/sizing"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-cpu"/></svg>Which machine do I need?</a></li>
<li><a href="/en/technology"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-layers"/></svg>Technology</a></li>
<li><a href="/en/backups"><svg class="ico" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-shield-check"/></svg>Backups</a></li>
</ul>
<div class="sidebar-bottom">
<a href="/en/testing" class="btn btn-primary sidebar-cta">Apply as a tester →</a>
<div class="nav-group-label">About Felhom</div>
<ul class="nav-links nav-links-sub">
<li><a href="/en/#how-it-works">How it works</a></li>
<li><a href="/en/faq">FAQ</a></li>
<li><a href="/en/contact" class="active">Contact</a></li>
</ul>
<div class="sidebar-footer">
<details class="lang-globe"><summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico" 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>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<h1>Write to <span>me!</span></h1>
</div>
<p class="page-lede">A question, or want to talk about what you need? Fill in the form and I will answer soon. I answer in English or Hungarian.</p>
<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">
<label class="cb-hit" for="privacy"><input type="checkbox" id="privacy" name="privacy" required></label>
<label for="privacy">
I have read the <a href="/adatkezeles" target="_blank" rel="noopener">privacy notice (in Hungarian)</a> and agree that my name, e-mail address, message and attached files are used to answer my enquiry. The message is carried by mail-sending and mailbox providers. I can withdraw this consent at any time at info@felhom.eu.
</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 me 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>I usually answer <strong style="color: var(--blue-bright);">within 1–2 days</strong>.</p>
<p>I do not promise a reply time during the closed test – the test terms say the same.</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 I 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-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-monitor"/></svg></span> Questions about the closed test</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<section class="content-section alt-bg" id="ki-all-mogotte">
<div class="container">
<div class="who">
<figure class="who-photo">
<img src="/assets/operator-portrait.webp" alt="Viktor Nagyfenyvesi, who runs Felhom" width="720" height="900" loading="lazy">
</figure>
<div class="who-text">
<h2>Who is <span>behind it</span></h2>
<p><strong>I am Viktor Nagyfenyvesi.</strong> I have designed networks and infrastructure for ten years, I work as a DevOps engineer, and I have run my own server at home for years; the current one has been running for about a year.</p>
<p>Felhom started because I could not find this service here. Plenty of people can build their own server, but afterwards someone has to back it up, update it and keep an eye on it. That is the part I take on.</p>
<p>If you write, I answer, and I do not promise anything the box cannot do today.</p>
</div>
</div>
</div>
</section>
<footer>
<div class="container">
<p>© 2026 felhom.eu – Your own cloud, your own rules. Felhom serves households in Hungary.</p>
<p class="footer-legal">Operated by Nagyfenyvesi Viktor · <a href="mailto:info@felhom.eu">info@felhom.eu</a> · <a href="/adatkezeles">Privacy notice (Hungarian)</a> · <a href="/feltetelek">Terms (Hungarian)</a> · <a href="https://www.facebook.com/felhom.eu">Facebook</a></p>
</div>
</footer>
</main>
<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">&#215;</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. I 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 b=document.getElementById('navBurger'),s=document.getElementById('sidebar'),k=document.getElementById('navBackdrop');
if(!b||!s||!k)return;
function open(){s.classList.add('is-open');k.hidden=false;document.body.classList.add('nav-open');b.setAttribute('aria-expanded','true');}
function close(){s.classList.remove('is-open');k.hidden=true;document.body.classList.remove('nav-open');b.setAttribute('aria-expanded','false');}
b.addEventListener('click',function(){s.classList.contains('is-open')?close():open();});
k.addEventListener('click',close);
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&s.classList.contains('is-open')){close();b.focus();}});
s.querySelectorAll('a').forEach(function(l){l.addEventListener('click',close);});
window.addEventListener('resize',function(){if(window.innerWidth>768)close();});
})();
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var g=btn.closest('.nav-group');if(!g)return;
var willOpen=!g.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(x){
x.classList.remove('is-open');
var t=x.querySelector('.nav-group-toggle');if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){g.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
</script>
</body>
</html>