5 Commits

Author SHA1 Message Date
Adriano 59dff13e22 Consulenza: bottone nel menu in alto su ogni pagina, voce nel menu mobile (#22)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 05:19:40 +00:00
Adriano 5c39b04c31 Consulenza: la nota sta sotto il suo bottone, non sotto Contattaci (#22)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 20:39:15 +00:00
Adriano ae98c4b889 Consulenza: nota sotto i bottoni con lo stile giusto (:global sul componente T) (#22)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 20:32:54 +00:00
Adriano c65effb0ed Consulenza: sotto il bottone la sede, via Leandro Alberti 76 (#22)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 20:32:07 +00:00
Adriano 67dc554810 Reformer Class: bottone consulenza conoscitiva gratuita (#22)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 20:30:09 +00:00
5 changed files with 83 additions and 2 deletions
+14
View File
@@ -2,6 +2,7 @@
import { site } from '../data/site'; import { site } from '../data/site';
import { t, contentImageUrl } from '../lib/content'; import { t, contentImageUrl } from '../lib/content';
import { landingFor } from '../lib/auth'; import { landingFor } from '../lib/auth';
import { linkConsulenza } from '../data/consulenza';
interface Props { interface Props {
user: { username: string; role: string } | null; user: { username: string; role: string } | null;
showTags: boolean; showTags: boolean;
@@ -17,6 +18,8 @@ const path = Astro.url.pathname;
// si cambia una riga — ed e' l'unico posto del sito che nomina l'area riservata. // si cambia una riga — ed e' l'unico posto del sito che nomina l'area riservata.
const AREA_RISERVATA = 'https://piattaforme.tielogic.xyz'; const AREA_RISERVATA = 'https://piattaforme.tielogic.xyz';
const areaHref = user ? landingFor(user.role) : '/admin'; const areaHref = user ? landingFor(user.role) : '/admin';
// Consulenza con Nicola su ogni pagina, non solo sulla Reformer Class (Adriano 30/09, #22).
const consulenzaHref = linkConsulenza(t('global.consulenza.url'));
--- ---
<header class="hdr" id="site-header"> <header class="hdr" id="site-header">
<div class="container hdr__in"> <div class="container hdr__in">
@@ -27,7 +30,9 @@ const areaHref = user ? landingFor(user.role) : '/admin';
<a href={item.href} class:list={['hdr__link', { 'is-active': path === item.href || (item.href !== '/' && path.startsWith(item.href)) }]}>{t(`global.nav.${i + 1}.label`)}</a> <a href={item.href} class:list={['hdr__link', { 'is-active': path === item.href || (item.href !== '/' && path.startsWith(item.href)) }]}>{t(`global.nav.${i + 1}.label`)}</a>
) )
))} ))}
<a class="hdr__link hdr__consulenza-mob" href={consulenzaHref} target="_blank" rel="noopener noreferrer">{t('global.consulenza.cta-breve')}</a>
</nav> </nav>
<a class="hdr__consulenza" href={consulenzaHref} target="_blank" rel="noopener noreferrer">{t('global.consulenza.cta-breve')}</a>
<div class="hdr__user"> <div class="hdr__user">
{user ? ( {user ? (
<details class="umenu"> <details class="umenu">
@@ -55,6 +60,8 @@ const areaHref = user ? landingFor(user.role) : '/admin';
.hdr.is-scrolled { box-shadow: 0 2px 14px rgba(0,0,0,.08); } .hdr.is-scrolled { box-shadow: 0 2px 14px rgba(0,0,0,.08); }
.hdr__in { display: flex; align-items: center; justify-content: space-between; height: var(--header-h); } .hdr__in { display: flex; align-items: center; justify-content: space-between; height: var(--header-h); }
.hdr__nav { display: flex; gap: 34px; } .hdr__nav { display: flex; gap: 34px; }
/* Col bottone Consulenza nella barra le voci non devono andare a capo (#22). */
@media (min-width: 992px) { .hdr__nav { gap: 22px; } .hdr__link, .hdr__login { white-space: nowrap; } .hdr__logo { flex-shrink: 0; margin-right: 28px; } }
.hdr__link { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); transition: color .3s ease; } .hdr__link { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); transition: color .3s ease; }
.hdr__link:hover, .hdr__link.is-active { color: var(--c-accent-dark); } .hdr__link:hover, .hdr__link.is-active { color: var(--c-accent-dark); }
.hdr__link--bio { color: var(--c-accent-dark); } .hdr__link--bio { color: var(--c-accent-dark); }
@@ -79,6 +86,13 @@ const areaHref = user ? landingFor(user.role) : '/admin';
.plat__panel { position: static; box-shadow: none; padding: 0 0 8px; min-width: 0; } .plat__panel { position: static; box-shadow: none; padding: 0 0 8px; min-width: 0; }
.plat__item { padding: 10px 0 10px 14px; } .plat__item { padding: 10px 0 10px 14px; }
} }
.hdr__consulenza { font-family: var(--font-heading); font-size: .68rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; text-decoration: none; color: #fff; background: #6b5d5b; padding: 10px 14px; margin: 0 18px; white-space: nowrap; transition: background .2s; }
.hdr__consulenza:hover { background: var(--c-accent-dark); }
.hdr__consulenza-mob { display: none; }
@media (max-width: 991px) {
.hdr__consulenza { display: none; }
.hdr__consulenza-mob { display: block; color: var(--c-accent-dark); }
}
.hdr__user { display: flex; align-items: center; } .hdr__user { display: flex; align-items: center; }
.hdr__login { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); } .hdr__login { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); }
.hdr__login:hover { color: var(--c-accent-dark); } .hdr__login:hover { color: var(--c-accent-dark); }
+15
View File
@@ -0,0 +1,15 @@
// Consulenza conoscitiva gratuita con Nicola (prenotazione su Google Calendar).
//
// Il link vive nel pannello contenuti (tag `global.consulenza.url`): qui c'è il valore di
// partenza e la guardia. Il tag è modificabile da admin, quindi si accetta solo https.
export const CONSULENZA_URL_DEFAULT = 'https://calendar.app.google/nWXMGD2S5g2pVeeD7';
/** Slug delle pagine servizio che mostrano il bottone di prenotazione. */
export const PAGINE_CON_CONSULENZA = ['reformer-class'];
/** Link da usare: il valore del pannello se è https, altrimenti quello di default
* (niente javascript:, http:, vuoto). */
export function linkConsulenza(valore: string): string {
const v = (valore ?? '').trim();
return v.startsWith('https://') ? v : CONSULENZA_URL_DEFAULT;
}
+5
View File
@@ -7,6 +7,7 @@ import { partners } from './partners';
import { disciplines, days, slots } from './agenda'; import { disciplines, days, slots } from './agenda';
import { services } from './services'; import { services } from './services';
import { performanceClassPricing } from './pricing'; import { performanceClassPricing } from './pricing';
import { CONSULENZA_URL_DEFAULT } from './consulenza';
export type ContentType = 'text' | 'html' | 'image'; export type ContentType = 'text' | 'html' | 'image';
export interface SeedEntry { tag: string; type: ContentType; value: string; styleOptions?: StyleGroup[] } export interface SeedEntry { tag: string; type: ContentType; value: string; styleOptions?: StyleGroup[] }
@@ -270,6 +271,10 @@ export const contentSeed: SeedEntry[] = [
T('service.pricing.entries-plural', 'ingressi settimanali', []), T('service.pricing.entries-plural', 'ingressi settimanali', []),
T('service.pricing.buy-cta', 'Acquista', []), T('service.pricing.buy-cta', 'Acquista', []),
T('service.contact-cta', 'Contattaci', []), T('service.contact-cta', 'Contattaci', []),
T('service.consulenza-cta', 'Prenota una consulenza conoscitiva gratuita', []),
T('service.consulenza-note', '30-45 minuti con Nicola, in sede: via Leandro Alberti 76, primo piano', []),
T('global.consulenza.url', CONSULENZA_URL_DEFAULT, []),
T('global.consulenza.cta-breve', 'Consulenza gratuita', []),
// --- Pricing performance class (data-driven da pricing.ts; monthly/entries restano nel data file) --- // --- Pricing performance class (data-driven da pricing.ts; monthly/entries restano nel data file) ---
T('pricing.heading', performanceClassPricing.heading), T('pricing.heading', performanceClassPricing.heading),
+21 -2
View File
@@ -10,6 +10,7 @@ import TImg from '../../components/content/TImg.astro';
import CalendarioModal from '../../components/CalendarioModal.astro'; import CalendarioModal from '../../components/CalendarioModal.astro';
import { calendariDaMostrare, PAGINE_CON_CALENDARIO } from '../../data/calendari'; import { calendariDaMostrare, PAGINE_CON_CALENDARIO } from '../../data/calendari';
import { t } from '../../lib/content'; import { t } from '../../lib/content';
import { PAGINE_CON_CONSULENZA, linkConsulenza } from '../../data/consulenza';
export const prerender = false; export const prerender = false;
const service = getService(Astro.params.slug!); const service = getService(Astro.params.slug!);
@@ -38,8 +39,18 @@ const tones = ['tan', 'taupe', 'dark'];
<div> <div>
<h2>{t(`service.${slug}.title`).toUpperCase()}</h2> <h2>{t(`service.${slug}.title`).toUpperCase()}</h2>
<T tag={`service.${slug}.long-description`} as="p" class="prog__desc" /> <T tag={`service.${slug}.long-description`} as="p" class="prog__desc" />
{slug === 'reformer-class' && ( {(slug === 'reformer-class' || PAGINE_CON_CONSULENZA.includes(slug)) && (
<p class="prog__cta"><a class="btn" href="/contact">{t('service.contact-cta')}</a></p> <div class="prog__cta">
<div class="prog__cta-row">
{slug === 'reformer-class' && <a class="btn" href="/contact">{t('service.contact-cta')}</a>}
{PAGINE_CON_CONSULENZA.includes(slug) && (
<div class="prog__cta-consulenza">
<a class="btn btn--consulenza" href={linkConsulenza(t('global.consulenza.url'))} target="_blank" rel="noopener noreferrer">{t('service.consulenza-cta')}</a>
<T tag="service.consulenza-note" as="p" class="prog__cta-note" />
</div>
)}
</div>
</div>
)} )}
{service.features.length > 0 && ( {service.features.length > 0 && (
<ul class="prog__features"> <ul class="prog__features">
@@ -149,6 +160,14 @@ const tones = ['tan', 'taupe', 'dark'];
.prog__calendario { display: flex; align-items: center; gap: 14px; width: 100%; margin-top: 36px; padding: 10px 16px 10px 10px; border: 0; cursor: pointer; text-align: left; background: #6b5d5b; color: #fff; font-family: var(--font-heading); font-size: .82rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; transition: background .2s; } .prog__calendario { display: flex; align-items: center; gap: 14px; width: 100%; margin-top: 36px; padding: 10px 16px 10px 10px; border: 0; cursor: pointer; text-align: left; background: #6b5d5b; color: #fff; font-family: var(--font-heading); font-size: .82rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; transition: background .2s; }
.prog__calendario:hover { background: var(--c-accent-dark); } .prog__calendario:hover { background: var(--c-accent-dark); }
.prog__cta { margin: 10px 0 30px; } .prog__cta { margin: 10px 0 30px; }
.prog__cta-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px; }
/* La nota (durata, sede) sta sotto il suo bottone, non sotto Contattaci (Adriano 29/09). */
.prog__cta-consulenza { display: flex; flex-direction: column; }
.btn--consulenza { background: #6b5d5b; }
.btn--consulenza:hover { background: var(--c-accent-dark); }
/* Il paragrafo lo stampa il componente T: senza :global lo stile scoped della pagina non lo raggiunge. */
.prog__cta :global(.prog__cta-note) { font-size: .8rem; color: var(--c-text-light); margin: 8px 0 0; }
@media (max-width: 600px) { .prog__cta-row > .btn, .prog__cta-consulenza { flex: 1 1 100%; } .prog__cta-row .btn { justify-content: center; text-align: center; } }
.prog__icon { display: inline-flex; align-items: center; justify-content: center; width: 48px; height: 48px; background: var(--c-accent); color: #fff; flex-shrink: 0; } .prog__icon { display: inline-flex; align-items: center; justify-content: center; width: 48px; height: 48px; background: var(--c-accent); color: #fff; flex-shrink: 0; }
.price__group { border-top: 1px solid #ddd5ca; padding-block: 18px; } .price__group { border-top: 1px solid #ddd5ca; padding-block: 18px; }
.price__group summary { cursor: pointer; font-family: var(--font-heading); letter-spacing: .12em; text-transform: uppercase; font-size: .85rem; color: var(--c-heading); } .price__group summary { cursor: pointer; font-family: var(--font-heading); letter-spacing: .12em; text-transform: uppercase; font-size: .85rem; color: var(--c-heading); }
+28
View File
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import { linkConsulenza, CONSULENZA_URL_DEFAULT, PAGINE_CON_CONSULENZA } from '../src/data/consulenza';
import { contentSeed } from '../src/data/content-seed';
import { services } from '../src/data/services';
describe('consulenza conoscitiva', () => {
it('accetta un link https, anche con spazi attorno', () => {
expect(linkConsulenza('https://example.com/x')).toBe('https://example.com/x');
expect(linkConsulenza(' https://example.com/x ')).toBe('https://example.com/x');
});
it.each(['javascript:alert(1)', 'http://x.it', '', ' '])('scarta %j e usa il default', (v) => {
expect(linkConsulenza(v)).toBe(CONSULENZA_URL_DEFAULT);
});
it('il seed ha i tre tag', () => {
const v = (tag: string) => contentSeed.find((e) => e.tag === tag)?.value;
expect(v('service.consulenza-cta')).toBe('Prenota una consulenza conoscitiva gratuita');
expect(v('service.consulenza-note')).toBe('30-45 minuti con Nicola, in sede: via Leandro Alberti 76, primo piano');
expect(v('global.consulenza.url')).toBe(CONSULENZA_URL_DEFAULT);
});
it('le pagine con consulenza esistono', () => {
expect(PAGINE_CON_CONSULENZA).toContain('reformer-class');
for (const slug of PAGINE_CON_CONSULENZA) {
const s = services.find((x) => x.slug === slug);
expect(s, slug).toBeDefined();
expect(s!.page).not.toBe(false);
}
});
});