Initial commit: sito NexStudio (Astro) con Cloudflare, CMS cookie e contenuti legali

- Home: hero, prodotti, servizi, stack, FAQ, CTA con form contatti inline (glossy) e particelle
- Header nero, menu con scroll-spy e alone su voce attiva, CTA Chattiamo (pre-chat)
- Pagine: privacy, cookie, GDPR, terms, about, codice etico, modello organizzativo, dove siamo, news
- Cookie consent first-party, chat gate, stampe-friendly su SubpageLayout
- Footer: Sezioni con Contattaci, Azienda con Dove siamo senza link per ora
- .gitignore: aggiunto .wrangler/ per stato locale Cloudflare

Made-with: Cursor
This commit is contained in:
Javaxman
2026-04-22 13:07:40 +02:00
commit 16c380dfda
64 changed files with 25028 additions and 0 deletions

View File

@ -0,0 +1,215 @@
interface ChatGateCfg {
entryUrl: string;
openInNewTab: boolean;
actionUrl: string;
mailto: string;
notifySubject: string;
successRedirect: string;
fieldName: string;
fieldEmail: string;
openChatAfterSubmit: boolean;
}
const OVERLAY_ID = 'nx-chat-gate-overlay';
const PANEL_SEL = '[data-nx-chat-gate-panel]';
function readCfg(): ChatGateCfg {
const el = document.getElementById('nx-chat-gate-model');
const raw = el?.textContent?.trim();
if (!raw) {
return {
entryUrl: '/#contatti',
openInNewTab: false,
actionUrl: '',
mailto: '',
notifySubject: '',
successRedirect: '',
fieldName: 'name',
fieldEmail: 'email',
openChatAfterSubmit: false,
};
}
return JSON.parse(raw) as ChatGateCfg;
}
function overlayEl(): HTMLElement | null {
return document.getElementById(OVERLAY_ID);
}
function isOpen(): boolean {
const el = overlayEl();
return Boolean(el && !el.hasAttribute('hidden'));
}
function showFormStep(): void {
document.getElementById('nx-chat-gate-step-form')?.classList.remove('hidden');
document.getElementById('nx-chat-gate-step-success')?.classList.add('hidden');
}
function showSuccessStep(): void {
document.getElementById('nx-chat-gate-step-form')?.classList.add('hidden');
document.getElementById('nx-chat-gate-step-success')?.classList.remove('hidden');
}
function closePanel(): void {
const el = overlayEl();
if (!el) return;
el.setAttribute('hidden', '');
el.setAttribute('aria-hidden', 'true');
document.body.classList.remove('overflow-hidden');
const f = document.getElementById('nx-chat-gate-form') as HTMLFormElement | null;
f?.reset();
const err = document.getElementById('nx-chat-gate-form-error');
if (err) {
err.textContent = '';
err.classList.add('hidden');
}
showFormStep();
}
function openChat(): void {
const cfg = readCfg();
const url = cfg.entryUrl;
if (url.startsWith('http')) {
window.open(url, '_blank', 'noopener,noreferrer');
} else {
window.location.assign(url);
}
}
function openPanel(): void {
const el = overlayEl();
if (!el) return;
el.removeAttribute('hidden');
el.setAttribute('aria-hidden', 'false');
document.body.classList.add('overflow-hidden');
showFormStep();
requestAnimationFrame(() => {
el.querySelector<HTMLElement>('#nx-cg-email')?.focus();
});
}
function eventTargetElement(e: Event): Element | null {
const t = e.target;
if (t instanceof Element) return t;
if (t instanceof Text) return t.parentElement;
return null;
}
function bind(): void {
const root = overlayEl();
const form = document.getElementById('nx-chat-gate-form') as HTMLFormElement | null;
if (!root || !form) return;
document.querySelectorAll<HTMLElement>('[data-nx-chat-open]').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault();
openPanel();
});
});
root.querySelectorAll<HTMLButtonElement>('[data-nx-chat-gate-close]').forEach((b) => {
b.addEventListener('click', () => closePanel());
});
document.getElementById('nx-chat-gate-open-chat')?.addEventListener('click', () => {
openChat();
closePanel();
});
const onDocClickCapture = (e: MouseEvent) => {
if (!isOpen()) return;
const el = eventTargetElement(e);
if (!el || !root.contains(el)) return;
if (el.closest('[data-nx-chat-gate-close]')) {
e.preventDefault();
e.stopPropagation();
closePanel();
return;
}
if (!el.closest(PANEL_SEL)) {
e.preventDefault();
e.stopPropagation();
closePanel();
}
};
document.addEventListener('click', onDocClickCapture, true);
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key !== 'Escape' || !isOpen()) return;
e.preventDefault();
closePanel();
});
form.addEventListener('submit', async (e) => {
const cfg = readCfg();
const err = document.getElementById('nx-chat-gate-form-error');
if (err) err.classList.add('hidden');
if (cfg.actionUrl) {
e.preventDefault();
const fd = new FormData(form);
try {
const res = await fetch(cfg.actionUrl, {
method: 'POST',
headers: { Accept: 'application/json' },
body: fd,
});
const data = (await res.json().catch(() => null)) as { error?: string } | null;
if (!res.ok) {
const msg =
(data && typeof data.error === 'string' && data.error) ||
'Invio non riuscito. Riprova tra poco.';
if (err) {
err.textContent = msg;
err.classList.remove('hidden');
}
return;
}
if (cfg.openChatAfterSubmit) {
openChat();
closePanel();
return;
}
showSuccessStep();
document.getElementById('nx-chat-gate-open-chat')?.focus();
} catch {
if (err) {
err.textContent = 'Errore di rete. Controlla la connessione e riprova.';
err.classList.remove('hidden');
}
}
return;
}
e.preventDefault();
const fd = new FormData(form);
const name = String(fd.get(cfg.fieldName) ?? '').trim();
const email = String(fd.get(cfg.fieldEmail) ?? '').trim();
if (cfg.mailto) {
const subject = encodeURIComponent(cfg.notifySubject || 'Accesso chat');
const body = encodeURIComponent(
`Richiesta accesso alla chat.\n\nNome: ${name || '—'}\nEmail: ${email}\n`,
);
window.location.href = `mailto:${cfg.mailto}?subject=${subject}&body=${body}`;
closePanel();
return;
}
if (err) {
err.textContent =
'Configura `chat.prechat.actionUrl` (Formspree) o `chat.prechat.mailto` in `src/data/home/chat.ts`.';
err.classList.remove('hidden');
}
});
}
if (typeof document !== 'undefined') {
window.addEventListener('pageshow', (e) => {
if (e.persisted) closePanel();
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bind, { once: true });
} else {
bind();
}
}

