Merge issue-52-content-C (Refs #52)
This commit is contained in:
commit
07eba1ba69
6 files changed
+492
-7
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import type { TemplateComponent } from '../../templates/definitions';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
|
||||
/** Small tree builders shared by the contact/hours/gallery/local families.
|
||||
* Everything is token-based and uses padding longhands (single padding control). */
|
||||
|
||||
export const sectionPad = (top = '72px', side = '20px', bg = tokenRef('bg')) => ({
|
||||
paddingTop: top, paddingRight: side, paddingBottom: top, paddingLeft: side, backgroundColor: bg,
|
||||
});
|
||||
|
||||
export const heading = (text: string, level = 'h2', align: 'center' | 'left' = 'center', color = tokenRef('text')): TemplateComponent => ({
|
||||
type: 'Heading',
|
||||
props: { text, level, style: { textAlign: align, color, fontFamily: tokenRef('heading-font'), marginBottom: '12px' } },
|
||||
});
|
||||
|
||||
export const para = (text: string, align: 'center' | 'left' = 'center', color = tokenRef('muted'), extra: Record<string, string> = {}): TemplateComponent => ({
|
||||
type: 'TextBlock',
|
||||
props: { text, style: { textAlign: align, color, lineHeight: '1.7', marginBottom: '20px', ...extra } },
|
||||
});
|
||||
|
||||
export const button = (text: string, href: string, solid = true): TemplateComponent => ({
|
||||
type: 'ButtonLink',
|
||||
props: {
|
||||
text, href,
|
||||
style: solid
|
||||
? { backgroundColor: tokenRef('primary'), color: tokenRef('bg'), padding: '12px 28px', borderRadius: '8px', fontWeight: '600', fontSize: '16px', border: 'none' }
|
||||
: { backgroundColor: 'transparent', color: tokenRef('primary'), padding: '12px 28px', borderRadius: '8px', fontWeight: '600', fontSize: '16px', border: `2px solid ${tokenRef('primary')}` },
|
||||
},
|
||||
});
|
||||
|
||||
/** A ColumnLayout column: each child becomes a col-N linked node, so it is a Container. */
|
||||
export const column = (...children: TemplateComponent[]): TemplateComponent => ({
|
||||
type: 'Container',
|
||||
props: { style: { padding: '0px', minHeight: '' } },
|
||||
children,
|
||||
});
|
||||
|
||||
export const columns = (split: string, gap: string, ...cols: TemplateComponent[]): TemplateComponent => ({
|
||||
type: 'ColumnLayout',
|
||||
props: { columns: cols.length, split, gap, style: { alignItems: 'center' } },
|
||||
children: cols,
|
||||
});
|
||||
|
||||
export const contactForm = (): TemplateComponent => ({
|
||||
type: 'ContactForm',
|
||||
props: {
|
||||
style: { backgroundColor: tokenRef('bg'), borderRadius: '12px', border: `1px solid ${tokenRef('border')}` },
|
||||
labelColor: tokenRef('text'), inputBg: tokenRef('bg'), inputBorder: tokenRef('border'),
|
||||
},
|
||||
});
|
||||
|
||||
export const hoursBlock = (title = 'Opening Hours', note = ''): TemplateComponent => ({
|
||||
type: 'OpeningHours',
|
||||
props: { title, note, style: { backgroundColor: 'transparent', paddingTop: '0px', paddingBottom: '0px', paddingLeft: '0px', paddingRight: '0px' } },
|
||||
});
|
||||
|
||||
export const map = (height = '360px'): TemplateComponent => ({
|
||||
type: 'MapEmbed',
|
||||
props: { address: 'Your Business, Main Street', zoom: 15, height },
|
||||
});
|
||||
|
||||
export const img = (alt: string, aspect = '4 / 3', radius = '12px'): TemplateComponent => ({
|
||||
type: 'ImageBlock',
|
||||
props: { alt, style: { width: '100%', height: 'auto', aspectRatio: aspect, objectFit: 'cover', borderRadius: radius } },
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, test, expect, afterEach } from 'vitest';
|
||||
import { renderEditorHarness, EditorHarness } from '../../test-utils/editorHarness';
|
||||
import { exportBodyHtml } from '../../utils/html-export';
|
||||
import { insertSection, type SectionDef } from '../catalog';
|
||||
import { CONTACT_SECTIONS } from './contact';
|
||||
import { HOURS_SECTIONS } from './hours';
|
||||
import { GALLERY_SECTIONS } from './gallery';
|
||||
import { LOCAL_SECTIONS } from './local';
|
||||
import type { TemplateComponent } from '../../templates/definitions';
|
||||
|
||||
let h: EditorHarness;
|
||||
afterEach(() => h?.unmount());
|
||||
|
||||
const ALL: SectionDef[] = [...CONTACT_SECTIONS, ...HOURS_SECTIONS, ...GALLERY_SECTIONS, ...LOCAL_SECTIONS];
|
||||
const TOP_LEVEL = new Set(['Section', 'HeroSimple', 'CTASection', 'CallToAction']);
|
||||
|
||||
/** Drop everything that is allowed to contain hex: linear-gradient(...) and data:image/svg URIs. */
|
||||
export function stripAllowedHex(html: string): string {
|
||||
return html
|
||||
.replace(/linear-gradient\([^)]*\)/g, '')
|
||||
.replace(/data:image\/svg[^"')\s]*/g, '')
|
||||
// ContactForm's required-field marker hardcodes a red in the component's own toHtml.
|
||||
.replace(/<span style="color:#ef4444;margin-left:2px">\*<\/span>/g, '')
|
||||
// ...and its submit button text (#ffffff, no prop) and Gallery's <img> placeholder background (#f1f5f9, no prop).
|
||||
.replace(/(<button type="submit"[^>]*?)color:#ffffff;/g, '$1')
|
||||
.replace(/(<img [^>]*?);background-color:#f1f5f9/g, '$1');
|
||||
}
|
||||
|
||||
function walk(t: TemplateComponent, fn: (n: TemplateComponent) => void) {
|
||||
fn(t);
|
||||
(t.children || []).forEach((c) => walk(c, fn));
|
||||
}
|
||||
|
||||
function exported(def: SectionDef): string {
|
||||
h = renderEditorHarness();
|
||||
h.act(() => { insertSection(h.query, h.actions, def); });
|
||||
const html = exportBodyHtml(h.getSerialized()).html;
|
||||
h.unmount();
|
||||
return html;
|
||||
}
|
||||
|
||||
describe('content C section families', () => {
|
||||
test('about 16 sections with 3+ layouts per family', () => {
|
||||
expect(ALL.length).toBeGreaterThanOrEqual(15);
|
||||
for (const fam of [CONTACT_SECTIONS, HOURS_SECTIONS, GALLERY_SECTIONS]) expect(fam.length).toBeGreaterThanOrEqual(3);
|
||||
expect(LOCAL_SECTIONS.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
test('ids unique across files, prefixed, and keywords >= 3', () => {
|
||||
expect(new Set(ALL.map((s) => s.id)).size).toBe(ALL.length);
|
||||
for (const s of ALL) expect(s.keywords.length).toBeGreaterThanOrEqual(3);
|
||||
for (const s of [...CONTACT_SECTIONS, ...HOURS_SECTIONS, ...GALLERY_SECTIONS]) expect(s.id.startsWith(`${s.family}-`)).toBe(true);
|
||||
for (const s of LOCAL_SECTIONS) expect(s.id.startsWith('local-')).toBe(true);
|
||||
});
|
||||
|
||||
test('stripAllowedHex really detects stray hex (guard against a vacuous check)', () => {
|
||||
expect(stripAllowedHex('<p style="color:#abcdef">x</p>')).toMatch(/#[0-9a-f]{6}/i);
|
||||
expect(stripAllowedHex('<i style="background:linear-gradient(#abcdef,#123456)"></i>')).not.toMatch(/#[0-9a-f]{6}/i);
|
||||
});
|
||||
|
||||
for (const def of ALL) {
|
||||
describe(def.id, () => {
|
||||
test('root is a top-level block with empty anchorId', () => {
|
||||
expect(TOP_LEVEL.has(def.tree.type)).toBe(true);
|
||||
expect((def.tree.props as any).anchorId ?? '').toBe('');
|
||||
});
|
||||
|
||||
test('inserts via the real editor and exports without hex colours', () => {
|
||||
const html = exported(def);
|
||||
expect(html.length).toBeGreaterThan(0);
|
||||
expect(html).not.toContain('Export failed');
|
||||
expect(stripAllowedHex(html)).not.toMatch(/#[0-9a-f]{6}/i);
|
||||
expect(JSON.stringify(def.tree)).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
|
||||
});
|
||||
|
||||
test('no animation props; ImageBlocks/gallery images have alt; links are # or page-relative (tel ok)', () => {
|
||||
walk(def.tree, (n) => {
|
||||
expect((n.props as any).animation ?? '').toBe('');
|
||||
if (n.type === 'ImageBlock') expect(String((n.props as any).alt || '').length).toBeGreaterThan(0);
|
||||
if (n.type === 'Gallery') for (const im of (n.props as any).images || []) expect(String(im.alt || '').length).toBeGreaterThan(0);
|
||||
const href = (n.props as any).href;
|
||||
if (typeof href === 'string') expect(href === '#' || /^(tel:\+?\d+|[a-z0-9][a-z0-9\-_./#]*)$/i.test(href) && !/^[a-z]+:\/\//i.test(href)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('exported <a> links are # or page-relative or tel:', () => {
|
||||
const html = exported(def);
|
||||
for (const m of html.matchAll(/<a [^>]*href="([^"]*)"/g)) {
|
||||
const href = m[1];
|
||||
// StickyCallBar's directions link is the one deliberate absolute https link.
|
||||
if (def.tree.children?.some((c) => c.type === 'StickyCallBar') && /^https:\/\/maps\.google\.com\//.test(href)) continue;
|
||||
expect(href === '#' || href.startsWith('tel:') || !/^[a-z][a-z0-9+.-]*:/i.test(href)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('every <img> in the export has alt', () => {
|
||||
const html = exported(def);
|
||||
for (const m of html.matchAll(/<img\b[^>]*>/g)) expect(m[0]).toMatch(/\balt="[^"]+"/);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('ColumnLayout columns match the declared column count', () => {
|
||||
for (const def of ALL) walk(def.tree, (n) => {
|
||||
if (n.type === 'ColumnLayout') expect(n.children?.length).toBe((n.props as any).columns);
|
||||
});
|
||||
});
|
||||
|
||||
test('array props carry full item[0] key sets', () => {
|
||||
for (const def of ALL) walk(def.tree, (n) => {
|
||||
if (n.type === 'Gallery') for (const im of (n.props as any).images) expect(Object.keys(im).sort()).toEqual(['alt', 'caption', 'src']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { SectionDef } from '../catalog';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
import { sectionPad, heading, para, button, column, columns, contactForm, hoursBlock, map } from './_shared-c';
|
||||
|
||||
/** Contact sections. Content tasks append to CONTACT_SECTIONS only. */
|
||||
export const CONTACT_SECTIONS: SectionDef[] = [
|
||||
@@ -14,7 +15,90 @@ export const CONTACT_SECTIONS: SectionDef[] = [
|
||||
children: [
|
||||
{ type: 'Heading', props: { text: 'Get in Touch', level: 'h2', style: { textAlign: 'center', color: tokenRef('text'), fontFamily: tokenRef('heading-font'), marginBottom: '12px' } } },
|
||||
{ type: 'TextBlock', props: { text: 'Send us a message and we will reply as soon as we can.', style: { textAlign: 'center', color: tokenRef('muted'), marginBottom: '24px' } } },
|
||||
{ type: 'ContactForm', props: {} },
|
||||
contactForm(),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'contact-form-map',
|
||||
name: 'Contact - form with map',
|
||||
family: 'contact',
|
||||
keywords: ['form', 'map', 'location', 'visit', 'directions'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Contact', anchorId: '', style: sectionPad() },
|
||||
children: [
|
||||
heading('Contact Us'),
|
||||
para('Drop us a line or come and see us. We are happy to help.'),
|
||||
columns('50-50', '32px', column(contactForm()), column(map('420px'))),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'contact-form-hours',
|
||||
name: 'Contact - form with hours',
|
||||
family: 'contact',
|
||||
keywords: ['form', 'hours', 'opening', 'enquiry', 'availability'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Contact', anchorId: '', style: sectionPad('72px', '20px', tokenRef('bg')) },
|
||||
children: [
|
||||
heading('Send Us a Message'),
|
||||
para('We usually reply within one working day.'),
|
||||
columns('60-40', '40px', column(contactForm()), column(hoursBlock('When to reach us', 'Closed on public holidays.'))),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'contact-info-columns',
|
||||
name: 'Contact - details in three columns',
|
||||
family: 'contact',
|
||||
keywords: ['address', 'phone', 'email', 'details', 'columns'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Contact', anchorId: '', style: sectionPad() },
|
||||
children: [
|
||||
heading('Find Us'),
|
||||
para('Three easy ways to get in touch.'),
|
||||
columns('equal', '24px',
|
||||
column(heading('Visit', 'h3'), para('123 Main Street, Your Town')),
|
||||
column(heading('Call', 'h3'), para('(555) 123-4567')),
|
||||
column(heading('Email', 'h3'), para('hello@yourbusiness.example')),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'contact-cta-strip',
|
||||
name: 'Contact - call and email strip',
|
||||
family: 'contact',
|
||||
keywords: ['cta', 'strip', 'call', 'email', 'banner'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Contact strip', anchorId: '', style: sectionPad('56px', '20px', tokenRef('primary')) },
|
||||
children: [
|
||||
heading('Ready when you are', 'h2', 'center', tokenRef('bg')),
|
||||
para('Call us today or send a quick message.', 'center', tokenRef('bg')),
|
||||
{
|
||||
type: 'ButtonLink',
|
||||
props: { text: 'Contact us', href: 'contact', style: { backgroundColor: tokenRef('bg'), color: tokenRef('primary'), padding: '12px 28px', borderRadius: '8px', fontWeight: '600', fontSize: '16px', border: 'none' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'contact-book-appointment',
|
||||
name: 'Contact - book an appointment',
|
||||
family: 'contact',
|
||||
keywords: ['booking', 'appointment', 'schedule', 'calendar', 'reserve'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Booking', anchorId: '', style: sectionPad() },
|
||||
children: [
|
||||
heading('Book an Appointment'),
|
||||
para('Pick a time that suits you. Paste your booking link into the embed below.'),
|
||||
{ type: 'BookingEmbed', props: { provider: 'calendly', url: '', height: '700px', title: 'Book an appointment' } },
|
||||
{ type: 'Container', props: { style: { padding: '0px', minHeight: '', textAlign: 'center' } }, children: [button('Prefer to ask first? Contact us', 'contact', false)] },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,80 @@
|
||||
import type { SectionDef } from '../catalog';
|
||||
import { PLACEHOLDER_SRC } from '../../components/media/ImageBlock';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
import { sectionPad, heading, para, button, column, columns, img } from './_shared-c';
|
||||
|
||||
/** Content task appends to GALLERY_SECTIONS only (see hero.ts for the shape). */
|
||||
export const GALLERY_SECTIONS: SectionDef[] = [];
|
||||
const photos = (n: number) =>
|
||||
Array.from({ length: n }, (_, i) => ({ src: PLACEHOLDER_SRC, alt: `Photo ${i + 1} of our work`, caption: '' }));
|
||||
|
||||
const galleryStyle = { backgroundColor: 'transparent', paddingTop: '0px', paddingRight: '0px', paddingBottom: '0px', paddingLeft: '0px' };
|
||||
|
||||
/** Gallery sections. */
|
||||
export const GALLERY_SECTIONS: SectionDef[] = [
|
||||
{
|
||||
id: 'gallery-grid-3',
|
||||
name: 'Gallery - three-column grid',
|
||||
family: 'gallery',
|
||||
keywords: ['gallery', 'photos', 'grid', 'portfolio', 'images'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Gallery', anchorId: '', style: sectionPad() },
|
||||
children: [
|
||||
heading('Our Work'),
|
||||
para('A few recent projects and moments.'),
|
||||
{ type: 'Gallery', props: { images: photos(6), columns: 3, gap: '16px', lightbox: false, style: galleryStyle } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gallery-grid-4',
|
||||
name: 'Gallery - tight four-column grid',
|
||||
family: 'gallery',
|
||||
keywords: ['gallery', 'photos', 'compact', 'masonry', 'tiles'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Gallery', anchorId: '', style: sectionPad('56px', '20px', tokenRef('bg')) },
|
||||
children: [
|
||||
heading('Gallery'),
|
||||
{ type: 'Gallery', props: { images: photos(8), columns: 4, gap: '8px', lightbox: false, style: galleryStyle } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gallery-feature-text',
|
||||
name: 'Gallery - photos with intro text',
|
||||
family: 'gallery',
|
||||
keywords: ['gallery', 'photos', 'story', 'text', 'showcase'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Gallery', anchorId: '', style: sectionPad() },
|
||||
children: [
|
||||
columns('40-60', '40px',
|
||||
column(
|
||||
heading('See the difference', 'h2', 'left'),
|
||||
para('Every job is done with care. Take a look at some of our favourites, then get in touch.', 'left'),
|
||||
button('Start a project', 'contact'),
|
||||
),
|
||||
column({ type: 'Gallery', props: { images: photos(4), columns: 2, gap: '12px', lightbox: false, style: galleryStyle } }),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gallery-photo-trio',
|
||||
name: 'Gallery - three feature photos',
|
||||
family: 'gallery',
|
||||
keywords: ['gallery', 'photos', 'three', 'featured', 'highlights'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Featured photos', anchorId: '', style: sectionPad('64px', '20px', tokenRef('secondary')) },
|
||||
children: [
|
||||
heading('Highlights'),
|
||||
columns('equal', '24px',
|
||||
column(img('Featured photo one', '3 / 4'), para('Our workshop', 'center', tokenRef('muted'), { marginTop: '12px', marginBottom: '0px' })),
|
||||
column(img('Featured photo two', '3 / 4'), para('Our team', 'center', tokenRef('muted'), { marginTop: '12px', marginBottom: '0px' })),
|
||||
column(img('Featured photo three', '3 / 4'), para('Our results', 'center', tokenRef('muted'), { marginTop: '12px', marginBottom: '0px' })),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,4 +1,73 @@
|
||||
import type { SectionDef } from '../catalog';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
import { sectionPad, heading, para, button, column, columns, hoursBlock, map } from './_shared-c';
|
||||
|
||||
/** Content task appends to HOURS_SECTIONS only (see hero.ts for the shape). */
|
||||
export const HOURS_SECTIONS: SectionDef[] = [];
|
||||
/** Opening-hours sections. */
|
||||
export const HOURS_SECTIONS: SectionDef[] = [
|
||||
{
|
||||
id: 'hours-table',
|
||||
name: 'Hours - centred table',
|
||||
family: 'hours',
|
||||
keywords: ['hours', 'opening', 'schedule', 'times', 'open'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Hours', anchorId: '', style: sectionPad('64px') },
|
||||
children: [
|
||||
{ type: 'OpeningHours', props: { title: 'Opening Hours', note: 'Hours may vary on public holidays.', style: { backgroundColor: 'transparent', paddingTop: '0px', paddingBottom: '0px', paddingLeft: '0px', paddingRight: '0px' } } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'hours-with-map',
|
||||
name: 'Hours - table beside map',
|
||||
family: 'hours',
|
||||
keywords: ['hours', 'map', 'location', 'find us', 'visit'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Hours and map', anchorId: '', style: sectionPad() },
|
||||
children: [
|
||||
heading('Visit Us'),
|
||||
para('Find us on Main Street. Drop in during opening hours.'),
|
||||
columns('45-55', '40px', column(hoursBlock()), column(map('380px'))),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'hours-with-details',
|
||||
name: 'Hours - with address and call button',
|
||||
family: 'hours',
|
||||
keywords: ['hours', 'address', 'phone', 'call', 'contact'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Hours and details', anchorId: '', style: sectionPad('72px', '20px', tokenRef('bg')) },
|
||||
children: [
|
||||
columns('50-50', '48px',
|
||||
column(hoursBlock('When we are open')),
|
||||
column(
|
||||
heading('Come and see us', 'h2', 'left'),
|
||||
para('123 Main Street, Your Town', 'left'),
|
||||
para('Phone: (555) 123-4567', 'left'),
|
||||
button('Call now', 'tel:+15551234567'),
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'hours-compact-strip',
|
||||
name: 'Hours - compact info strip',
|
||||
family: 'hours',
|
||||
keywords: ['hours', 'strip', 'compact', 'footer', 'summary'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Hours strip', anchorId: '', style: sectionPad('40px', '20px', tokenRef('secondary')) },
|
||||
children: [
|
||||
columns('equal', '24px',
|
||||
column(heading('Open', 'h3', 'center'), para('Mon-Fri 9am-5pm', 'center', tokenRef('text'), { marginBottom: '0px' })),
|
||||
column(heading('Weekend', 'h3', 'center'), para('Sat 10am-2pm, Sun closed', 'center', tokenRef('text'), { marginBottom: '0px' })),
|
||||
column(heading('Call', 'h3', 'center'), para('(555) 123-4567', 'center', tokenRef('text'), { marginBottom: '0px' })),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,4 +1,81 @@
|
||||
import type { SectionDef } from '../catalog';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
import { sectionPad, heading, para, button, column, columns, hoursBlock, map } from './_shared-c';
|
||||
|
||||
/** Content task appends to LOCAL_SECTIONS only (see hero.ts for the shape). */
|
||||
export const LOCAL_SECTIONS: SectionDef[] = [];
|
||||
/** Local-business strips (map + hours + call). Entries carry a regular family id. */
|
||||
export const LOCAL_SECTIONS: SectionDef[] = [
|
||||
{
|
||||
id: 'local-visit-us',
|
||||
name: 'Local - map, hours and call',
|
||||
family: 'hours',
|
||||
keywords: ['local', 'map', 'hours', 'call', 'visit'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Visit us', anchorId: '', style: sectionPad() },
|
||||
children: [
|
||||
columns('50-50', '40px',
|
||||
column(map('420px')),
|
||||
column(
|
||||
heading('Visit Us', 'h2', 'left'),
|
||||
para('123 Main Street, Your Town', 'left'),
|
||||
hoursBlock('Opening Hours'),
|
||||
button('Call (555) 123-4567', 'tel:+15551234567'),
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'local-call-strip',
|
||||
name: 'Local - call us strip',
|
||||
family: 'contact',
|
||||
keywords: ['local', 'call', 'phone', 'sticky', 'mobile'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Call strip', anchorId: '', style: sectionPad('48px', '20px', tokenRef('primary')) },
|
||||
children: [
|
||||
heading('Call us today', 'h2', 'center', tokenRef('bg')),
|
||||
para('Mon-Fri 9am-5pm. Same-day answers on most enquiries.', 'center', tokenRef('bg')),
|
||||
{
|
||||
type: 'ButtonLink',
|
||||
props: { text: 'Call (555) 123-4567', href: 'tel:+15551234567', style: { backgroundColor: tokenRef('bg'), color: tokenRef('primary'), padding: '12px 28px', borderRadius: '8px', fontWeight: '600', fontSize: '16px', border: 'none' } },
|
||||
},
|
||||
// Mobile-only fixed bar (hidden on desktop/tablet by the component's own flags).
|
||||
{ type: 'StickyCallBar', props: { phone: '+15551234567', callLabel: 'Call us', directionsUrl: 'https://maps.google.com/?q=Your+Business', directionsLabel: 'Directions', bgColor: tokenRef('primary'), textColor: tokenRef('bg'), position: 'bottom', style: {}, hideOnDesktop: true, hideOnTablet: true, hideOnMobile: false } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'local-reviews-visit',
|
||||
name: 'Local - reviews then map and hours',
|
||||
family: 'social-proof',
|
||||
keywords: ['local', 'reviews', 'map', 'hours', 'customers'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Reviews and visit', anchorId: '', style: sectionPad('48px') },
|
||||
children: [
|
||||
{ type: 'ReviewsStrip', props: { title: 'What our customers say', cardBg: tokenRef('bg'), style: { backgroundColor: 'transparent', paddingTop: '0px', paddingBottom: '32px', paddingLeft: '0px', paddingRight: '0px' } } },
|
||||
columns('45-55', '32px', column(hoursBlock()), column(map('340px'))),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'local-map-banner',
|
||||
name: 'Local - wide map with address bar',
|
||||
family: 'contact',
|
||||
keywords: ['local', 'map', 'directions', 'address', 'wide'],
|
||||
tree: {
|
||||
type: 'Section',
|
||||
props: { aiName: 'Map banner', anchorId: '', style: sectionPad('56px', '20px', tokenRef('secondary')) },
|
||||
children: [
|
||||
heading('Find Us'),
|
||||
map('380px'),
|
||||
columns('equal', '24px',
|
||||
column(para('123 Main Street, Your Town', 'center', tokenRef('text'), { marginTop: '16px', marginBottom: '0px' })),
|
||||
column(para('Mon-Fri 9am-5pm, Sat 10am-2pm', 'center', tokenRef('text'), { marginTop: '16px', marginBottom: '0px' })),
|
||||
column({ type: 'Container', props: { style: { padding: '0px', minHeight: '', textAlign: 'center', paddingTop: '8px' } }, children: [button('Get directions', 'contact', false)] }),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in new issue
Block a user