Files
soroushasadi/wwwroot/js/app.js
T
soroush.asadi 97bd2a12df
deploy / deploy (push) Successful in 1m20s
Redesign public site: minimal light editorial theme
Full design refactor of the public surface (home, blog, layout) using the
taste-skill anti-slop rules. Admin CMS is untouched.

- Single locked light theme: #fafafa bg, #18181b text, one accent #2563eb
- Syne headings + system body + Vazirmatn (fa); hairline rules, no glows/cards
- Remove AI tells: 5-colour palette, gradient text, neon glows, custom cursor,
  particle canvas, typewriter, scroll cue, per-section eyebrows, progress bars
- Replace window scroll listener with an IntersectionObserver sentinel
- 8 distinct section layouts; portfolio uses typographic covers (no broken imgs)
- Zero em-dashes in visible copy; fix relative-path-safe asset refs
- Add missing wwwroot/logo-mark.svg (was 404)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:16:06 +03:30

79 lines
3.3 KiB
JavaScript

/* ════════════════════════════════════════════════════════════════════════
Minimal interactions. MOTION_INTENSITY 3: hover + a single subtle reveal.
No custom cursor, no particle canvas, no typewriter, no scroll listeners.
════════════════════════════════════════════════════════════════════════ */
/* ─── Navbar border on scroll (IntersectionObserver, not a scroll listener) */
(function () {
const nav = document.getElementById('navbar');
const sentinel = document.getElementById('nav-sentinel');
if (!nav || !sentinel) return;
const io = new IntersectionObserver(
([entry]) => nav.classList.toggle('scrolled', !entry.isIntersecting),
{ threshold: 0 }
);
io.observe(sentinel);
})();
/* ─── Mobile menu toggle ─────────────────────────────────────────────── */
(function () {
const btn = document.getElementById('menu-btn');
const menu = document.getElementById('mobile-menu');
if (!btn || !menu) return;
btn.addEventListener('click', () => {
const hidden = menu.classList.toggle('hidden');
btn.setAttribute('aria-expanded', String(!hidden));
});
menu.querySelectorAll('a').forEach(a => a.addEventListener('click', () => menu.classList.add('hidden')));
})();
/* ─── Scroll reveal (one subtle entry per element, then unobserve) ────── */
(function () {
const els = document.querySelectorAll('.reveal');
if (!els.length) return;
if (!('IntersectionObserver' in window)) {
els.forEach(el => el.classList.add('visible'));
return;
}
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); }
});
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
els.forEach(el => io.observe(el));
})();
/* ─── Contact form (AJAX, JSON → /contact) ───────────────────────────── */
(function () {
const form = document.getElementById('contact-form');
if (!form) return;
const status = document.getElementById('contact-status');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(form));
const btn = form.querySelector('[type="submit"]');
if (btn) btn.disabled = true;
const setStatus = (msg, cls) => { if (status) { status.textContent = msg; status.className = 'mt-1 text-sm ' + cls; } };
try {
const res = await fetch('/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (res.ok) {
setStatus(form.dataset.successMsg || 'Sent.', 'text-emerald-600');
form.reset();
} else {
setStatus(form.dataset.errorMsg || 'Something went wrong.', 'text-red-600');
}
} catch {
setStatus(form.dataset.errorMsg || 'Network error.', 'text-red-600');
} finally {
if (btn) btn.disabled = false;
}
});
})();