View File

@ -0,0 +1,75 @@
/** Modulo contatti in pagina (#contatti): mailto o POST Formspree. */
interface ContactFormCfg {
actionUrl: string;
mailto: string;
notifySubject: string;
successRedirect: string;
fieldName: string;
fieldCompany: string;
fieldCountry: string;
fieldEmail: string;
fieldMessage: string;
}
function readCfg(): ContactFormCfg {
const el = document.getElementById('nx-contact-model');
const raw = el?.textContent?.trim();
if (!raw) {
return {
actionUrl: '',
mailto: '',
notifySubject: '',
successRedirect: '',
fieldName: 'name',
fieldCompany: 'company',
fieldCountry: 'country',
fieldEmail: 'email',
fieldMessage: 'message',
};
}
return JSON.parse(raw) as ContactFormCfg;
}
function bind(): void {
const form = document.getElementById('nx-contact-form') as HTMLFormElement | null;
if (!form) return;
form.addEventListener('submit', (e) => {
const cfg = readCfg();
const err = document.getElementById('nx-contact-form-error');
if (err) err.classList.add('hidden');
if (cfg.actionUrl) {
return;
}
e.preventDefault();
const fd = new FormData(form);
const name = String(fd.get(cfg.fieldName) ?? '').trim();
const company = String(fd.get(cfg.fieldCompany) ?? '').trim();
const email = String(fd.get(cfg.fieldEmail) ?? '').trim();
const country = String(fd.get(cfg.fieldCountry) ?? '').trim();
const message = String(fd.get(cfg.fieldMessage) ?? '').trim();
if (cfg.mailto) {
const subject = encodeURIComponent(cfg.notifySubject || 'Contatto sito');
const companyLine = company ? `\nAzienda: ${company}` : '';
const body = encodeURIComponent(
`Nome: ${name}${companyLine}\nEmail: ${email}\nPaese: ${country}\n\n${message}`,
);
window.location.href = `mailto:${cfg.mailto}?subject=${subject}&body=${body}`;
return;
}
if (err) {
err.textContent =
'Configura `contactForm.actionUrl` (Formspree) o `contactForm.mailto` in `src/data/home/cta.ts`.';
err.classList.remove('hidden');
}
});
}
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bind, { once: true });
} else {
bind();
}
}

View File

