Jake Vanderwerf
2026-05-03 9bbeea742424837fb58207d88e10dbca0b2cae04
=SEO Field registration and formatting
15 files modified
1062 ■■■■ changed files
inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php 6 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php 102 ●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/alternateNameTrait.php 18 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/amenityFeatureTrait.php 63 ●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/awardTrait.php 55 ●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/dissolutionDateTrait.php 35 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php 103 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/keywordsTrait.php 27 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/offersTrait.php 173 ●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php 66 ●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/paymentAcceptedTrait.php 33 ●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/photoTrait.php 24 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/reviewTrait.php 156 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/sloganTrait.php 13 ●●●●● patch | view | raw | blame | history
inc/registrar/Fields.php 188 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php
@@ -38,6 +38,12 @@
                return property_exists($property, 'value') && !empty($property->value);
            });
        }
        if (!empty($this->additionalProperty)) {
            if (!is_array($this->additionalProperty)) {
                $this->additionalProperty = [$this->additionalProperty];
            }
            $additionalProperty = array_merge($this->additionalProperty, $additionalProperty);
        }
        $this->additionalProperty = $additionalProperty;
    }
inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php
@@ -3,6 +3,8 @@
use JVBase\base\SchemaHelper;
use JVBase\managers\SEO\render\Thing\Intangible\Rating\AggregateRating;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
@@ -29,32 +31,94 @@
        $this->aggregateRating = $aggregateRating;
    }
    public function getAggregateRatingFieldConfig():array
    public function setAggregateRatingField(Fields $fields):void
    {
        return [
            'type'  => 'group',
            'label' => 'Aggregate/Average Rating',
            'wrap'  => 'details',
            'fields'    => [
                'ratingCount'   => [
                    'type'  => 'number',
                    'label' => 'The total number of ratings (without text)',
        $fields->addField('average_rating', [
            'type'  => 'select',
            'label' => __('Average Rating', 'jvb'),
            'options'   => [
                'none'  => 'None',
                '0.5'   => '0.5',
                '1'     => '1',
                '1.5'   => '1.5',
                '2'     => '2',
                '2.5'   => '2.5',
                '3'     => '3',
                '3.5'   => '3.5',
                '4'     => '4',
                '4.5'   => '4.5',
                '5'     => '5',
                ],
                'reviewCount'   => [
            'default'   => 'none',
        ]);
        $fields->addField('ratingCount', [
                    'type'  => 'number',
                    'label' => 'The total number of reviews (with text)'
                ],
                'bestRating'    => [
            'label' => __('The total number of ratings', 'jvb'),
            'condition' => [
                'field'     => 'average_rating',
                'operator'  => '!=',
                'value'     => 'none',
            ]
        ]);
        $fields->addField('reviewCount', [
                    'type'  => 'number',
                    'label' => 'The best rating',
            'label' => __('The total number of reviews (with text)', 'jvb'),
            'condition' => [
                'field'     => 'average_rating',
                'operator'  => '!=',
                'value'     => 'none',
            ]
        ]);
        $fields->addField('bestRating', [
            'type'  => 'number',
            'label' => __('The best possible rating value (top of scale)', 'jvb'),
                    'default'=> 5,
                ],
                'worstRating'   => [
            'condition' => [
                'field'     => 'average_rating',
                'operator'  => '!=',
                'value'     => 'none',
            ]
        ]);
        $fields->addField('worstRating', [
                    'type'  => 'number',
                    'label' => 'The worst rating',
            'label' => __('The worst possible rating value (bottom of scale)', 'jvb'),
                    'default'=> 1,
            'condition' => [
                'field'     => 'average_rating',
                'operator'  => '!=',
                'value'     => 'none',
                ]
            ]
        ];
        ]);
    }
    public function formatAggregateRatingField(Meta $meta):void
    {
        [$average, $ratingCount, $reviewCount, $bestRating, $worstRating] = $meta->getAll([
            'average_rating',
            'ratingCount',
            'reviewCount',
            'bestRating',
            'worstRating'
        ]);
        if (!empty($average)) {
            $rating = new AggregateRating();
            $rating->setRatingValue((float)$average);
            if (!empty($ratingCount)) {
                $rating->setRatingCount($ratingCount);
            }
            if (!empty($reviewCount)){
                $rating->setReviewCount($reviewCount);
            }
            if ($bestRating !== 5) {
                $rating->setBestRating($bestRating);
            }
            if ($worstRating !== 1) {
                $rating->setWorstRating($worstRating);
            }
            $this->setAggregateRating($rating);
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/alternateNameTrait.php
@@ -2,6 +2,7 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
@@ -27,4 +28,21 @@
        }
        $this->alternateName = $alternateName;
    }
    public function setAlternateNameField(Fields $fields):void
    {
        $fields->addField(
            'alternate_name',
            [
                'type'    => 'repeater',
                'label'   => 'Alternate Name',
                'fields'  => [
                    'name' => [
                        'type'  => 'text',
                        'label' => 'Name',
                    ]
                ]
            ]
        );
    }
}
inc/managers/SEO/render/Traits/_Properties/amenityFeatureTrait.php
@@ -2,7 +2,10 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\LocationFeatureSpecification;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PropertyValue;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
@@ -26,15 +29,61 @@
        $this->amenityFeature = $amenityFeature;
    }
    public function getAmenityFeatureFieldConfig():array
    public function setAmenityFeatureField(Fields $fields):void
    {
        return [
            'type'  => 'repeater',
            'label' => 'Amenity Feature',
            'hint'  => 'An amenity feature (e.g. a characteristic or service) of the Accommodation. This generic property does not make a statement about whether the feature is included in an offer for the main accommodation or available at extra costs.',
            'fields'    => [
        $fields->addField('amenityFeature', [
            'type'  => 'set',
            'label' => __('Amenities', 'jvb'),
            'options' => [
                'Wheelchair Accessible' => 'Wheelchair Accessible',
                'Free Parking' => 'Free Parking',
                'Private Rooms' => 'Private Rooms',
                'Air Conditioning' => 'Air Conditioning',
                'WiFi' => 'WiFi',
                'Gender Neutral Restroom' => 'Gender Neutral Restroom',
                'LGBTQ+ Friendly' => 'LGBTQ+ Friendly',
                'Sterilization Room' => 'Sterilization Room',
                'Refreshments Available' => 'Refreshments Available',
                'Street Level Access' => 'Street Level Access',
                'Single Use Needles' => 'Single Use Needles',
                'Consultation Room' => 'Consultation Room',
                'Aftercare Products Available' => 'Aftercare Products Available',
                'Walk-Ins Welcome' => 'Walk-Ins Welcome',
            ]
        ]);
    }
    public function formatAmenityFeatureField(Meta $meta):void
    {
        $amenities = $meta->get('amenityFeature');
        $properties = [
            'Walk-Ins Welcome',
            'LGBTQ+ Friendly',
        ];
        if (!empty($amenities)) {
            $out = [];
            $prop = [];
            foreach ($amenities as $amenity) {
                if (in_array($amenity, $properties)) {
                    $pr = new PropertyValue();
                    $pr->setName($amenity);
                    $pr->setValue(true);
                    $prop[] = $pr;
                } else {
                    $am = new LocationFeatureSpecification();
                    $am->setName($amenity);
                    $am->setValue(true);
                    $out[] = $am;
                }
            }
            if (!empty($out)) {
                $this->setAmenityFeature($out);
            }
            if (!empty($prop)) {
                $this->setAdditionalProperty($prop);
            }
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/awardTrait.php
@@ -2,6 +2,8 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
@@ -24,19 +26,56 @@
        }
        $this->award = $award;
    }
    public function getAwardFieldConfig():array
    public function setAwardField(Fields $fields):void
    {
        return [
            'type'      => 'repeater',
            'label'     => 'Award(s)',
        $fields->addField('awards', [
            'type'  => 'tagList',
            'label' => __('Award(s)', 'jvb'),
            'hint'      => 'List any awards won by this Thing.',
            'fields'    => [
                'award' => [
                'name'  => [
                    'type'  => 'text',
                    'label' => 'Award',
                    'hint'  => 'Include the award name, year, and presenter (if any)'
                    'label' => __('Award Name', 'jvb'),
                    'required'  => true,
                ],
                'presenter' => [
                    'type'  => 'text',
                    'label' => __('Presenter', 'jvb'),
                ],
                'year'  => [
                    'type'  => 'number',
                    'label' => __('Year', 'jvb')
                ]
            ]
        ];
        ]);
    }
    public function formatAwardField(Meta $meta):void
    {
        $awards = $meta->get('awards');
        $out = [];
        if (!empty($awards)) {
            foreach ($awards as $a) {
                $award = false;
                if (!empty($a['name'])) {
                    $award = '"'.$a['name'].'"';
                }else {
                    continue;
                }
                if (!empty($a['presenter'])) {
                    $award .= ', presented by '.$a['presenter'];
                }
                if (!empty($a['year'])) {
                    $award .= ' - '.$a['year'];
                }
                if ($award) {
                    $out[] = $award;
                }
            }
        }
        if (!empty ($out)) {
            $this->setAward($out);
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/dissolutionDateTrait.php
@@ -2,6 +2,8 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\DataType\Date;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
@@ -31,4 +33,37 @@
            'hint'  => 'IMPORTANT: Do not fill this out, unless your business has actually closed.'
        ];
    }
    public function setDissolutionDateField(Fields $fields):void
    {
        $fields->addField(
            'permanently_close',
            [
                'type'  => 'true_false',
                'label' => __('Permanently Close', 'jvb'),
                'hint'  => '*IMPORTANT* This signals to search engines that this business is no longer in business. Use only if your shop is closing!',
            ]
        );
        $fields->addField(
            'dissolution_date',
            [
                'type'  => 'date',
                'label' => __('Dissolution Date', 'jvb'),
                'condition' => [
                    'field'     => 'permanently_close',
                    'operator'  => '==',
                    'value'     => true,
                ]
            ]
        );
    }
    public function formatDissolutionDateField(Meta $meta):void
    {
        [$closed, $dissolution] = $meta->getAll(['permanently_close', 'dissolution_date']);
        if (!empty($closed) && $closed === true) {
            if (!empty($dissolution)) {
                $this->setDissolutionDate($dissolution);
            }
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php
@@ -3,6 +3,12 @@
use JVBase\base\SchemaHelper;
use JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList\OfferCatalog;
use JVBase\managers\SEO\render\Thing\Intangible\Offer;
use JVBase\managers\SEO\render\Thing\Intangible\Service;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PriceSpecification;
use JVBase\managers\SEO\render\Thing\Product\Product;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
use JVBase\registrar\Registrar;
if (!defined('ABSPATH')) {
@@ -97,4 +103,101 @@
            ]
        ];
    }
    public function setHasOfferCatalogField(Fields $fields):void
    {
        $fields->addField(
            'hasOfferCatalog',
            [
                'type'  => 'repeater',
                'label' => 'Main Products & Services',
                'fields'    => [
                    'type'  => [
                        'type'  => 'radio',
                        'label' => __('Select Type', 'jvb'),
                        'options'   => [
                            'product'   => 'Product',
                            'service'   => 'Service'
                        ],
                        'required' => true,
                    ],
                    'name'  => [
                        'type'  => 'text',
                        'label' => __('Name of Product or Service', 'jvb'),
                        'required'  => true,
                    ],
                    'description'   => [
                        'type'  => 'textarea',
                        'label' => __('Description (optional)', 'jvb'),
                    ],
                    'price' => [
                        'type'  => 'text',
                        'subtype'   => 'number',
                        'label'     => __('Price', 'jvb'),
                    ],
                    'unitText'  => [
                        'type'  => 'radio',
                        'label' => 'Price per unit',
                        'options'   => [
                            'hour'  => 'Hour',
                            'unit'  => 'Unit',
                        ],
                        'default'   => 'unit',
                    ]
                ],
            ]
        );
    }
    public function formatHasOfferCatalogField(Meta $meta):void
    {
        $catalog = $meta->get('hasOfferCatalog');
        if (!empty($catalog)) {
            $offerCatalog = [];
            $name = '';
            $services = array_filter($catalog, function ($item) {
                return $item['type'] === 'service';
            });
            $products =array_filter($catalog, function ($item) {
                return $item['type'] === 'product';
            });
            if (count($products) > 0) {
                $name = 'Products';
                if (count($services) > 0) {
                    $name .= ' & ';
                }
            }
            if (count($services) > 0) {
                $name .= 'Services';
            }
            foreach ($catalog as $row) {
                $offer = new Offer();
                $item = match($row['type']) {
                    'product'   => new Product(),
                    'service'   => new Service(),
                };
                $item->setName($row['name']);
                if (!empty($row['description'])) {
                    $item->setDescription($row['description']);
                }
                if (!empty($row['price'])) {
                    $price = new PriceSpecification();
                    $price->setPrice($row['price']);
                    $price->setPriceCurrency('CAD');
                    $price->setUnitText($row['unitText']);
                    $offer->setPriceSpecification($price);
                }
                $offer->setItemOffered($item);
                $offerCatalog[] =  $offer;
            }
            if (!empty(!$offerCatalog)) {
                $final = new OfferCatalog();
                $final->setName($name);
                $final->setItemListElement($offerCatalog);
                $this->sethasOfferCatalog($final);
            }
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/keywordsTrait.php
@@ -3,6 +3,8 @@
use JVBase\managers\SEO\render\Thing\Intangible\DefinedTerm;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
@@ -25,4 +27,29 @@
        }
        $this->keywords = $keywords;
    }
    public function setKeywordsField(Fields $fields):void
    {
        $fields->addField(
            'keywords',
            [
                'type'      => 'repeater',
                'label'     => 'Keywords',
                'fields'    => [
                    'keyword' => [
                        'type'  => 'text',
                        'label' => 'Keyword',
                    ],
                ],
            ]
        );
    }
    public function formatKeywordsField(Meta $meta):void
    {
        $keywords = $meta->get('keywords');
        if (!empty($keywords)) {
            $this->setKeywords($keywords);
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/offersTrait.php
@@ -1,9 +1,17 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList\OfferCatalog;
use JVBase\managers\SEO\render\Thing\Intangible\Demand;
use JVBase\managers\SEO\render\Thing\Intangible\Offer;
use JVBase\managers\SEO\render\Thing\Intangible\Service;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PriceSpecification;
use JVBase\managers\SEO\render\Thing\Product\Product;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
use JVBase\registrar\Registrar;
use WP_Query;
if (!defined('ABSPATH')) {
    exit;
@@ -30,34 +38,155 @@
        }
        $this->offers = $offers;
    }
    public function getOpeningHoursFieldConfig():array
    public function setOffersField(Fields $fields):void
    {
        return [
        $fields->addField('generate_offers_from_content',
        [
            'type'  => 'true_false',
            'label' => __('Generate Offers from Content', 'jvb'),
        ]);
        $fields->addField('seo_offers', [
            'type'  => 'repeater',
            'label' => 'Opening Hours',
            'label' => __('Offers', 'jvb'),
            'condition' => [
                'field' => 'generate_offers_from_content',
                'value' => true,
                'operator'  => '=='
            ],
            'fields'    => [
                'dayOfWeek' => [
                'type'  => [
                    'type'  => 'radio',
                    'label' => 'Day(s) of Week',
                    'label' => __('Select Type', 'jvb'),
                    'options'   => [
                        'Mo'    => 'Monday',
                        'Tu'    => 'Tuesday',
                        'We'    => 'Wednesday',
                        'Th'    => 'Thursday',
                        'Fr'    => 'Friday',
                        'Sa'    => 'Saturday',
                        'Su'    => 'Sunday'
                        'product'   => 'Product',
                        'service'   => 'Service'
                    ],
                    'required' => true,
                ],
                'name'  => [
                    'type'  => 'text',
                    'label' => __('Name of Product or Service', 'jvb'),
                    'required'  => true,
                ],
                'description'   => [
                    'type'  => 'textarea',
                    'label' => __('Description (optional)', 'jvb'),
                ],
                'price' => [
                    'type'  => 'text',
                    'subtype'   => 'number',
                    'label'     => __('Price', 'jvb'),
                ],
                'unitText'  => [
                    'type'  => 'radio',
                    'label' => 'Price per unit',
                    'options'   => [
                        'hour'  => 'Hour',
                        'unit'  => 'Unit',
                    ],
                    'default'   => 'unit',
                    ]
                ],
                'opens' => [
                    'type'  => 'time',
                    'label' => 'Opens At',
                ],
                'closes'    => [
                    'type'  => 'time',
                    'label' => 'Closes At',
                ]
            ]
        ];
        ]);
    }
    public function formatOffersField(Meta $meta):void
    {
        $generate = $meta->get('generate_offers_from_content');
        if (!empty($generate) && $generate === true) {
            $this->generateOffersFromContent($meta);
            return;
        }
        $catalog = $meta->get('seo_offers');
        if (!empty($catalog)) {
            $offerCatalog = [];
            $name = '';
            $services = array_filter($catalog, function ($item) {
                return $item['type'] === 'service';
            });
            $products =array_filter($catalog, function ($item) {
                return $item['type'] === 'product';
            });
            if (count($products) > 0) {
                $name = 'Products';
                if (count($services) > 0) {
                    $name .= ' & ';
                }
            }
            if (count($services) > 0) {
                $name .= 'Services';
            }
            foreach ($catalog as $row) {
                $offer = new Offer();
                $item = match($row['type']) {
                    'product'   => new Product(),
                    'service'   => new Service(),
                };
                $item->setName($row['name']);
                if (!empty($row['description'])) {
                    $item->setDescription($row['description']);
                }
                if (!empty($row['price'])) {
                    $price = new PriceSpecification();
                    $price->setPrice($row['price']);
                    $price->setPriceCurrency('CAD');
                    $price->setUnitText($row['unitText']);
                    $offer->setPriceSpecification($price);
                }
                $offer->setItemOffered($item);
                $offerCatalog[] =  $offer;
            }
            if (!empty(!$offerCatalog)) {
                $this->setOffers($offerCatalog);
            }
        }
    }
    /**
     * This presumes that the Meta instance being passed is for a profile content type.
     * @param Meta $meta
     * @return void
     */
    public function generateOffersFromContent(Meta $meta):void
    {
        $userID = (int)get_post_meta($meta->id(), BASE.'profile_link', true);
        if (empty($userID)) {
            return;
        }
        $user = get_userdata($userID);
        if (!$user) {
            return;
        }
        $registrar = Registrar::getInstance(jvbUserRole($userID));
        if (!$registrar) {
            return;
        }
        $types = $registrar->getCreatable();
        if (empty($types)) {
            return;
        }
        $offers = [];
        $cities = $meta->get('city');
        $rate = $meta->get('rate');
        $reviews = $meta->get('reviews');
        $rating = $meta->get('average_rating');
        $theCities = $theRate = $theReviews = $theRating = null;
        foreach($types as $type) {
            $based = jvbCheckBase($type);
            $hasAny = new WP_Query([
                'post_type'     => $based,
                'posts_per_page'=> 1,
                'fields'        => 'ids',
                'post_author'   => $userID
            ]);
            if ($hasAny->have_posts()) {
                $offer = new Offer();
                $typeRegistrar = Registrar::getInstance($type);
            }
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php
@@ -4,6 +4,8 @@
use JVBase\base\SchemaHelper;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\OpeningHoursSpecification;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
@@ -41,15 +43,15 @@
        $this->openingHoursSpecification = $openingHoursSpecification;
    }
    public function getOpeningHoursSpecificationFieldConfig():array
    public function setOpeningHoursSpecificationField(Fields $fields):void
    {
        return [
        $fields->addField('openingHours', [
            'type'  => 'repeater',
            'label' => 'Opening Hours',
            'label' => __('Opening Hours', 'jvb'),
            'fields'    => [
                'dayOfWeek' => [
                    'type'  => 'radio',
                    'label' => 'Day(s) of Week',
                    'type'      => 'set',
                    'label'     => __('Day(s) of Week', 'jvb'),
                    'options'   => [
                        'Mo'    => 'Monday',
                        'Tu'    => 'Tuesday',
@@ -58,17 +60,65 @@
                        'Fr'    => 'Friday',
                        'Sa'    => 'Saturday',
                        'Su'    => 'Sunday'
                    ]
                    ],
                    'required'  => true
                ],
                'opens' => [
                    'type'  => 'time',
                    'label' => 'Opens At',
                    'label' => __('Opens at', 'jvb'),
                    'required'  => true
                ],
                'closes'    => [
                    'type'  => 'time',
                    'label' => 'Closes At',
                    'label' => __('Closes at', 'jvb'),
                    'required'  => true
                ]
            ]
        ]);
        $fields->addField('by_appointment', [
            'type'  => 'true_false',
            'label' => __('By Appointment Only', 'jvb'),
        ]);
        $fields->addField('allow_walkins', [
            'type'  => 'true_false',
            'label' => __('Walk Ins Welcome', 'jvb')
        ]);
    }
    public function formatOpeningHoursSpecificationField(Meta $meta):void
    {
        $openingHours = $meta->get('openingHours');
        if (!empty($openingHours)) {
            $used = [
                'Mo'    => false,
                'Tu'    => false,
                'We'    => false,
                'Th'    => false,
                'Fr'    => false,
                'Sa'    => false,
                'Su'    => false,
        ];
            $hours = [];
            foreach ($openingHours as $row) {
                $days = array_filter(explode(',', $row['dayOfWeek']),
                    function ($d) use ($used) {
                        if ($used[$d] === false) {
                            $used[$d] = true;
                            return true;
                        }
                        return false;
                    });
                if (empty($days)) {
                    continue;
                }
                $opens = new OpeningHoursSpecification();
                $opens->setDayOfWeek($days);
                $opens->setOpens($row['opens']);
                $opens->setCloses($row['closes']);
            }
            if (!empty($hours)){
                $this->setOpeningHoursSpecification($hours);
            }
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/paymentAcceptedTrait.php
@@ -2,6 +2,8 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
@@ -25,12 +27,31 @@
        $this->paymentAccepted = $paymentAccepted;
    }
    public function getPaymentAcceptedFieldConfig():array
    public function setPaymentAcceptedField(Fields $fields):void
    {
        return [
            'type'  => 'string',
            'label' => 'Payment Accepted',
            'hint'  => 'A comma separated list of payment accepted, example: Cash, Credit Card, Cryptocurrency, Local Exchange Tradings System, etc.'
        ];
        $fields->addField(
            'payment_accepted',
            [
                'type'  => 'set',
                'label' => __('Payment Accepted', 'jvb'),
                'options'   => [
                    'Cash'          => 'Cash',
                    'Credit Card'   => 'Credit Card',
                    'Debit'         => 'Debit',
                    'Google Pay'    => 'Google Pay',
                    'Apple Pay'     => 'Apple Pay',
                    'PayPal'        => 'PayPal',
                    'Interac'       => 'Interac',
                    'AMEX'          => 'AMEX',
                ],
            ]
        );
    }
    public function formatPaymentAcceptedField(Meta $meta):void
    {
        $accepted = $meta->get('payment_accepted');
        if (!empty($accepted)){
            $this->setPaymentAccepted($accepted);
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/photoTrait.php
@@ -3,6 +3,8 @@
use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject;
use JVBase\managers\SEO\render\Thing\CreativeWork\Photograph;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
@@ -31,4 +33,26 @@
            'hint'  => 'A photograph of this place.'
        ];
    }
    public function setPhotoField(Fields $fields):void
    {
        $fields->addField(
            'outside_photo',
            [
                'type'  => 'image',
                'limit' => 1,
                'label' => __('Outside Photo', 'jvb')
            ]
        );
    }
    public function formatPhotoField(Meta $meta):void
    {
        $image = $meta->get('outside_photo');
        if (!empty($image) && $image > 0) {
            $image = $this->createImageFromID($image);
            if ($image) {
                $this->setPhoto($image);
            }
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/reviewTrait.php
@@ -2,7 +2,11 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\CreativeWork\Review;
use JVBase\managers\SEO\render\Thing\Intangible\Rating\Rating;
use JVBase\managers\SEO\render\Thing\Person\Person;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
use JVBase\meta\Meta;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
@@ -64,4 +68,156 @@
            ]
        ];
    }
    public function setReviewField(Fields $fields):void
    {
        $fields->addField(
            'enable_gmb_review_sync',
            [
                'type'  => 'true_false',
                'label' => __('Enable Google My Business Sync', 'jvb'),
                'hint'  => 'Automagically fetch your latest positive reviews from your Google Business Listing.
                Note: you must enable your GMB integration in settings.',
            ]
        );
        $fields->addField('gmb_min_rating',
            [
                'type'  => 'number',
                'default'   => 4,
                'min'   => 1,
                'max'   => 5,
                'label' => 'Minimum Rating to fetch',
                'hint'  => 'Defaults to 4 stars and above',
                'condition' => [
                    'field' => 'enable_gmb_review_sync',
                    'operator'  => '==',
                    'value' => true,
                ]
            ]
        );
        $fields->addField(
            'reviews',
            [
                'type'  => 'repeater',
                'label' => __('Reviews', 'jvb'),
                'fields'    => [
                    'author'    => [
                        'type'  => 'text',
                        'label' => __('Review Author', 'jvb'),
                        'required'  => true,
                    ],
                    'reviewRating'  => [
                        'type'  => 'number',
                        'min'   => 1,
                        'max'   => 5,
                        'label' => __('Review Rating', 'jvb'),
                        'required'  => true,
                    ],
                    'reviewBody'    => [
                        'type'  => 'textarea',
                        'label' => __('Review Text Content', 'jvb'),
                    ],
                    'url'       => [
                        'type'  => 'url',
                        'label' => __('Link to actual review'),
                        'hint'  => 'Optional. Link directly to the review source for added authenticity.'
                    ]
                ],
                'condition' => [
                    'field' => 'enable_gmb_review_sync',
                    'operator'  => '!=',
                    'value' => true,
                ]
            ]
        );
    }
    public function formatReviewField(Meta $meta):void
    {
        $gmb = $meta->get('enable_gmb_review_sync');
        if (!empty($gmb) && $gmb === true) {
            $this->fetchGMBReviews($meta);
            return;
        }
        $reviews = $meta->get('reviews');
        if (!empty($reviews)) {
            $theReviews = [];
            foreach ($reviews as $row) {
                $review = new Review();
                $author = $row['author'];
                $reviewer = new Person();
                $reviewer->setName($author);
                $review->setAuthor($reviewer);
                $rating = new Rating();
                $rating->setRatingValue($row['reviewRating']);
                $review->setReviewRating($rating);
                if (!empty($row['reviewBody'])) {
                    $review->setReviewBody($row['reviewBody']);
                }
                if (!empty($row['url'])){
                    $review->setUrl($row['url']);
                }
                $theReviews[] = $review;
            }
            if (!empty($theReviews)) {
                $this->setReview($theReviews);
            }
        }
    }
    protected function fetchGMBReviews(Meta $meta):void
    {
        $userID = (int) get_post_meta($meta->id(), BASE.'profile_link', true);
        if ($userID && $userID > 0) {
            $integration = JVB()->connect('gmb', $userID);
            if ($integration && $integration->isSetUp()){
                $fetched = $integration->getReviews(20);
                if (!empty($fetched)) {
                    $reviews = [];
                    $minRating = $meta->get('gmb_min_rating');
                    foreach ($fetched as $review) {
                        $rating = $review['starRating'] ?? 0;
                        if ($rating >= $minRating) {
                            $reviews[] = $review;
                            if (count($reviews) >= 10) {
                                break;
                            }
                        }
                    }
                }
                if (!empty($reviews)) {
                    $theReviews = [];
                    foreach ($reviews as $r) {
                        $review = new Review();
                        $author = $r['reviewer']['displayName'] ?? 'Anonymous';
                        $author = strtok($author, ' ');
                        $reviewer = new Person();
                        $reviewer->setName($author);
                        $review->setAuthor($reviewer);
                        $rating = new Rating();
                        $rating->setRatingValue(match($r['starRating']) {
                            'FIVE'  => 5,
                            'FOUR'  => 4,
                            'THREE' => 3,
                            'TWO'   => 2,
                            'ONE'   => 1,
                            default => 0
                        });
                        $review->setReviewRating($rating);
                        if (!empty($r['comment'])) {
                            $review->setReviewBody($r['comment']);
                        }
                        if (!empty($r['updateTime']??'')){
                            $review->setDateCreated($r['updateTime']);
                        }
                        $theReviews[] = $review;
                    }
                    if (!empty($theReviews)) {
                        $this->setReview($theReviews);
                    }
                }
            }
        }
    }
}
inc/managers/SEO/render/Traits/_Properties/sloganTrait.php
@@ -1,6 +1,8 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\registrar\Fields;
if (!defined('ABSPATH')) {
    exit;
}
@@ -18,4 +20,15 @@
    {
        $this->slogan = $slogan;
    }
    public function setSloganField(Fields $fields):void
    {
        $fields->addField(
            'slogan',
            [
                'type'  => 'text',
                'label' => __('Tagline or Slogan', 'jvb')
            ]
        );
    }
}
inc/registrar/Fields.php
@@ -1,6 +1,8 @@
<?php
namespace JVBase\registrar;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\registrar\fields\Field;
use JVBase\registrar\fields\GroupedField;
use JVBase\registrar\fields\OptionsField;
@@ -267,159 +269,97 @@
    protected function addReviewField(?string $label = null):void
    {
        $this->addField(
            'reviews',
            [
                'type'    => 'repeater',
                'add_label' => 'name',
                'label'   => $label ?: 'Reviews',
                'fields'  => [
                    'name'   => [
                        'type'  => 'text',
                        'label' => 'Reviewer Name',
                    ],
                    'review' => [
                        'type'  => 'textarea',
                        'quill' => false,
                        'label' => 'Review',
                    ],
                    'rating' => [
                        'type'    => 'select',
                        'label'   => 'Rating',
                        'options' => [
                            'none' => 'Not Given',
                            '0.5'  => '0.5',
                            '1'    => '1',
                            '1.5'  => '1.5',
                            '2'    => '2',
                            '2.5'  => '2.5',
                            '3'    => '3',
                            '3.5'  => '3.5',
                            '4'    => '4',
                            '4.5'  => '4.5',
                            '5'    => '5',
                        ],
                        'default' => 'none'
                    ],
                    'date'   => [
                        'type'  => 'date',
                        'label' => 'Date of Review',
                    ],
                    'url'    => [
                        'type'  => 'url',
                        'label' => 'Link to Review (optional)',
                    ],
                ],
                'section' => 'seo'
            ]
        );
        $biz = new LocalBusiness();
        $biz->setReviewField($this);
    }
    protected function addAlternateNameField():void
    {
        $this->addField(
            'alternate_name',
            [
                'type'    => 'repeater',
                'label'   => 'Alternate Name',
                'fields'  => [
                    'name' => [
                        'type'  => 'text',
                        'label' => 'Name',
                    ]
                ],
                'section' => 'seo'
            ]
        );
        $thing = new Thing();
        $thing->setAlternateNameField($this);
    }
    protected function addKeywordsField():void
    {
        $this->addField(
            'keywords',
            [
                'type'      => 'repeater',
                'label'     => 'Keywords',
                'fields'    => [
                    'keyword' => [
                        'type'  => 'text',
                        'label' => 'Keyword',
                    ],
                ],
                'default'   => $labels ?? [ 'Edmonton tattoos', 'Edmonton tattoo artist', 'Edmonton tattooist' ],
                'section'   => 'seo',
                'quickEdit' => true,
            ]
        );
        $thing = new LocalBusiness();
        $thing->setKeywordsField($this);
    }
    protected function addOutsidePhotoField():void
    {
        $this->addField(
            'outside_photo',
            [
                'type'  => 'image',
                'limit' => 1,
                'label' => __('Outside Photo', 'jvb')
            ]
        );
        $business = new LocalBusiness();
        $business->setPhotoField($this);
    }
    protected function addSloganField():void
    {
        $this->addField(
            'slogan',
            [
                'type'  => 'text',
                'label' => __('Tagline or Slogan', 'jvb')
            ]
        );
        $business = new LocalBusiness();
        $business->setSloganField($this);
    }
    protected function addPaymentField():void
    {
        $this->addField(
            'payment_accepted',
            [
                'type'  => 'set',
                'label' => __('Payment Accepted', 'jvb'),
                'options'   => [
                    'Cash'          => 'Cash',
                    'Credit Card'   => 'Credit Card',
                    'Debit'         => 'Debit',
                    'Google Pay'    => 'Google Pay',
                    'Apple Pay'     => 'Apple Pay',
                    'PayPal'        => 'PayPal',
                    'Interac'       => 'Interac',
                    'AMEX'          => 'AMEX',
                ],
            ]
        );
        $business = new LocalBusiness();
        $business->setPaymentAcceptedField($this);
    }
    protected function addAmenitiesField():void
    {
        $business = new LocalBusiness();
        $business->setAmenityFeatureField($this);
    }
    protected function addCredentialsField():void
    {
        $this->addField(
            'amenities',
            'credentials',
            [
                'type'  => 'set',
                'label' => __('Amenities', 'jvb'),
                'label' => __('Credentials', 'jvb'),
                'options' => [
                    'Wheelchair Accessible' => 'Wheelchair Accessible',
                    'Free Parking' => 'Free Parking',
                    'Private Rooms' => 'Private Rooms',
                    'Air Conditioning' => 'Air Conditioning',
                    'WiFi' => 'WiFi',
                    'Gender Neutral Restroom' => 'Gender Neutral Restroom',
                    'LGBTQ+ Friendly' => 'LGBTQ+ Friendly',
                    'Sterilization Room' => 'Sterilization Room',
                    'Refreshments Available' => 'Refreshments Available',
                    'Street Level Access' => 'Street Level Access',
                    'Single Use Needles' => 'Single Use Needles',
                    'Consultation Room' => 'Consultation Room',
                    'Aftercare Products Available' => 'Aftercare Products Available',
                    'Walk-Ins Welcome' => 'Walk-Ins Welcome',
                    'WHMIS 2015' => 'WHMIS 2015',
                    'Tattoo and Piercing Safety Standards' => 'Tattoo and Piercing Safety Standards',
                    'Bloodborne Pathogens and Infection Control' => 'Bloodborne Pathogens and Infection Control',
                    'First Aid Training' => 'First Aid Training',
                ]
            ]
        );
    }
    protected function addPermanentlyCloseField():void
    {
        $business = new LocalBusiness();
        $business->setDissolutionDateField($this);
    }
    protected function addHoursField():void
    {
        $business = new LocalBusiness();
        $business->setOpeningHoursSpecificationField($this);
    }
    protected function addRateField():void
    {
        $this->addField('rate',
        [
            'type'  => 'text',
            'subtype'   => 'number',
            'label'     => __('Hourly Rate', 'jvb')
        ]);
    }
    protected function addAwardsField():void
    {
        $business = new LocalBusiness();
        $business->setAwardField($this);
    }
    protected function addRatingsField():void
    {
        $business = new LocalBusiness();
        $business->setAggregateRatingField($this);
    }
    protected function addServicesField():void
    {
        $business = new LocalBusiness();
        $business->setHasOfferCatalogField($this);
    }
}