✨feature: Set up contact info block and map
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"key": "group_6867eaa8c0001",
|
||||
"title": "Contact Info",
|
||||
"fields": [
|
||||
{
|
||||
"key": "field_6a4997f2d7b1f",
|
||||
"label": "Heading",
|
||||
"name": "heading",
|
||||
"aria-label": "",
|
||||
"type": "text",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "",
|
||||
"maxlength": "",
|
||||
"allow_in_bindings": 0,
|
||||
"placeholder": "",
|
||||
"prepend": "",
|
||||
"append": ""
|
||||
},
|
||||
{
|
||||
"key": "field_6867eaa8c0002",
|
||||
"label": "Map Latitude",
|
||||
"name": "map_latitude",
|
||||
"aria-label": "",
|
||||
"type": "number",
|
||||
"instructions": "Latitude for the Leaflet map center.",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "100",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "49.8951",
|
||||
"min": -90,
|
||||
"max": 90,
|
||||
"allow_in_bindings": 1,
|
||||
"placeholder": "49.8951",
|
||||
"step": "0.000001",
|
||||
"prepend": "",
|
||||
"append": ""
|
||||
},
|
||||
{
|
||||
"key": "field_6867eaa8c0003",
|
||||
"label": "Map Longitude",
|
||||
"name": "map_longitude",
|
||||
"aria-label": "",
|
||||
"type": "number",
|
||||
"instructions": "Longitude for the Leaflet map center.",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "100",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "-97.1384",
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"allow_in_bindings": 1,
|
||||
"placeholder": "-97.1384",
|
||||
"step": "0.000001",
|
||||
"prepend": "",
|
||||
"append": ""
|
||||
},
|
||||
{
|
||||
"key": "field_6867eaa8c0004",
|
||||
"label": "Map Zoom",
|
||||
"name": "map_zoom",
|
||||
"aria-label": "",
|
||||
"type": "number",
|
||||
"instructions": "Initial map zoom level (1-19).",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "100",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": 14,
|
||||
"min": 1,
|
||||
"max": 19,
|
||||
"allow_in_bindings": 1,
|
||||
"placeholder": 14,
|
||||
"step": 1,
|
||||
"prepend": "",
|
||||
"append": ""
|
||||
}
|
||||
],
|
||||
"location": [
|
||||
[
|
||||
{
|
||||
"param": "block",
|
||||
"operator": "==",
|
||||
"value": "acf\/contact-info"
|
||||
}
|
||||
]
|
||||
],
|
||||
"menu_order": 0,
|
||||
"position": "side",
|
||||
"style": "default",
|
||||
"label_placement": "top",
|
||||
"instruction_placement": "label",
|
||||
"hide_on_screen": "",
|
||||
"active": true,
|
||||
"description": "heading and map fields for the Contact Info block.",
|
||||
"show_in_rest": 0,
|
||||
"display_title": "",
|
||||
"allow_ai_access": false,
|
||||
"ai_description": "",
|
||||
"modified": 1783208109
|
||||
}
|
||||
@@ -0,0 +1,880 @@
|
||||
# Contact Page + Hero Re-Scope Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the contact page layout (two-column 50/50 with contact info, Gravity Forms, and a Leaflet placeholder) and re-scope the existing inner-page hero to render only on the Services page, its children, and its grandchildren.
|
||||
|
||||
**Architecture:** The contact page work is a block-level change: rewrite `views/blocks/contact-info/contact-info.php` and `contact-info.css`, add three SVG icons, and add a Playwright test. The hero re-scope is a single PHP helper in `lib/extras.php` plus a one-line change in `header.php`. Both are independently testable. A test bypass constant lets the existing `inner-page.spec.js` fixtures keep working without moving them under the Services page.
|
||||
|
||||
**Tech Stack:** WordPress 6.x, ACF Pro, PHP 8.x, Tailwind CSS v4, Playwright, axe-core, PHPCS (WordPress standard).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Tabs for PHP indentation (project standard, see `composer.json` and existing files).
|
||||
- The theme's Tailwind build outputs `static/dist/theme.css`; that file is gitignored but committed with `git add -f` per project convention.
|
||||
- PHPCS uses the WordPress coding standard; `composer lint` must pass.
|
||||
- All Playwright tests in `tests/*.spec.js` must pass; the existing `mobile-homepage.spec.js` has 4 known pre-existing failures that are out of scope.
|
||||
- The `acf/contact-info` block currently has an `InnerBlocks` call. Removing it is a breaking change for any page that was using the block as a wrapper. A grep at task start confirms there are zero such pages.
|
||||
- The hero re-scope uses slug `services` to find the Services page (`get_page_by_path( 'services' )`). The Services page must exist on the live site with slug `services` and the test fixture page must continue to render the hero even though it is not a Services descendant (handled by `CWC_TEST_BYPASS_HERO_GATE`).
|
||||
- The Leaflet map embed and the map pin SVG are not yet available; the right column renders as a styled placeholder.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility | Created/Modified |
|
||||
| --- | --- | --- |
|
||||
| `views/blocks/contact-info/contact-info.php` | Renders the two-column 50/50 layout: heading + address/email/phone + Gravity Form on the left, Leaflet placeholder on the right. | Modify |
|
||||
| `views/blocks/contact-info/contact-info.css` | Layout, icon sizing, Gravity Forms field overrides, and the right-edge break-out for the map. | Modify |
|
||||
| `static/img/contact/icon-address.svg` | Map pin outline, 24×24, `stroke="currentColor" fill="none"`. | Create |
|
||||
| `static/img/contact/icon-email.svg` | Envelope outline, 24×24, `stroke="currentColor" fill="none"`. | Create |
|
||||
| `static/img/contact/icon-phone.svg` | Phone outline, 24×24, `stroke="currentColor" fill="none"`. | Create |
|
||||
| `lib/extras.php` | Add `isServicesDescendant()` and `CWC_TEST_BYPASS_HERO_GATE` constant. | Modify |
|
||||
| `header.php` | Update `$showHero` to also require `isServicesDescendant()` on the `page` post type. | Modify |
|
||||
| `static/dist/theme.css` | Rebuilt via `npm run build`. | Modified (committed with `git add -f`). |
|
||||
| `tests/contact-page.spec.js` | New Playwright spec for the contact page layout. | Create |
|
||||
| `tests/services-hero-scope.spec.js` | New Playwright spec for the hero re-scope. | Create |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add the three contact icon SVGs
|
||||
|
||||
**Files:**
|
||||
- Create: `static/img/contact/icon-address.svg`
|
||||
- Create: `static/img/contact/icon-email.svg`
|
||||
- Create: `static/img/contact/icon-phone.svg`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing
|
||||
- Produces: three static SVG assets that the `contact-info` block references via `get_theme_file_uri( '/static/img/contact/...svg' )`.
|
||||
|
||||
The user is providing the final SVGs. Until they arrive, use these placeholders (24×24 viewBox, `stroke="currentColor" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"`). The CSS colors them via `currentColor`.
|
||||
|
||||
- [ ] **Step 1: Create the static/img/contact directory**
|
||||
|
||||
Run: `mkdir -p static/img/contact`
|
||||
Expected: directory created; no error.
|
||||
|
||||
- [ ] **Step 2: Write `static/img/contact/icon-address.svg`**
|
||||
|
||||
```xml
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M20 10c0 7-8 12-8 12s-8-5-8-12a8 8 0 0 1 16 0Z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write `static/img/contact/icon-email.svg`**
|
||||
|
||||
```xml
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2"/>
|
||||
<path d="m3 7 9 6 9-6"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Write `static/img/contact/icon-phone.svg`**
|
||||
|
||||
```xml
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.8a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.34 1.84.57 2.8.7A2 2 0 0 1 22 16.92Z"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify all three files exist and are valid XML**
|
||||
|
||||
Run: `ls -la static/img/contact/ && for f in static/img/contact/*.svg; do echo "== $f =="; head -1 "$f"; done`
|
||||
Expected: three files, each starting with `<svg xmlns=...`.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add static/img/contact/
|
||||
git commit -m "feat(contact): add address, email, phone icon SVGs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Rewrite `views/blocks/contact-info/contact-info.php`
|
||||
|
||||
**Files:**
|
||||
- Modify: `views/blocks/contact-info/contact-info.php`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the global `contact_info` option (`get_field( 'contact_info', 'option' )['address' | 'email' | 'phone']`).
|
||||
- Produces: a `<section class="contact-info">` with `.contact-info__grid` > `.contact-info__details` and `.contact-info__map`. The details column has the heading, an `<ul>` of address/email/phone `<li>`s with `.contact-info__icon` spans, and a `.contact-info__form` div with `do_shortcode( '[gravityform id="1" title="false" description="false" ajax="true"]' )`. The map column has `<div id="contact-map">`.
|
||||
|
||||
- [ ] **Step 1: Verify no other page uses the `InnerBlocks` form of this block**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -rn "contact-info" wp-content/themes/community-works-collaborative/views/ --include="*.php" | grep -v "contact-info/contact-info.php" | grep -v "contact-info/block.json"
|
||||
```
|
||||
Expected: no matches. If matches exist, list them and confirm the InnerBlocks content is empty for each before proceeding.
|
||||
|
||||
- [ ] **Step 2: Replace `views/blocks/contact-info/contact-info.php` with the new content**
|
||||
|
||||
Write the entire file:
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Block Name: Contact Info
|
||||
*
|
||||
* Two-column contact page layout: contact info, address/email/phone,
|
||||
* and a Gravity Forms contact form on the left; Leaflet map placeholder
|
||||
* on the right. Reads from the global `contact_info` option.
|
||||
*
|
||||
* @package CWC
|
||||
*/
|
||||
|
||||
namespace CWC;
|
||||
|
||||
$info = get_field( 'contact_info', 'option' );
|
||||
$address = $info['address'] ?? '';
|
||||
$email = $info['email'] ?? '';
|
||||
$phone = $info['phone'] ?? '';
|
||||
|
||||
$classes = 'contact-info';
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
?>
|
||||
|
||||
<section <?php echo esc_attr( $wrapper ); ?>>
|
||||
<div class="contact-info__grid">
|
||||
<div class="contact-info__details">
|
||||
<h1 class="contact-info__heading">Contact</h1>
|
||||
|
||||
<ul class="contact-info__items">
|
||||
<?php if ( $address ) : ?>
|
||||
<li class="contact-info__item contact-info__item--address">
|
||||
<span class="contact-info__icon" aria-hidden="true">
|
||||
<img src="<?php echo esc_url( get_theme_file_uri( '/static/img/contact/icon-address.svg' ) ); ?>" alt="" width="24" height="24" loading="lazy" />
|
||||
</span>
|
||||
<span class="contact-info__value"><?php echo wp_kses_post( $address ); ?></span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $email ) : ?>
|
||||
<li class="contact-info__item contact-info__item--email">
|
||||
<span class="contact-info__icon" aria-hidden="true">
|
||||
<img src="<?php echo esc_url( get_theme_file_uri( '/static/img/contact/icon-email.svg' ) ); ?>" alt="" width="24" height="24" loading="lazy" />
|
||||
</span>
|
||||
<a class="contact-info__value" href="mailto:<?php echo esc_attr( $email ); ?>"><?php echo esc_html( $email ); ?></a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $phone ) : ?>
|
||||
<li class="contact-info__item contact-info__item--phone">
|
||||
<span class="contact-info__icon" aria-hidden="true">
|
||||
<img src="<?php echo esc_url( get_theme_file_uri( '/static/img/contact/icon-phone.svg' ) ); ?>" alt="" width="24" height="24" loading="lazy" />
|
||||
</span>
|
||||
<a class="contact-info__value" href="tel:<?php echo esc_attr( $phone ); ?>"><?php echo esc_html( $phone ); ?></a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
|
||||
<div class="contact-info__form">
|
||||
<?php echo do_shortcode( '[gravityform id="1" title="false" description="false" ajax="true"]' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-info__map" id="contact-map" aria-label="Map of downtown Winnipeg" role="region">
|
||||
<!-- Leaflet map will mount here. Pin: orange-01 + white map marker (TBD). -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the file parses (no PHP syntax errors)**
|
||||
|
||||
Run: `php -l views/blocks/contact-info/contact-info.php`
|
||||
Expected: `No syntax errors detected in views/blocks/contact-info/contact-info.php`.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add views/blocks/contact-info/contact-info.php
|
||||
git commit -m "feat(contact): rewrite contact-info block to two-column layout"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Write `views/blocks/contact-info/contact-info.css` (layout + icons)
|
||||
|
||||
**Files:**
|
||||
- Modify: `views/blocks/contact-info/contact-info.css`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the new HTML structure from Task 2.
|
||||
- Produces: a mobile-first stylesheet that lays out the two columns, sizes the icons, and breaks the right column out to the viewport edge on desktop.
|
||||
|
||||
- [ ] **Step 1: Replace `views/blocks/contact-info/contact-info.css` with the new content**
|
||||
|
||||
```css
|
||||
/* Contact Info block styles */
|
||||
|
||||
.contact-info {
|
||||
background: var(--color-white);
|
||||
padding-block: clamp(3rem, 6vw, 4rem);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contact-info__grid {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
grid-template-columns: 1fr;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contact-info__details {
|
||||
max-width: 36rem;
|
||||
padding-inline: clamp(1.5rem, 5vw, 3rem);
|
||||
}
|
||||
|
||||
.contact-info__heading {
|
||||
color: var(--color-cwc-blue-01);
|
||||
font-family: var(--font-quincy, 'Quincy', serif);
|
||||
font-size: clamp(2.5rem, 6vw, 4.5rem);
|
||||
font-weight: 400;
|
||||
line-height: 1.05;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.contact-info__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
list-style: none;
|
||||
margin: 0 0 2.5rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.contact-info__item {
|
||||
align-items: flex-start;
|
||||
color: var(--color-dark);
|
||||
display: flex;
|
||||
font-size: 1rem;
|
||||
gap: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.contact-info__icon {
|
||||
align-items: center;
|
||||
color: var(--color-cwc-blue-03);
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
height: 1.5rem;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
.contact-info__icon img {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contact-info__value {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.contact-info__value:hover {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.contact-info__map {
|
||||
background: color-mix(in oklch, var(--color-cwc-blue-03) 30%, white);
|
||||
border-radius: 0.5rem;
|
||||
min-height: 24rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.contact-info__grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.contact-info__details {
|
||||
padding-inline: clamp(1.5rem, 5vw, 3rem) 0;
|
||||
}
|
||||
|
||||
.contact-info__map {
|
||||
align-self: stretch;
|
||||
border-radius: 0;
|
||||
margin-right: calc(50% - 50vw);
|
||||
margin-left: 0;
|
||||
min-height: 32rem;
|
||||
width: calc(50vw - 0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.contact-info {
|
||||
padding-block: 2rem;
|
||||
}
|
||||
|
||||
.contact-info__map {
|
||||
margin-inline: clamp(1.5rem, 5vw, 3rem);
|
||||
min-height: 18rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the import to `styles/blocks/index.css` if not already present**
|
||||
|
||||
Run: `grep -n "contact-info" styles/blocks/index.css`
|
||||
Expected: a matching line. If not present, add the views-relative path (the file lives at `views/blocks/contact-info/contact-info.css`, NOT at `styles/blocks/`, so a sibling-relative path will fail the build):
|
||||
|
||||
```css
|
||||
@import '../../views/blocks/contact-info/contact-info.css';
|
||||
```
|
||||
|
||||
> **Why views-relative, not sibling-relative:** `styles/blocks/` only contains `buttons.css`, `core.css`, `misc.css`, and `page-hero.css`. The new CSS is at `views/blocks/contact-info/contact-info.css` (so the per-block `style: ['file:./contact-info.css']` in `block.json` resolves for the block editor preview). A sibling-relative import would silently produce "Can't resolve './contact-info.css' in 'styles/blocks'" and fail the build.
|
||||
|
||||
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add views/blocks/contact-info/contact-info.css styles/blocks/index.css
|
||||
git commit -m "feat(contact): add two-column layout and icon styles"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add Gravity Forms field overrides to `contact-info.css`
|
||||
|
||||
**Files:**
|
||||
- Modify: `views/blocks/contact-info/contact-info.css`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the layout from Task 3; the `gform_*` HTML output from Gravity Forms.
|
||||
- Produces: uppercase CWC Blue 01 labels, right-aligned CWC Orange 01 required asterisks, 30% opacity CWC Blue 03 input fills, slightly rounded corners, comfortable padding, a taller message textarea, and a submit button styled like the header Contact button.
|
||||
|
||||
- [ ] **Step 1: Append the Gravity Forms overrides to the end of `views/blocks/contact-info/contact-info.css`**
|
||||
|
||||
```css
|
||||
/* Gravity Forms field overrides (scoped to .contact-info__form) */
|
||||
|
||||
.contact-info__form .gform_wrapper {
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.contact-info__form .gfield {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.contact-info__form .gfield_label {
|
||||
color: var(--color-cwc-blue-01);
|
||||
display: flex;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
justify-content: space-between;
|
||||
letter-spacing: 0.05em;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.contact-info__form .gfield_required {
|
||||
color: var(--color-cwc-orange-01);
|
||||
font-weight: 700;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.contact-info__form input[type="text"],
|
||||
.contact-info__form input[type="email"],
|
||||
.contact-info__form input[type="tel"],
|
||||
.contact-info__form input[type="url"],
|
||||
.contact-info__form textarea,
|
||||
.contact-info__form select {
|
||||
background: color-mix(in oklch, var(--color-cwc-blue-03) 30%, white);
|
||||
border: 1px solid var(--color-cwc-blue-03);
|
||||
border-radius: 0.375rem;
|
||||
color: var(--color-dark);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
padding: 0.75rem 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contact-info__form input[type="text"]:focus,
|
||||
.contact-info__form input[type="email"]:focus,
|
||||
.contact-info__form input[type="tel"]:focus,
|
||||
.contact-info__form input[type="url"]:focus,
|
||||
.contact-info__form textarea:focus,
|
||||
.contact-info__form select:focus {
|
||||
border-color: var(--color-cwc-blue-01);
|
||||
outline: 2px solid var(--color-cwc-blue-01);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.contact-info__form textarea {
|
||||
min-height: 10rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.contact-info__form .gform_footer {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.contact-info__form .gform_footer input[type="submit"],
|
||||
.contact-info__form .gform_footer button[type="submit"] {
|
||||
background: var(--background-image-button-gradient);
|
||||
border: 3px solid var(--color-secondary);
|
||||
border-radius: 1.25rem 0.25rem 1.25rem 0.25rem;
|
||||
color: var(--color-white);
|
||||
cursor: pointer;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
padding: 0.625rem 2rem;
|
||||
transition: background 200ms, border-color 200ms, color 200ms;
|
||||
}
|
||||
|
||||
.contact-info__form .gform_footer input[type="submit"]:hover,
|
||||
.contact-info__form .gform_footer button[type="submit"]:hover {
|
||||
background: var(--color-secondary);
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.contact-info__form .validation_message {
|
||||
color: var(--color-danger);
|
||||
font-style: italic;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add views/blocks/contact-info/contact-info.css
|
||||
git commit -m "feat(contact): style Gravity Forms fields and submit button"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Build the dist CSS and verify
|
||||
|
||||
**Files:**
|
||||
- Modify: `static/dist/theme.css` (committed with `git add -f`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the new `views/blocks/contact-info/contact-info.css` and any touched source files.
|
||||
- Produces: a rebuilt `static/dist/theme.css` that includes the new contact-info styles.
|
||||
|
||||
- [ ] **Step 1: Run the build**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: build completes without errors. The dist file is regenerated.
|
||||
|
||||
- [ ] **Step 2: Verify the new classes are present in the dist CSS**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -c "contact-info" static/dist/theme.css
|
||||
```
|
||||
Expected: at least 10 matches (a count of 0 means the build did not pick up the new CSS).
|
||||
|
||||
- [ ] **Step 3: Commit the rebuilt dist**
|
||||
|
||||
```bash
|
||||
git add -f static/dist/theme.css
|
||||
git commit -m "build: regenerate dist with contact-info styles"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Add `isServicesDescendant()` and the bypass constant to `lib/extras.php`
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/extras.php`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the global `$post` and an optional services-page override.
|
||||
- Produces: `isServicesDescendant( ?WP_Post $post = null, ?int $services_id = null ): bool` that returns `true` for the Services page, its children, and its grandchildren. Returns `false` if the Services page does not exist. A `CWC_TEST_BYPASS_HERO_GATE` constant (default `false`; can be flipped to `true` in test setup) bypasses the check.
|
||||
|
||||
- [ ] **Step 1: Append the constant and helper to `lib/extras.php`**
|
||||
|
||||
Find the end of the `hasPageHeader()` function (the `return true; }` block near the bottom of the relevant section). After that function's closing brace, add:
|
||||
|
||||
```php
|
||||
/**
|
||||
* Bypass constant for the inner-page hero ancestor check.
|
||||
*
|
||||
* When `true`, `isServicesDescendant()` returns `true` for every page.
|
||||
* Set this in test setup (e.g. in `wp-config.php` or a test bootstrap) so
|
||||
* test fixture pages that are not Services descendants can still render
|
||||
* the hero.
|
||||
*/
|
||||
if ( ! defined( 'CWC_TEST_BYPASS_HERO_GATE' ) ) {
|
||||
define( 'CWC_TEST_BYPASS_HERO_GATE', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current page is the Services page, a child of it, or
|
||||
* a grandchild of it. Used to gate the inner-page hero.
|
||||
*
|
||||
* @param WP_Post|null $post The post to check. Defaults to the global `$post`.
|
||||
* @param int|null $services_id Override the Services page ID. Defaults to
|
||||
* the page with slug `services`.
|
||||
*
|
||||
* @return bool true if the post is the Services page or a descendant up to 2 levels.
|
||||
*/
|
||||
function isServicesDescendant( $post = null, $services_id = null ) {
|
||||
if ( CWC_TEST_BYPASS_HERO_GATE ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( null === $post ) {
|
||||
global $post;
|
||||
}
|
||||
|
||||
if ( ! $post instanceof WP_Post ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( null === $services_id ) {
|
||||
$services = get_page_by_path( 'services' );
|
||||
if ( ! $services ) {
|
||||
return false;
|
||||
}
|
||||
$services_id = (int) $services->ID;
|
||||
}
|
||||
|
||||
$services_id = (int) $services_id;
|
||||
|
||||
if ( (int) $post->ID === $services_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( (int) $post->post_parent === $services_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$parent = get_post( $post->post_parent );
|
||||
if ( $parent && (int) $parent->post_parent === $services_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the file parses**
|
||||
|
||||
Run: `php -l lib/extras.php`
|
||||
Expected: `No syntax errors detected in lib/extras.php`.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add lib/extras.php
|
||||
git commit -m "feat(hero): add isServicesDescendant() helper and bypass constant"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Update `header.php` to gate the hero on `isServicesDescendant()`
|
||||
|
||||
**Files:**
|
||||
- Modify: `header.php`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `isServicesDescendant()` from Task 6.
|
||||
- Produces: a `$showHero` calculation that requires `isServicesDescendant() && hero_style === 'default'` for the `page` post type. The other gates (`is_home() || is_archive() || ...`) are unchanged.
|
||||
|
||||
- [ ] **Step 1: Update `$showHero` in `header.php`**
|
||||
|
||||
Replace the existing `$showHero` block:
|
||||
|
||||
```php
|
||||
$showHero = in_array(
|
||||
true,
|
||||
array(
|
||||
isServicesDescendant() && get_field( 'hero_style' ) === 'default',
|
||||
is_home(),
|
||||
is_archive(),
|
||||
is_single(),
|
||||
is_search(),
|
||||
is_404(),
|
||||
),
|
||||
true
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the file parses**
|
||||
|
||||
Run: `php -l header.php`
|
||||
Expected: `No syntax errors detected in header.php`.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add header.php
|
||||
git commit -m "feat(hero): gate inner-page hero on Services descendants"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Add Playwright test for the contact page
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/contact-page.spec.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the rendered contact page at `/contact/`.
|
||||
- Produces: a Playwright spec that verifies the two-column layout, icon rows, form area, and Leaflet placeholder render at desktop and mobile viewports, with no console errors and no axe violations.
|
||||
|
||||
- [ ] **Step 1: Create `tests/contact-page.spec.js`**
|
||||
|
||||
```js
|
||||
import { expect, test } from "@playwright/test";
|
||||
const AxeBuilder = require("@axe-core/playwright").default;
|
||||
|
||||
const contactUrl =
|
||||
process.env.TEST_CONTACT_URL ||
|
||||
"http://community-works-collaborative.test/contact/";
|
||||
|
||||
test.describe("contact page", () => {
|
||||
test("desktop: two-column layout with form area and map placeholder", async ({
|
||||
page,
|
||||
}) => {
|
||||
const pageErrors = [];
|
||||
page.on("pageerror", (err) => pageErrors.push(err.message));
|
||||
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await page.goto(contactUrl);
|
||||
|
||||
// No hero on the contact page.
|
||||
expect(await page.locator(".page-hero").count()).toBe(0);
|
||||
|
||||
// Block renders.
|
||||
const block = page.locator(".contact-info").first();
|
||||
await expect(block).toBeVisible();
|
||||
|
||||
// Left column has heading, items, and form area.
|
||||
const heading = page.locator(".contact-info__heading").first();
|
||||
await expect(heading).toBeVisible();
|
||||
await expect(heading).toHaveText(/contact/i);
|
||||
|
||||
const items = page.locator(".contact-info__item");
|
||||
const itemsCount = await items.count();
|
||||
expect(itemsCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const formArea = page.locator(".contact-info__form").first();
|
||||
await expect(formArea).toBeAttached();
|
||||
|
||||
// Right column has the Leaflet placeholder.
|
||||
const map = page.locator(".contact-info__map").first();
|
||||
await expect(map).toBeAttached();
|
||||
await expect(map).toHaveId("contact-map");
|
||||
|
||||
// No JS errors.
|
||||
expect(pageErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test("mobile 402: single column, map drops below form, no horizontal overflow", async ({
|
||||
page,
|
||||
}) => {
|
||||
const pageErrors = [];
|
||||
page.on("pageerror", (err) => pageErrors.push(err.message));
|
||||
|
||||
await page.setViewportSize({ width: 402, height: 874 });
|
||||
await page.goto(contactUrl);
|
||||
|
||||
const details = page.locator(".contact-info__details").first();
|
||||
const map = page.locator(".contact-info__map").first();
|
||||
|
||||
await expect(details).toBeVisible();
|
||||
await expect(map).toBeAttached();
|
||||
|
||||
const detailsBox = await details.boundingBox();
|
||||
const mapBox = await map.boundingBox();
|
||||
|
||||
expect(detailsBox).not.toBeNull();
|
||||
expect(mapBox).not.toBeNull();
|
||||
expect(mapBox.x).toBeGreaterThanOrEqual(detailsBox.x - 1);
|
||||
expect(mapBox.x + mapBox.width).toBeLessThanOrEqual(402 + 3);
|
||||
|
||||
expect(pageErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test("mobile 320: nothing clips horizontally", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 320, height: 800 });
|
||||
await page.goto(contactUrl);
|
||||
|
||||
const block = page.locator(".contact-info").first();
|
||||
await expect(block).toBeVisible();
|
||||
|
||||
const blockBox = await block.boundingBox();
|
||||
expect(blockBox.x + blockBox.width).toBeLessThanOrEqual(320 + 3);
|
||||
});
|
||||
|
||||
test("axe: no violations", async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await page.goto(contactUrl);
|
||||
|
||||
const results = await new AxeBuilder({ page })
|
||||
.withTags([
|
||||
"wcag2a",
|
||||
"wcag2aa",
|
||||
"wcag21a",
|
||||
"wcag21aa",
|
||||
"wcag22a",
|
||||
"wcag22aa",
|
||||
])
|
||||
.analyze();
|
||||
|
||||
await testInfo.attach("accessibility-scan-results", {
|
||||
body: JSON.stringify(results, null, 2),
|
||||
contentType: "application/json",
|
||||
});
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the new test**
|
||||
|
||||
Run: `npx playwright test tests/contact-page.spec.js`
|
||||
Expected: all 4 tests pass. If a test fails, fix the underlying code (do not weaken the test) and re-run.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/contact-page.spec.js
|
||||
git commit -m "test(contact): add contact-page Playwright spec"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Add Playwright test for the hero re-scope
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/services-hero-scope.spec.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: rendered pages: Services, a child of Services, a grandchild, a non-Services page (e.g. About), and `/news/` (blog index).
|
||||
- Produces: a Playwright spec that asserts the hero renders on the first three and not on the latter two.
|
||||
|
||||
**Pre-flight check (this task's pre-step):** confirm that the test fixture pages exist on the live site. If the test site is fresh, the developer may need to seed these pages in `beforeAll`. The spec uses environment variables so the URLs can be overridden:
|
||||
|
||||
```bash
|
||||
TEST_SERVICES_URL=http://community-works-collaborative.test/services/
|
||||
TEST_SERVICES_CHILD_URL=http://community-works-collaborative.test/communication-services/
|
||||
TEST_SERVICES_GRANDCHILD_URL=http://community-works-collaborative.test/communication-services/some-grandchild/
|
||||
TEST_NON_SERVICES_URL=http://community-works-collaborative.test/about/
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Create `tests/services-hero-scope.spec.js`**
|
||||
|
||||
```js
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const servicesUrl =
|
||||
process.env.TEST_SERVICES_URL ||
|
||||
"http://community-works-collaborative.test/services/";
|
||||
const childUrl =
|
||||
process.env.TEST_SERVICES_CHILD_URL ||
|
||||
"http://community-works-collaborative.test/communication-services/";
|
||||
const grandchildUrl =
|
||||
process.env.TEST_SERVICES_GRANDCHILD_URL ||
|
||||
"http://community-works-collaborative.test/communication-services/sample-grandchild/";
|
||||
const nonServicesUrl =
|
||||
process.env.TEST_NON_SERVICES_URL ||
|
||||
"http://community-works-collaborative.test/about/";
|
||||
const blogUrl =
|
||||
process.env.TEST_BLOG_URL ||
|
||||
"http://community-works-collaborative.test/news/";
|
||||
|
||||
test.describe("hero ancestor scope", () => {
|
||||
test("Services page renders the hero", async ({ page }) => {
|
||||
await page.goto(servicesUrl);
|
||||
await expect(page.locator(".page-hero").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("child of Services renders the hero", async ({ page }) => {
|
||||
await page.goto(childUrl);
|
||||
await expect(page.locator(".page-hero").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("grandchild of Services renders the hero", async ({ page }) => {
|
||||
await page.goto(grandchildUrl);
|
||||
await expect(page.locator(".page-hero").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("non-Services page does NOT render the hero", async ({ page }) => {
|
||||
await page.goto(nonServicesUrl);
|
||||
expect(await page.locator(".page-hero").count()).toBe(0);
|
||||
});
|
||||
|
||||
test("blog index still renders the hero (bypasses ancestor gate)", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(blogUrl);
|
||||
await expect(page.locator(".page-hero").first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the test fixture URLs exist**
|
||||
|
||||
Run (per URL):
|
||||
```bash
|
||||
curl -sI "$url" | head -1
|
||||
```
|
||||
Expected: `HTTP/1.1 200 OK` (or `301/302` for trailing-slash redirects). If a URL returns 404, the test will need a `beforeAll` that seeds the page; flag this and do not proceed without a real fixture.
|
||||
|
||||
- [ ] **Step 3: Run the new test**
|
||||
|
||||
Run: `npx playwright test tests/services-hero-scope.spec.js`
|
||||
Expected: all 5 tests pass. If `nonServicesUrl` still shows the hero, verify the live page's `post_parent` chain (the About page must not be a child or grandchild of `/services/`).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/services-hero-scope.spec.js
|
||||
git commit -m "test(hero): add ancestor-scope Playwright spec"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Run the full Playwright suite + PHPCS
|
||||
|
||||
**Files:**
|
||||
- Modify: `static/dist/theme.css` (only if not already rebuilt).
|
||||
- Modify: any source files if a regression is found.
|
||||
|
||||
- [ ] **Step 1: Run the full Playwright suite**
|
||||
|
||||
Run: `npx playwright test`
|
||||
Expected: all tests pass except the 4 known pre-existing `mobile-homepage.spec.js` failures. The new `tests/contact-page.spec.js` and `tests/services-hero-scope.spec.js` pass.
|
||||
|
||||
- [ ] **Step 2: Run PHPCS**
|
||||
|
||||
Run: `composer lint`
|
||||
Expected: no errors. If errors are found, fix them (most likely are tab-vs-spaces in PHP files; use `composer lint:fix` to auto-fix).
|
||||
|
||||
- [ ] **Step 3: Verify the rebuilt dist is current**
|
||||
|
||||
Run: `git status static/dist/theme.css`
|
||||
Expected: `nothing to commit, working tree clean` (i.e. the dist file is up to date with the source CSS). If uncommitted, run `git add -f static/dist/theme.css` and commit with `chore: sync dist after lint/fix`.
|
||||
|
||||
- [ ] **Step 4: Final commit (if anything changed in step 1-3)**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore: final pass after Playwright and PHPCS"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes
|
||||
|
||||
- **Spec coverage:** Task 1 (icons), Task 2 (block PHP), Task 3 (layout CSS), Task 4 (form CSS), Task 5 (build), Task 6 (PHP helper + bypass), Task 7 (header.php), Task 8 (contact test), Task 9 (hero-scope test), Task 10 (full suite). Every spec section is covered.
|
||||
- **Placeholder scan:** No "TBD", "TODO", or "implement later" in any step. The "Leaflet placeholder" and "map pin SVG TBD" comments in the PHP and CSS are intentional (they document where the embed will mount, not unfinished work).
|
||||
- **Type consistency:** `isServicesDescendant( ?WP_Post $post = null, ?int $services_id = null ): bool` is used identically in Task 6 (definition) and Task 7 (call site). `CWC_TEST_BYPASS_HERO_GATE` is defined in Task 6 and respected in Task 7 by virtue of the helper checking it.
|
||||
- **Spec section "Out of Scope" (Leaflet embed, map pin SVG, ACF fields on the block, audit of existing InnerBlocks usage):** all flagged in the spec. Task 2's Step 1 audit confirms no InnerBlocks usage before removing it.
|
||||
@@ -0,0 +1,274 @@
|
||||
# Contact Page Layout
|
||||
|
||||
## Goal
|
||||
|
||||
Build the contact page layout shown in `notes/contact-mockup-figma-2.png` (the Figma desktop export) so the contact page renders as a clean two-column 50/50 split: contact info (heading + address/email/phone with CWC Blue 03 outline icons) on the left with a white background and generous padding, and a desaturated CWC Blue 03 Leaflet map on the right that fills to the viewport edge. A Gravity Forms contact form sits between the contact info and the map. The page has no hero — just a ~3rem top margin below the header and no page title.
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers two related changes:
|
||||
|
||||
1. **The contact page layout** (the primary work) — a new two-column 50/50 layout for the contact page: contact info on the left, Gravity Forms + Leaflet placeholder on the right, no hero, no page title.
|
||||
2. **Re-scope the existing inner-page hero** so it renders only on Services children and grandchildren. The current logic in `header.php` (`$showHero = ... hero_style === 'default' ...`) gates the hero per-page via the `hero_style` ACF field; the new logic adds an additional gate: even when `hero_style === 'default'`, the hero is suppressed unless the current page is the Services page, a child of the Services page, or a grandchild of the Services page. All other pages get a plain page body with no hero (matching the contact page's no-hero treatment).
|
||||
|
||||
### Contact page scope
|
||||
|
||||
- Applies only to the contact page, via the editor dropping the `acf/contact-info` block on the contact page.
|
||||
- The existing `acf/contact-info` block is modified to render the new layout (left/contact-info, right/Leaflet placeholder) and drop the `InnerBlocks` call.
|
||||
- `page.php` is not changed — non-contact pages still get the inner-page hero (gated per the re-scope) or no hero at all.
|
||||
- No hero is rendered on the contact page. The contact page's `hero_style` ACF field should be set to `none` (this is the existing default, already handled by `header.php`).
|
||||
- The existing site-header, site-footer, and `hasSidebar()` filter are unchanged. The contact page does not show the sidebar (the contact-info block replaces it).
|
||||
- The existing `contact-block` block (the dark-navy CTA banner used on the home page) is unaffected. So is the footer.
|
||||
|
||||
### Inner-page hero re-scope
|
||||
|
||||
- The hero (already implemented: gradient backdrop, hero image, vector, intro) renders only when ALL of the following are true:
|
||||
- `get_field( 'hero_style' ) === 'default'` (the existing gate), AND
|
||||
- the current page is the Services page, a child of the Services page, or a grandchild of the Services page.
|
||||
- "Services page" is identified by slug: a page with `post_name = 'services'`. This is robust to the Services page's ID changing across environments.
|
||||
- "Child of the Services page" means `post_parent == $services_id`.
|
||||
- "Grandchild of the Services page" means the current page's `post_parent`'s `post_parent == $services_id`. (Two levels only; we do not need a recursive walker because the site's structure is shallow.)
|
||||
- A new PHP helper `isServicesDescendant()` lives in `lib/extras.php` next to `hasPageHeader()` and `hasSidebar()`. It returns `true` for the Services page itself, its children, and its grandchildren; `false` otherwise. It accepts the current post (default: `$post`) and an optional services-page override (default: looked up by slug).
|
||||
- `header.php` updates the `$showHero` calculation to also require `isServicesDescendant()`. The existing `is_home() || is_archive() || is_single() || is_search() || is_404()` conditions still gate the hero on those views (those views bypass the Services gate).
|
||||
- The `Page Heading` ACF group's `hero_style` field is unchanged. The conditional-logic that hides the hero_image and hero_vector fields when `hero_style === 'none'` is unchanged. Editors continue to control the gate per-page; the new check is a global "is this a Services descendant?" filter on top of that.
|
||||
- Pages that currently have `hero_style === 'default'` set but are NOT Services descendants lose their hero after this change. This is intentional (per the user's "scope only for Services children/grandchildren" instruction), but the editor may need to set `hero_style = 'none'` on those pages if they don't already have it. A pre-flight check lists the affected pages so the editor can review.
|
||||
|
||||
## Architecture
|
||||
|
||||
The contact page is composed of three layers:
|
||||
|
||||
1. **Page wrapper** — `page.php` continues to call `get_header()` and `get_footer()`. The contact page's body is the new `acf/contact-info` block, which the editor drops onto the page (the existing block). Because the block manages its own layout, no new page template is needed.
|
||||
2. **Contact info block** — `views/blocks/contact-info/contact-info.php` is rewritten to render the new two-column 50/50 layout. The left column holds a heading, the address/email/phone rows (from the `contact_info` global option, with CWC Blue 03 outline icons), and a Gravity Forms `[gravityform id="1" title="false"]` shortcode. The right column holds a placeholder `<div id="contact-map">` for the Leaflet embed.
|
||||
3. **Block CSS** — `views/blocks/contact-info/contact-info.css` is rewritten to lay out the two columns, the icon rows, and the Gravity Forms field overrides. Forms.css (`styles/base/forms.css`) already styles most of the form; the new CSS adds the field-level overrides (uppercase CWC Blue 01 labels, right-aligned CWC Orange 01 required asterisks, 30% opacity CWC Blue 03 input fills with borders, taller message textarea, and the secondary-button submit).
|
||||
|
||||
`page.php` is untouched. The contact page's editor just adds the `Contact Info` block and the existing flow renders it.
|
||||
|
||||
## `isServicesDescendant()` — `lib/extras.php`
|
||||
|
||||
```php
|
||||
function isServicesDescendant( $post = null, $services_id = null ) {
|
||||
if ( null === $post ) {
|
||||
global $post;
|
||||
}
|
||||
|
||||
if ( ! $post instanceof WP_Post ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( null === $services_id ) {
|
||||
$services = get_page_by_path( 'services' );
|
||||
if ( ! $services ) {
|
||||
return false;
|
||||
}
|
||||
$services_id = $services->ID;
|
||||
}
|
||||
|
||||
// The Services page itself.
|
||||
if ( (int) $post->ID === (int) $services_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Direct child of the Services page.
|
||||
if ( (int) $post->post_parent === (int) $services_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Grandchild: walk one level up.
|
||||
$parent = get_post( $post->post_parent );
|
||||
if ( $parent && (int) $parent->post_parent === (int) $services_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
`get_page_by_path( 'services' )` is a core WP function that finds a page by slug. It returns `null` if the Services page doesn't exist (e.g. on a fresh dev install), in which case the function returns `false` and no hero renders anywhere — a safe default.
|
||||
|
||||
## `header.php` change
|
||||
|
||||
The current `$showHero` is:
|
||||
|
||||
```php
|
||||
$showHero = in_array(
|
||||
true,
|
||||
array(
|
||||
get_field( 'hero_style' ) === 'default',
|
||||
is_home(),
|
||||
is_archive(),
|
||||
is_single(),
|
||||
is_search(),
|
||||
is_404(),
|
||||
),
|
||||
true
|
||||
);
|
||||
```
|
||||
|
||||
The new `$showHero` is:
|
||||
|
||||
```php
|
||||
$showHero = in_array(
|
||||
true,
|
||||
array(
|
||||
isServicesDescendant() && get_field( 'hero_style' ) === 'default',
|
||||
is_home(),
|
||||
is_archive(),
|
||||
is_single(),
|
||||
is_search(),
|
||||
is_404(),
|
||||
),
|
||||
true
|
||||
);
|
||||
```
|
||||
|
||||
The `is_home() / is_archive() / is_single() / is_search() / is_404()` conditions still gate the hero on those views, bypassing the Services check (so blog posts still get the inner-page layout they had before, single posts still get the dark hero from `page-hero.php`, etc.). The Services gate only applies to the `page` post type.
|
||||
|
||||
## ACF Field Group Changes
|
||||
|
||||
None. The `contact_info` global option group (`acf/group_5fd3e006e5da5.json`) already provides `email`, `phone`, `address`, `fax`, and `hours` sub-fields. We read `email`, `phone`, and `address` only.
|
||||
|
||||
The `acf/contact-info` block (`acf/group_5fd3e006e5da5.json`'s consumer, defined by `views/blocks/contact-info/block.json`) currently has no ACF fields of its own — it's an `InnerBlocks` block. We will not add ACF fields. The heading, the shortcode, and the Leaflet placeholder are hard-coded into the block's render template, since they are part of the design, not per-page content.
|
||||
|
||||
If the editor later needs to override the heading or shortcode, that becomes a follow-up: add ACF fields to the block, default them to the current hard-coded values, and render the field value with a fallback to the hard-coded string.
|
||||
|
||||
## Contact Info Block — `views/blocks/contact-info/contact-info.php`
|
||||
|
||||
The block renders a `<section class="contact-info">` with a flex row that flips to a column on mobile. The block manages its own width — it does NOT inherit the `.container` constraint from `page.php`'s `.entry-content`. To achieve this, the block's outermost wrapper gets `container` itself and resets the page's `entry-content` padding so the map can reach the viewport edge on the right.
|
||||
|
||||
```php
|
||||
<section <?php echo esc_attr( $wrapper ); ?>>
|
||||
<div class="contact-info__grid">
|
||||
<div class="contact-info__details">
|
||||
<h1 class="contact-info__heading">Contact</h1>
|
||||
|
||||
<ul class="contact-info__items">
|
||||
<li class="contact-info__item contact-info__item--address">
|
||||
<span class="contact-info__icon" aria-hidden="true">[icon-address]</span>
|
||||
<span class="contact-info__value"><?php echo wp_kses_post( get_field( 'contact_info', 'option' )['address'] ); ?></span>
|
||||
</li>
|
||||
<li class="contact-info__item contact-info__item--email">
|
||||
<span class="contact-info__icon" aria-hidden="true">[icon-email]</span>
|
||||
<a class="contact-info__value" href="mailto:<?php echo esc_attr( get_field( 'contact_info', 'option' )['email'] ); ?>">
|
||||
<?php echo esc_html( get_field( 'contact_info', 'option' )['email'] ); ?>
|
||||
</a>
|
||||
</li>
|
||||
<li class="contact-info__item contact-info__item--phone">
|
||||
<span class="contact-info__icon" aria-hidden="true">[icon-phone]</span>
|
||||
<a class="contact-info__value" href="tel:<?php echo esc_attr( get_field( 'contact_info', 'option' )['phone'] ); ?>">
|
||||
<?php echo esc_html( get_field( 'contact_info', 'option' )['phone'] ); ?>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="contact-info__form">
|
||||
<?php echo do_shortcode( '[gravityform id="1" title="false" description="false" ajax="true"]' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-info__map" id="contact-map" aria-label="Map of downtown Winnipeg" role="region">
|
||||
<!-- Leaflet map will mount here. Pin: orange-01 + white map marker (TBD). -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
The `[icon-address]`, `[icon-email]`, `[icon-phone]` placeholders are inlined SVGs with `stroke="currentColor"` and `fill="none"`, sized to the CWC Blue 03 color via CSS `color: var(--color-cwc-blue-03)`. The three SVGs are stored in `static/img/contact/` and referenced via `get_theme_file_uri()` (the same pattern the existing `static/img/footer-email.svg` etc. use).
|
||||
|
||||
## Contact Info Block CSS — `views/blocks/contact-info/contact-info.css`
|
||||
|
||||
Rules (mobile-first, desktop via `@media (min-width: 1024px)`):
|
||||
|
||||
- `.contact-info` — `position: relative; background: var(--color-white); padding-block: clamp(2rem, 6vw, 4rem);`. Resets the `.entry-content` container.
|
||||
- `.contact-info__grid` — on mobile, single column. On `≥1024px`, `display: grid; grid-template-columns: 1fr 1fr; gap: 0;`. The right column extends to the viewport edge via negative right margin matching the page's container offset (the same trick the homepage-hero and the contact-block use to break out of the container on one side).
|
||||
- `.contact-info__details` — `padding-inline: clamp(1.5rem, 5vw, 3rem); max-width: 36rem;` so the heading, address, and form are aligned with the logo's left edge and don't span the full half.
|
||||
- `.contact-info__heading` — `font-family: var(--font-quincy, 'Quincy', serif); font-weight: 400; font-size: clamp(2.5rem, 6vw, 4.5rem); line-height: 1.05; color: var(--color-cwc-blue-01); margin-bottom: 1.5rem;`. Matches the inner-page hero heading for visual consistency.
|
||||
- `.contact-info__items` — `list-style: none; padding: 0; margin: 0 0 2rem 0; display: flex; flex-direction: column; gap: 0.75rem;`.
|
||||
- `.contact-info__item` — `display: flex; align-items: flex-start; gap: 0.75rem; color: var(--color-dark); font-size: 1rem; line-height: 1.5;`.
|
||||
- `.contact-info__icon` — `display: inline-flex; align-items: center; justify-content: center; width: 1.5rem; height: 1.5rem; flex-shrink: 0; color: var(--color-cwc-blue-03);`. Icons inherit the color via `currentColor`.
|
||||
- `.contact-info__value` — `text-decoration: none; color: inherit; &:hover { color: var(--color-secondary); }`.
|
||||
- `.contact-info__form` — Gravity Forms wrapper. Adds a top margin (`mt-8`) and the form-field overrides below.
|
||||
- `.contact-info__map` — `background: color-mix(in oklch, var(--color-cwc-blue-03) 30%, white); min-height: 24rem; position: relative;`. On `≥1024px`, breaks out to the viewport's right edge by setting `margin-right: calc(50% - 50vw); width: calc(50vw + 0px);`. The right edge is anchored to `100vw`, not the parent grid, so the map fills the right half of the screen on any desktop width.
|
||||
- Mobile (≤767px): `.contact-info__grid` collapses to one column, the map drops below the form, the right-edge break-out is removed.
|
||||
|
||||
### Gravity Forms field overrides (in `contact-info.css`)
|
||||
|
||||
Append after the layout rules:
|
||||
|
||||
- `.contact-info__form .gfield_label` — `text-transform: uppercase; color: var(--color-cwc-blue-01); font-size: 0.875rem; font-weight: 700; letter-spacing: 0.05em; margin-bottom: 0.5rem;`.
|
||||
- `.contact-info__form .gfield_required` — `color: var(--color-cwc-orange-01); margin-left: 0.25rem; font-weight: 700;`. Hide the default asterisk text and use an `::after` pseudo-element with content `" *"` so the asterisk is right-aligned inside the label (the default `gfield_required` is a trailing span; we override the label's flex layout to push the asterisk to the right with `display: flex; justify-content: space-between;` and put the `::after` on the label itself).
|
||||
- `.contact-info__form input[type="text"], .contact-info__form input[type="email"], .contact-info__form textarea` — `background: color-mix(in oklch, var(--color-cwc-blue-03) 30%, white); border: 1px solid var(--color-cwc-blue-03); border-radius: 0.375rem; padding: 0.75rem 1rem; font-size: 1rem;`. Override the existing `border-2 border-primary` from `forms.css` (this block has its own scope, so cascade works).
|
||||
- `.contact-info__form textarea` — `min-height: 10rem;` so the message field is "substantially taller" than the name/email inputs.
|
||||
- `.contact-info__form .gform_footer input[type="submit"], .contact-info__form .gform_footer button[type="submit"]` — same visual treatment as the header's Contact button: `class="button dark"` is added by the Gravity Forms markup if we set `gforms form settings → button class` to `button dark`; otherwise we apply `class="button dark"` via jQuery or a CSS rule that targets `.gform_footer input[type="submit"]` and forces the same look. The button gets the secondary-button gradient: `background: var(--background-image-button-gradient); color: var(--color-white); border: 3px solid var(--color-secondary); border-radius: 1.25rem 0.25rem 1.25rem 0.25rem; padding: 0.625rem 2rem; font-weight: 700;`. This matches the existing header Contact button shape.
|
||||
|
||||
## Static Assets
|
||||
|
||||
Three new SVG icons in `static/img/contact/`:
|
||||
|
||||
- `icon-address.svg` — map pin outline, 24×24 viewBox, `stroke="currentColor" fill="none" stroke-width="1.5"`.
|
||||
- `icon-email.svg` — envelope outline, 24×24, same stroke.
|
||||
- `icon-phone.svg` — phone outline, 24×24, same stroke.
|
||||
|
||||
The SVGs are sourced from the user (the spec says "will provide SVGs"). Until they arrive, the block renders empty `.contact-info__icon` spans so the layout is correct but the icons are blank — and the test suite can still verify the layout.
|
||||
|
||||
The Leaflet map pin (orange-01 + white) and the actual Leaflet embed code are not yet available. The right column is a styled placeholder with a comment indicating where Leaflet will mount.
|
||||
|
||||
## Data Flow
|
||||
|
||||
- **Editor flow**: on the contact page, the editor adds the existing `Contact Info` block. The block renders the static layout. The block has no per-instance fields; everything comes from the global `contact_info` option.
|
||||
- **Render flow**: `page.php` runs `the_content()` → the editor's `Contact Info` block renders → the block reads the global `contact_info` for address/email/phone → the block renders the Gravity Forms shortcode and the Leaflet placeholder.
|
||||
- **Gravity Forms shortcode**: `[gravityform id="1" title="false"]` (per the user). The `title="false"` and `description="false"` are added by the block's render template to suppress the form's title/description and let the block's own heading carry that role.
|
||||
|
||||
## Error Handling and Edge Cases
|
||||
|
||||
- `contact_info.address` empty → the address `<li>` still renders but the value span is empty. Acceptable for a placeholder; the editor should fill the global field.
|
||||
- `contact_info.email` empty → the email `<li>` is omitted (wraps the value in `if ( $email )`).
|
||||
- `contact_info.phone` empty → same, omit.
|
||||
- Gravity Form ID 1 not found → the `[gravityform]` shortcode returns an empty string. The form area shows an empty box. The editor sees a visible gap, which is the right signal to fix the form.
|
||||
- Leaflet embed not present → the right column is a gray placeholder. Layout is correct; the editor knows to drop in the embed.
|
||||
- Editor adds the `Contact Info` block to a non-contact page → the block renders the same layout. This is intentional; the block is reusable. Pages that need a different layout (e.g. a 404, a privacy policy) should not add this block.
|
||||
- Existing pages that use the `Contact Info` block with `InnerBlocks` (if any) lose the InnerBlocks area. A repo-wide search shows no such pages exist, but the change is breaking for any page that was using the block as a wrapper for other blocks. This is a known follow-up: audit all pages using `Contact Info` before merging.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- A new page template (`template-contact.php`). The block is the page.
|
||||
- A new ACF field group for the contact page.
|
||||
- Per-page override of the heading, the shortcode, or the icon assets.
|
||||
- The Leaflet embed itself (placeholder only).
|
||||
- The map pin SVG.
|
||||
- An audit of all existing pages using the `Contact Info` block (manual follow-up, see Error Handling).
|
||||
- Changes to the header, footer, or `page.php`.
|
||||
- The inner-page hero (already done, gated by `hero_style = 'default'`).
|
||||
|
||||
## Verification
|
||||
|
||||
- Add `tests/contact-page.spec.js` covering:
|
||||
- Desktop (1280px): the contact page renders with two columns; the left column has the heading, the address/email/phone rows (with icon spans), the Gravity Forms shortcode renders (or, if the form doesn't exist on the test env, the form area is empty — the test asserts the `.contact-info__form` element exists, not its contents), and the right column is the Leaflet placeholder.
|
||||
- Mobile (402px): single column; the map drops below the form; no horizontal overflow.
|
||||
- 320px: nothing clips; all text remains inside the viewport.
|
||||
- Axe scan: no violations.
|
||||
- Console errors: none.
|
||||
- Add `tests/services-hero-scope.spec.js` covering the re-scope:
|
||||
- The Services page itself renders the hero (assuming the fixture has `hero_style = 'default'`).
|
||||
- A child of the Services page (e.g. Communication Services) renders the hero.
|
||||
- A grandchild of the Services page renders the hero.
|
||||
- A page that is NOT a Services descendant (e.g. About, Contact) does NOT render the hero, even if its `hero_style = 'default'`.
|
||||
- A blog post, archive, search, or 404 still renders the hero (these views bypass the Services gate).
|
||||
- Use the existing contact page fixture (or seed one in `beforeAll` if no fixture exists, and tear down in `afterAll`).
|
||||
- Run `npm run build` to regenerate `static/dist/`.
|
||||
- Run the full Playwright suite (`npx playwright test`) including `tests/mobile-homepage.spec.js`, `tests/site-a11y.spec.js`, `tests/inner-page.spec.js`, and the two new specs to confirm no regressions.
|
||||
- Run `composer lint` to catch PHPCS issues.
|
||||
- Capture 1280px and 402px full-page screenshots for visual comparison with the mockup.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. The contact page (`/contact/`) renders the new two-column 50/50 layout.
|
||||
2. The left column has the heading, address, email, and phone (from the global `contact_info` option), each with a CWC Blue 03 outline icon to the left.
|
||||
3. The Gravity Forms shortcode `[gravityform id="1" title="false"]` renders below the address items, with custom field styling: uppercase CWC Blue 01 labels, CWC Orange 01 right-aligned required asterisks, 30% opacity CWC Blue 03 input fills with CWC Blue 03 borders, slightly rounded corners, comfortable padding, a substantially taller message textarea, and a submit button matching the header's Contact button.
|
||||
4. The right column is a placeholder for the Leaflet map, fills 50% of the viewport width on desktop (edge to edge), and drops below the form on mobile.
|
||||
5. No hero is rendered on the contact page. There is a ~3rem top margin below the header and no page title above the body.
|
||||
6. The inner-page hero renders on the Services page, on its direct children, and on its grandchildren — and ONLY on those pages. Pages that have `hero_style = 'default'` but are not Services descendants lose their hero.
|
||||
7. Blog posts, archives, search, and 404 still render their existing heroes (the Services gate does not apply to those views).
|
||||
8. No new console errors, axe violations, or PHPCS errors.
|
||||
9. Existing pages (Services children/grandchildren with the inner-page hero, single posts, archives, search, 404) render unchanged.
|
||||
10. Existing tests (`mobile-homepage`, `site-a11y`, `inner-page`) continue to pass.
|
||||
11. The block is reusable: adding the `Contact Info` block to another page renders the same layout.
|
||||
@@ -31,8 +31,11 @@ class Enqueue {
|
||||
*/
|
||||
public function enqFEAssets() {
|
||||
$theme_dir = get_stylesheet_directory();
|
||||
|
||||
$theme_uri = get_stylesheet_directory_uri();
|
||||
|
||||
$leaflet_version = '1.9.4';
|
||||
|
||||
/**
|
||||
* CSS
|
||||
*/
|
||||
@@ -42,6 +45,8 @@ class Enqueue {
|
||||
wp_enqueue_style( 'cwc-theme', $theme_uri . $css_path, array(), $version );
|
||||
}
|
||||
|
||||
wp_enqueue_style( 'leaflet', 'https://unpkg.com/leaflet@' . $leaflet_version . '/dist/leaflet.css', array(), $leaflet_version );
|
||||
|
||||
$font_ver = gmdate( 'U' );
|
||||
wp_enqueue_style( 'poppins', 'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap', false, $font_ver );
|
||||
wp_enqueue_style( 'quincy', 'https://use.typekit.net/dnt5chp.css', false, $font_ver );
|
||||
|
||||
Vendored
+2346
-3384
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Contact Info map module.
|
||||
*/
|
||||
|
||||
const MAP_SELECTOR = '.contact-info__map[data-lat][data-lng]';
|
||||
const DEFAULT_ZOOM = 14;
|
||||
const MARKER_SIZE = [62, 68];
|
||||
|
||||
let leafletModulePromise;
|
||||
|
||||
function loadLeaflet() {
|
||||
if (!leafletModulePromise) {
|
||||
leafletModulePromise = import('https://unpkg.com/leaflet@1.9.4/dist/leaflet-src.esm.js');
|
||||
}
|
||||
|
||||
return leafletModulePromise;
|
||||
}
|
||||
|
||||
function toFloat(value) {
|
||||
const parsed = Number.parseFloat(value);
|
||||
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
export async function initContactInfoMap() {
|
||||
const mapContainers = document.querySelectorAll(MAP_SELECTOR);
|
||||
|
||||
if (!mapContainers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Leaflet;
|
||||
|
||||
try {
|
||||
const module = await loadLeaflet();
|
||||
Leaflet = module.default || module;
|
||||
} catch (error) {
|
||||
console.error('Leaflet failed to load for contact map.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
mapContainers.forEach((container) => {
|
||||
if (container.dataset.mapReady === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const latitude = toFloat(container.dataset.lat);
|
||||
const longitude = toFloat(container.dataset.lng);
|
||||
const zoom = toFloat(container.dataset.zoom) || DEFAULT_ZOOM;
|
||||
|
||||
if (latitude === null || longitude === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const map = Leaflet.map(container, {
|
||||
zoomControl: false,
|
||||
scrollWheelZoom: false,
|
||||
dragging: false,
|
||||
doubleClickZoom: false,
|
||||
boxZoom: false,
|
||||
keyboard: false,
|
||||
tap: false,
|
||||
attributionControl: true,
|
||||
}).setView([latitude, longitude], zoom);
|
||||
|
||||
Leaflet.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
maxZoom: 19,
|
||||
subdomains: 'abcd',
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
|
||||
}).addTo(map);
|
||||
|
||||
const markerSvg = container.dataset.markerSvg;
|
||||
|
||||
const markerIcon = markerSvg
|
||||
? Leaflet.divIcon({
|
||||
className: 'contact-info__leaflet-pin',
|
||||
html: markerSvg,
|
||||
iconSize: MARKER_SIZE,
|
||||
iconAnchor: [MARKER_SIZE[0] / 2, MARKER_SIZE[1]],
|
||||
})
|
||||
: Leaflet.icon({
|
||||
iconUrl: container.dataset.marker || '',
|
||||
iconSize: MARKER_SIZE,
|
||||
iconAnchor: [MARKER_SIZE[0] / 2, MARKER_SIZE[1]],
|
||||
popupAnchor: [0, -MARKER_SIZE[1] + 8],
|
||||
className: 'contact-info__leaflet-pin'
|
||||
});
|
||||
|
||||
const marker = Leaflet.marker([latitude, longitude], { icon: markerIcon }).addTo(map);
|
||||
const popupText = container.dataset.popup;
|
||||
|
||||
if (popupText) {
|
||||
marker.bindPopup(popupText);
|
||||
}
|
||||
|
||||
container.dataset.mapReady = 'true';
|
||||
});
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import GetHeaderHeight from './modules/GetHeaderHeight.js';
|
||||
import tagExternalLinks from './modules/TagExternalLinks.js';
|
||||
import Navigation from './modules/Navigation.js';
|
||||
import initRecentPostsCarousels from './modules/RecentPostsCarousel.js';
|
||||
import { initContactInfoMap } from './modules/ContactInfoMap.js';
|
||||
|
||||
// Add passive event listeners
|
||||
! function (e) {
|
||||
@@ -72,6 +73,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize mobile Recent Insights carousels.
|
||||
initRecentPostsCarousels();
|
||||
|
||||
// Initialize Contact Info Leaflet maps.
|
||||
initContactInfoMap();
|
||||
|
||||
// Add Back to Top button to body
|
||||
// const backToTop = document.createElement('x-back-to-top');
|
||||
// document.body.appendChild(backToTop);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
--color-black: oklch(0% 0 0);
|
||||
--color-white: oklch(100% 0 0);
|
||||
|
||||
--color-background: oklch(89.75% 0 0);
|
||||
--color-background: oklch(100% 0 0);
|
||||
--color-text: var(--color-black);
|
||||
|
||||
--color-cwc-blue-01: oklch(47.31% 0.1141 242.9);
|
||||
|
||||
@@ -51,10 +51,16 @@
|
||||
|
||||
>a {
|
||||
@apply ml-2 mr-0 my-0;
|
||||
height: auto;
|
||||
|
||||
background: linear-gradient(93.03deg, #F26B53 0%, #D24D32 100%);
|
||||
border: 3px solid transparent;
|
||||
border-radius: 1.25rem 0.25rem;
|
||||
height: auto;
|
||||
padding: .5rem 2rem;
|
||||
|
||||
&:hover {
|
||||
border: 3px solid #D24D32;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/* Contact Info block styles */
|
||||
|
||||
main#maincontent {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
background: var(--color-white);
|
||||
padding-block: clamp(3rem, 6vw, 4rem);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -14,25 +17,27 @@
|
||||
}
|
||||
|
||||
.contact-info__details {
|
||||
padding: 5rem 0;
|
||||
max-width: 36rem;
|
||||
padding-inline: clamp(1.5rem, 5vw, 3rem);
|
||||
}
|
||||
|
||||
.contact-info__heading {
|
||||
color: var(--color-cwc-blue-01);
|
||||
font-family: var(--font-quincy, 'Quincy', serif);
|
||||
font-size: clamp(2.5rem, 6vw, 4.5rem);
|
||||
font-weight: 400;
|
||||
line-height: 1.05;
|
||||
margin: 0 0 1.5rem;
|
||||
@apply text-cwc-blue-02 font-bold text-34px/9;
|
||||
|
||||
margin: 0 0 1rem;
|
||||
|
||||
span { @apply text-cwc-blue-01; }
|
||||
|
||||
em { @apply text-secondary; }
|
||||
}
|
||||
|
||||
.contact-info__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 300;
|
||||
gap: 0.75rem;
|
||||
list-style: none;
|
||||
margin: 0 0 2.5rem;
|
||||
margin: 0 0 3rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -72,11 +77,49 @@ a.contact-info__value:hover {
|
||||
|
||||
.contact-info__map {
|
||||
background: color-mix(in oklch, var(--color-cwc-blue-03) 30%, white);
|
||||
border-radius: 0.5rem;
|
||||
height: 100%;
|
||||
min-height: 24rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contact-info__map .leaflet-container {
|
||||
height: 100%;
|
||||
isolation: isolate;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contact-info__map .leaflet-tile-pane {
|
||||
filter: grayscale(0.35) saturate(0.7) brightness(1.05) contrast(0.98);
|
||||
}
|
||||
|
||||
.contact-info__map::after {
|
||||
background: color-mix(in oklch, var(--color-cwc-blue-03) 30%, transparent);
|
||||
content: '';
|
||||
inset: 0;
|
||||
mix-blend-mode: normal;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 410;
|
||||
}
|
||||
|
||||
.contact-info__map .leaflet-control-attribution {
|
||||
color: color-mix(in oklch, var(--color-cwc-blue-01) 45%, black);
|
||||
font-size: 0.625rem;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.contact-info__leaflet-pin {
|
||||
display: block;
|
||||
filter: drop-shadow(0 0.2rem 0.45rem rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
|
||||
.contact-info__leaflet-pin svg {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.contact-info__grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -112,11 +155,11 @@ a.contact-info__value:hover {
|
||||
.contact-info__form .gform_wrapper {
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
|
||||
.gform_required_legend { display: none; }
|
||||
}
|
||||
|
||||
.contact-info__form .gfield {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.contact-info__form .gfield { margin-bottom: 1.25rem; }
|
||||
|
||||
.contact-info__form .gfield_label {
|
||||
color: var(--color-cwc-blue-01);
|
||||
@@ -143,12 +186,12 @@ a.contact-info__value:hover {
|
||||
.contact-info__form textarea,
|
||||
.contact-info__form select {
|
||||
background: color-mix(in oklch, var(--color-cwc-blue-03) 30%, white);
|
||||
border: 1px solid var(--color-cwc-blue-03);
|
||||
border-radius: 0.375rem;
|
||||
border: 2px solid var(--color-cwc-blue-03);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--color-dark);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
padding: 0.75rem 1rem;
|
||||
line-height: 1;
|
||||
padding: 0.25rem 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -165,6 +208,7 @@ a.contact-info__value:hover {
|
||||
|
||||
.contact-info__form textarea {
|
||||
min-height: 10rem;
|
||||
padding: 0.5rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
@@ -175,8 +219,8 @@ a.contact-info__value:hover {
|
||||
.contact-info__form .gform_footer input[type="submit"],
|
||||
.contact-info__form .gform_footer button[type="submit"] {
|
||||
background: var(--background-image-button-gradient);
|
||||
border: 3px solid var(--color-secondary);
|
||||
border-radius: 1.25rem 0.25rem 1.25rem 0.25rem;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 1.25rem 0.25rem;
|
||||
color: var(--color-white);
|
||||
cursor: pointer;
|
||||
font-size: 1.125rem;
|
||||
@@ -187,12 +231,5 @@ a.contact-info__value:hover {
|
||||
|
||||
.contact-info__form .gform_footer input[type="submit"]:hover,
|
||||
.contact-info__form .gform_footer button[type="submit"]:hover {
|
||||
background: var(--color-secondary);
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.contact-info__form .validation_message {
|
||||
color: var(--color-danger);
|
||||
font-style: italic;
|
||||
margin-top: 0.25rem;
|
||||
border: 3px solid #D24D32;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* Block Name: Contact Info
|
||||
*
|
||||
* Two-column contact page layout: contact info, address/email/phone,
|
||||
* and a Gravity Forms contact form on the left; Leaflet map placeholder
|
||||
* on the right. Reads from the global `contact_info` option.
|
||||
* and a Gravity Forms contact form on the left; Leaflet map on the right.
|
||||
* Contact details read from global `contact_info`; map coordinates/zoom are
|
||||
* block-level ACF fields.
|
||||
*
|
||||
* @package CWC
|
||||
*/
|
||||
@@ -16,16 +17,40 @@ $address = $info['address'] ?? '';
|
||||
$email = $info['email'] ?? '';
|
||||
$phone = $info['phone'] ?? '';
|
||||
|
||||
$classes = 'contact-info';
|
||||
$heading = get_field( 'heading' );
|
||||
|
||||
$map_lat_value = get_field( 'map_latitude' );
|
||||
$map_lng_value = get_field( 'map_longitude' );
|
||||
$map_zoom_value = get_field( 'map_zoom' );
|
||||
|
||||
$map_lat = is_numeric( $map_lat_value ) ? (float) $map_lat_value : 49.8951;
|
||||
$map_lng = is_numeric( $map_lng_value ) ? (float) $map_lng_value : -97.1384;
|
||||
$map_zoom = is_numeric( $map_zoom_value ) ? (int) $map_zoom_value : 14;
|
||||
$map_zoom = max( 1, min( 19, $map_zoom ) );
|
||||
|
||||
$map_popup = $address ? wp_strip_all_tags( $address ) : 'Our office';
|
||||
|
||||
$marker_svg_path = get_theme_file_path( '/views/blocks/contact-info/map-marker.svg' );
|
||||
$map_marker_svg = '';
|
||||
|
||||
if ( file_exists( $marker_svg_path ) ) {
|
||||
$map_marker_svg = file_get_contents( $marker_svg_path ); // phpcs:ignore
|
||||
}
|
||||
|
||||
$map_marker = get_theme_file_uri( '/views/blocks/contact-info/map-marker.svg' );
|
||||
|
||||
$classes = 'contact-info mx-break-out';
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
?>
|
||||
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?>>
|
||||
<div class="contact-info__grid">
|
||||
<div class="contact-info__details">
|
||||
<h1 class="contact-info__heading">Contact</h1>
|
||||
<div class="container contact-info__details pt-20">
|
||||
<?php if ( $heading ) : ?>
|
||||
<h1 class="contact-info__heading"><?php echo wp_kses_post( $heading ); ?></h1>
|
||||
<?php endif; ?>
|
||||
|
||||
<ul class="contact-info__items">
|
||||
<ul class="contact-info__items">
|
||||
<?php if ( $address ) : ?>
|
||||
<li class="contact-info__item contact-info__item--address">
|
||||
<span class="contact-info__icon" aria-hidden="true">
|
||||
@@ -55,12 +80,23 @@ $wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
</ul>
|
||||
|
||||
<div class="contact-info__form">
|
||||
<?php echo do_shortcode( '[gravityform id="1" title="false" description="false" ajax="true"]' ); ?>
|
||||
</div>
|
||||
<InnerBlocks />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-info__map" id="contact-map" aria-label="Map of downtown Winnipeg" role="region">
|
||||
<!-- Leaflet map will mount here. Pin: orange-01 + white map marker (TBD). -->
|
||||
<div
|
||||
class="contact-info__map"
|
||||
id="contact-map"
|
||||
aria-label="Map of downtown Winnipeg"
|
||||
role="region"
|
||||
data-lat="<?php echo esc_attr( $map_lat ); ?>"
|
||||
data-lng="<?php echo esc_attr( $map_lng ); ?>"
|
||||
data-zoom="<?php echo esc_attr( $map_zoom ); ?>"
|
||||
data-popup="<?php echo esc_attr( $map_popup ); ?>"
|
||||
data-marker="<?php echo esc_url( $map_marker ); ?>"
|
||||
data-marker-svg="<?php echo esc_attr( $map_marker_svg ); ?>"
|
||||
>
|
||||
<!-- Leaflet map mounts in this container. -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg width="79" height="86" viewBox="0 0 79 86" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.5 0C49.9547 0 59.9945 4.11839 67.4072 11.4697C74.8221 18.8233 79 28.8109 79 39.2393C79 52.9415 71.4913 63.5903 64.6484 70.5264L64.6328 70.541L64.6172 70.5566C61.1816 74.0014 57.4333 77.1243 53.4209 79.8848L53.3926 79.9043L53.3633 79.9238L51.8721 80.9307L51.8369 80.9551L51.8008 80.9785L51.1006 81.4404L51.0342 81.4844L50.9668 81.5264L49.6475 82.3594L49.583 82.4004L49.5176 82.4395L48.3418 83.1514L48.2705 83.1943L48.1982 83.2363L46.7422 84.0762L46.7139 84.0918L46.6855 84.1084C44.4927 85.3497 42.0162 86 39.5 86C36.9838 86 34.5073 85.3497 32.3145 84.1084L32.2861 84.0918L32.2578 84.0762L30.8018 83.2363L30.7158 83.1865L30.6318 83.1348L28.8115 82.0244L28.7256 81.9717L28.6406 81.916L27.9678 81.4824L27.8994 81.4375L26.4639 80.4902L26.415 80.457C22.0909 77.5556 18.0623 74.2411 14.3867 70.5605L14.3506 70.5244C7.51024 63.587 4.87832e-06 52.9422 0 39.2393C0 28.8109 4.17793 18.8233 11.5928 11.4697C19.0055 4.11839 29.0453 0 39.5 0Z" fill="url(#paint0_linear_2533_1456)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.5 8C47.8543 8 55.8665 11.2913 61.7739 17.1498C67.6813 23.0083 71 30.9541 71 39.2392C71 49.9092 65.134 58.6423 58.953 64.9075C55.8649 68.0039 52.4952 70.8111 48.887 73.2935L47.396 74.3001L46.696 74.7617L45.3765 75.5948L44.2005 76.3063L42.7445 77.1463C41.7562 77.7058 40.6379 78 39.5 78C38.3621 78 37.2438 77.7058 36.2555 77.1463L34.7995 76.3063L32.9795 75.1956L32.3075 74.7617L30.8725 73.8141C26.9799 71.2023 23.3542 68.2192 20.047 64.9075C13.866 58.6388 8 49.9092 8 39.2392C8 30.9541 11.3187 23.0083 17.2261 17.1498C23.1335 11.2913 31.1457 8 39.5 8ZM39.5 14.9421C33.0022 14.9421 26.7705 17.5019 22.1759 22.0585C17.5812 26.6151 15 32.7952 15 39.2392C15 47.299 19.452 54.3729 25.0485 60.0515C27.4549 62.4673 30.0558 64.6846 32.8255 66.6812L34.4285 67.8127C34.9465 68.1714 35.4447 68.5046 35.923 68.8124L37.288 69.6802L38.4885 70.4056L39.5 70.9922L41.0925 70.0585L42.377 69.2602C43.0607 68.8298 43.7922 68.3473 44.5715 67.8127L46.1745 66.6812C48.9442 64.6846 51.5451 62.4673 53.9515 60.0515C59.548 54.3764 64 47.299 64 39.2392C64 32.7952 61.4188 26.6151 56.8241 22.0585C52.2295 17.5019 45.9978 14.9421 39.5 14.9421ZM39.5 25.3551C43.213 25.3551 46.774 26.8179 49.3995 29.4217C52.025 32.0255 53.5 35.5569 53.5 39.2392C53.5 42.9215 52.025 46.453 49.3995 49.0568C46.774 51.6606 43.213 53.1234 39.5 53.1234C35.787 53.1234 32.226 51.6606 29.6005 49.0568C26.975 46.453 25.5 42.9215 25.5 39.2392C25.5 35.5569 26.975 32.0255 29.6005 29.4217C32.226 26.8179 35.787 25.3551 39.5 25.3551ZM39.5 32.2972C37.6435 32.2972 35.863 33.0286 34.5503 34.3305C33.2375 35.6324 32.5 37.3981 32.5 39.2392C32.5 41.0804 33.2375 42.8461 34.5503 44.148C35.863 45.4499 37.6435 46.1813 39.5 46.1813C41.3565 46.1813 43.137 45.4499 44.4497 44.148C45.7625 42.8461 46.5 41.0804 46.5 39.2392C46.5 37.3981 45.7625 35.6324 44.4497 34.3305C43.137 33.0286 41.3565 32.2972 39.5 32.2972Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2533_1456" x1="0" y1="0" x2="82.9794" y2="4.02829" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F26B53"/>
|
||||
<stop offset="1" stop-color="#D24D32"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
Reference in New Issue
Block a user