@ -0,0 +1,279 @@
type CategoryId = 'necessary' | 'preferences' | 'analytics' | 'marketing';
interface CcConfig {
storageKey: string;
policyVersion: string;
categories: readonly { id: CategoryId; required: boolean }[];
}
interface ConsentPayload {
policyVersion: string;
updatedAt: string;
categories: Record<CategoryId, boolean>;
}
const EVENT_NAME = 'nexstudio:cookie-consent';
const PANEL_ID = 'nx-cc-dialog';
/** Chrome può usare come `target` un nodo `#text` dentro al `<button>` */
function eventTargetElement(e: Event): Element | null {
const t = e.target;
if (t instanceof Element) return t;
if (t instanceof Text) return t.parentElement;
return null;
}
function panelRoot(): HTMLElement | null {
return document.getElementById(PANEL_ID);
}
function isPanelOpen(): boolean {
const el = panelRoot();
return Boolean(el && !el.hasAttribute('hidden'));
}
function closeConsentPanel(): void {
const el = panelRoot();
if (!el) return;
el.setAttribute('hidden', '');
el.setAttribute('aria-hidden', 'true');
document.body.classList.remove('overflow-hidden');
}
function openConsentPanel(): void {
const el = panelRoot();
if (!el) return;
el.removeAttribute('hidden');
el.setAttribute('aria-hidden', 'false');
document.body.classList.add('overflow-hidden');
}
function defaultCategories(cfg: CcConfig): Record<CategoryId, boolean> {
const out = {} as Record<CategoryId, boolean>;
for (const c of cfg.categories) {
out[c.id] = c.required ? true : false;
}
return out;
}
function parsePayload(raw: string | null, cfg: CcConfig): ConsentPayload | null {
if (!raw) return null;
try {
const v = JSON.parse(raw) as Partial<ConsentPayload>;
if (!v || typeof v !== 'object') return null;
if (v.policyVersion !== cfg.policyVersion) return null;
if (typeof v.updatedAt !== 'string') return null;
if (!v.categories || typeof v.categories !== 'object') return null;
const cats = { ...defaultCategories(cfg) };
for (const c of cfg.categories) {
if (typeof v.categories[c.id] === 'boolean') {
cats[c.id] = c.required ? true : Boolean(v.categories[c.id]);
}
}
return { policyVersion: cfg.policyVersion, updatedAt: v.updatedAt, categories: cats };
} catch {
return null;
}
}
function readConfig(): CcConfig {
const el = document.getElementById('nx-cc-model');
const raw = el?.textContent?.trim();
if (!raw) {
return {
storageKey: 'nexstudio.cookieConsent',
policyVersion: '1.0',
categories: [
{ id: 'necessary', required: true },
{ id: 'preferences', required: false },
{ id: 'analytics', required: false },
{ id: 'marketing', required: false },
],
};
}
return JSON.parse(raw) as CcConfig;
}
function save(cfg: CcConfig, categories: Record<CategoryId, boolean>): ConsentPayload {
const payload: ConsentPayload = {
policyVersion: cfg.policyVersion,
updatedAt: new Date().toISOString(),
categories: { ...categories },
};
for (const c of cfg.categories) {
if (c.required) payload.categories[c.id] = true;
}
try {
localStorage.setItem(cfg.storageKey, JSON.stringify(payload));
} catch {
/* ignore quota / private mode */
}
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: payload }));
return payload;
}
function getStored(cfg: CcConfig): ConsentPayload | null {
try {
return parsePayload(localStorage.getItem(cfg.storageKey), cfg);
} catch {
return null;
}
}
let nxCcDocCapture: ((e: MouseEvent) => void) | undefined;
function bind(): void {
const cfg = readConfig();
const banner = document.getElementById('nx-cc-banner');
const root = panelRoot();
if (!banner || !root) return;
closeConsentPanel();
function readForm(): Record<CategoryId, boolean> {
const d = panelRoot();
const base = defaultCategories(cfg);
base.preferences = Boolean(d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="preferences"]')?.checked);
base.analytics = Boolean(d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="analytics"]')?.checked);
base.marketing = Boolean(d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="marketing"]')?.checked);
return base;
}
function writeForm(cats: Record<CategoryId, boolean>) {
const d = panelRoot();
const p = d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="preferences"]');
const a = d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="analytics"]');
const m = d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="marketing"]');
if (p) p.checked = cats.preferences;
if (a) a.checked = cats.analytics;
if (m) m.checked = cats.marketing;
}
function hideBanner() {
banner.setAttribute('hidden', '');
banner.setAttribute('aria-hidden', 'true');
}
function showBanner() {
banner.removeAttribute('hidden');
banner.setAttribute('aria-hidden', 'false');
}
function applyFromStorage() {
const stored = getStored(cfg);
if (stored) {
writeForm(stored.categories);
hideBanner();
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: stored }));
} else {
writeForm(defaultCategories(cfg));
showBanner();
}
closeConsentPanel();
}
function persistAndClose(cats: Record<CategoryId, boolean>) {
save(cfg, cats);
hideBanner();
closeConsentPanel();
}
banner.querySelector('[data-nx-cc-reject]')?.addEventListener('click', () => {
persistAndClose(defaultCategories(cfg));
});
banner.querySelector('[data-nx-cc-accept]')?.addEventListener('click', () => {
const cats = defaultCategories(cfg);
for (const c of cfg.categories) {
if (!c.required) cats[c.id] = true;
}
persistAndClose(cats);
});
banner.querySelector('[data-nx-cc-customize]')?.addEventListener('click', () => {
const stored = getStored(cfg);
writeForm(stored?.categories ?? defaultCategories(cfg));
openConsentPanel();
});
const onDocClickCapture = (e: MouseEvent) => {
if (!isPanelOpen()) return;
const d = panelRoot();
if (!d) return;
const el = eventTargetElement(e);
if (!el || !d.contains(el)) return;
if (el.closest('[data-nx-cc-save]')) {
e.preventDefault();
e.stopPropagation();
persistAndClose(readForm());
return;
}
if (el.closest('[data-nx-cc-cancel]')) {
e.preventDefault();
e.stopPropagation();
closeConsentPanel();
return;
}
if (!el.closest('[data-nx-cc-panel]')) {
e.preventDefault();
e.stopPropagation();
closeConsentPanel();
}
};
if (nxCcDocCapture) {
document.removeEventListener('click', nxCcDocCapture, true);
}
nxCcDocCapture = onDocClickCapture;
document.addEventListener('click', nxCcDocCapture, true);
document.querySelectorAll<HTMLElement>('[data-nx-cc-open]').forEach((el) => {
el.addEventListener('click', (e) => {
e.preventDefault();
const stored = getStored(cfg);
writeForm(stored?.categories ?? defaultCategories(cfg));
openConsentPanel();
});
});
applyFromStorage();
(window as unknown as { NexStudioCookieConsent?: unknown }).NexStudioCookieConsent = {
getConsent: () => getStored(cfg),
openPreferences: () => {
const stored = getStored(cfg);
writeForm(stored?.categories ?? defaultCategories(cfg));
openConsentPanel();
},
acceptAll: () => {
const cats = defaultCategories(cfg);
for (const c of cfg.categories) {
if (!c.required) cats[c.id] = true;
}
persistAndClose(cats);
},
rejectOptional: () => {
persistAndClose(defaultCategories(cfg));
},
};
}
if (typeof document !== 'undefined') {
window.addEventListener('pageshow', (e) => {
if (e.persisted) closeConsentPanel();
});
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
if (!isPanelOpen()) return;
e.preventDefault();
closeConsentPanel();
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bind, { once: true });
} else {
bind();
}
}

56
src/scripts/nx-nav-spy.ts Normal file
View File

@ -0,0 +1,56 @@
/**
* Home: evidenzia in bianco il link del menu che corrisponde alla sezione in vista.
*/
const SECTION_IDS = ['prodotti', 'servizi', 'stack', 'faq', 'contatti'] as const;
const HEADER_OFFSET = 88;
function getActiveSectionId(): string | null {
const y = window.scrollY + HEADER_OFFSET;
let active: string | null = null;
for (const id of SECTION_IDS) {
const el = document.getElementById(id);
if (!el) continue;
if (el.offsetTop <= y) active = id;
}
return active;
}
function applyActive(): void {
const active = getActiveSectionId();
document.querySelectorAll<HTMLAnchorElement>('a[data-section]').forEach((a) => {
const id = a.dataset.section;
const on = Boolean(id && id === active);
a.dataset.navActive = on ? 'true' : 'false';
if (on) a.setAttribute('aria-current', 'true');
else a.removeAttribute('aria-current');
});
}
let scheduled = false;
function onScroll(): void {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
applyActive();
});
}
function bind(): void {
const path = window.location.pathname;
if (path !== '/' && path !== '/index.html') return;
applyActive();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll, { passive: true });
window.addEventListener('hashchange', applyActive);
window.addEventListener('load', applyActive);
}
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bind, { once: true });
} else {
bind();
}
}