Adding your own features
Adding your own features
Worked examples for the most common additions. Each one is the same pattern the theme uses for its own features, so it looks and behaves like the rest of the site. Put the code in a child theme or your own plugin.
A card in the listing editor
Two hooks work together: ppt_listing_form_cards prints your card at the end of the listing editor (for members and administrators), and ppt_listing_editor_saved saves it. The editor has already checked who is saving before your save runs.
// 1. Print the card. Use the editor's own classes so it matches.
add_action('ppt_listing_form_cards', function ($listingId, $mode) {
$value = $listingId ? get_post_meta($listingId, 'my_opening_note', true) : '';
?>
<div class="le-card" id="le-my-note">
<h3><?php esc_html_e('Opening note', 'my-site'); ?></h3>
<p class="le-sub"><?php esc_html_e('A short line shown under your listing title.', 'my-site'); ?></p>
<input type="hidden" name="my_note_present" value="1">
<div class="le-field">
<label class="le-lab" for="my-note"><?php esc_html_e('Note', 'my-site'); ?></label>
<input type="text" class="le-in" id="my-note" name="my_opening_note" maxlength="120"
value="<?php echo esc_attr($value); ?>">
</div>
</div>
<?php
}, 20, 2);
// 2. Save it. $in is the posted form. The hidden "present" field tells you the
// card was on the form, so an empty box clears the value.
add_action('ppt_listing_editor_saved', function ($listingId, $in, $mode) {
if (empty($in['my_note_present'])) {
return;
}
$note = sanitize_text_field($in['my_opening_note'] ?? '');
update_post_meta((int) $listingId, 'my_opening_note', $note);
}, 10, 3);
This card shows in the Directory Theme's listing editor. Products with their own editor do not fire ppt_listing_form_cards yet.
For simple extra information (a text box, a dropdown, a checkbox), you may not need code at all: Listings ▸ Custom Fields adds fields to the editor and shows them on the listing.
A section on the listing page
ppt_listing_after_sections runs after the listing's own sections. It passes the listing ID and a function that draws a section heading in the theme's style:
add_action('ppt_listing_after_sections', function ($listingId, $heading = null) {
$note = get_post_meta((int) $listingId, 'my_opening_note', true);
if ($note === '') {
return; // print nothing when there is nothing to show
}
echo '<section id="opening-note" class="ppt-s-block ppt-s-sec">';
echo is_callable($heading) ? $heading(__('Opening note', 'my-site')) : '<h2>' . esc_html__('Opening note', 'my-site') . '</h2>';
echo '<p>' . esc_html($note) . '</p>';
echo '</section>';
}, 20, 2);
To put something under the title instead, use ppt_listing_after_title; for the top of the sidebar, ppt_single_sidebar_top. These run on the shared listing page; see Product differences for the themes that draw their own.
A tab in the member area
Two hooks again: ppt_hub_nav_extend adds your tab to the member area's menu, and ppt_hub_extra_panels returns its content.
// 1. Add a group with one tab. $groups and $tabMap are changed in place.
add_action('ppt_hub_nav_extend', function (&$groups, &$tabMap, $userId, $listings = array()) {
$groups['my_rewards'] = array(
'label' => __('Rewards', 'my-site'),
'short' => __('Rewards', 'my-site'),
'mini' => __('Rewards', 'my-site'),
'sub' => __('Points you have earned', 'my-site'),
'icon' => 'star',
'items' => array(
array('tab' => 'my-rewards', 'label' => __('My points', 'my-site'), 'icon' => 'star', 'n' => ''),
),
);
$tabMap['my-rewards'] = array('g' => 'my_rewards', 'gl' => __('Rewards', 'my-site'), 'l' => __('My points', 'my-site'));
}, 10, 4);
// 2. Return the tab's panel. Keep the HTML that was passed in, and add yours.
add_filter('ppt_hub_extra_panels', function ($html, $userId, $listings = array(), $defaultTab = '') {
$points = (int) get_user_meta($userId, 'my_points', true);
$hidden = $defaultTab === 'my-rewards' ? '' : ' hidden';
return $html . '<section class="ppt-hub-panel" data-panel="my-rewards"' . $hidden . '>'
. '<h2>' . esc_html__('My points', 'my-site') . '</h2>'
. '<p>' . sprintf(esc_html__('You have %d points.', 'my-site'), $points) . '</p>'
. '</section>';
}, 10, 4);
The tab key (my-rewards here) must match in the menu item, the tab map and the panel's data-panel. The Dating, Escort and Video themes have their own member area and do not fire these hooks.
A homepage block
Blocks are the sections you add to the homepage and other pages in the Site Generator and in Elementor. A block is a PHP class with six methods:
// The third part of the namespace must be "_common": see below.
namespace MySite\Blocks\_common;
use Ppt\Blocks\Contracts\Block;
final class OpeningHours implements Block
{
public function key(): string { return 'my_opening_hours'; } // unique
public function category(): string { return 'text'; } // also: hero, listings, cta, faq, pricing, blog
public function label(): string { return 'Opening hours'; }
public function defaults(): array
{
return array('title' => 'Opening hours', 'text' => 'Mon to Fri, 9am to 5pm');
}
public function render(array $data): string
{
return '<section class="my-hours"><div class="container">'
. '<h2>' . esc_html($data['title']) . '</h2>'
. '<p>' . esc_html($data['text']) . '</p>'
. '</div></section>';
}
public function css(): string
{
return '.my-hours{padding:48px 0;text-align:center}';
}
}
Register it with ppt_blocks:
add_filter('ppt_blocks', function (array $classes) {
$classes[] = \MySite\Blocks\_common\OpeningHours::class;
return $classes;
});
Why _common. To keep sites fast, PremiumPress only loads the blocks of the product that is running, plus a shared set. It tells them apart by the third part of the class's namespace, and _common is the shared set. A block in MySite\Blocks\_common is offered on every product; one in, say, MySite\Blocks\OpeningHours would be skipped.
Once registered, the block is available in Elementor too, with no extra code.
Your own email
Add a template with ppt_email_catalog. It then appears on the Emails screen, where the site owner can edit its wording, and you send it from your code:
add_filter('ppt_email_catalog', function (array $catalog) {
$catalog['my_points_earned'] = array(
'group' => 'customer',
'name' => __('Points earned', 'my-site'),
'desc' => __('Sent when a member earns reward points.', 'my-site'),
'subject' => __('You earned {points} points on {site_name}', 'my-site'),
'body' => __("Hi {user_name},\n\nYou now have {points} points.", 'my-site'),
);
return $catalog;
});
// Later, in your own code:
\Ppt\Content\EmailTemplates::send('my_points_earned', $user->user_email, array(
'user_name' => $user->display_name,
'points' => 120,
));
Words in braces, such as {points}, are replaced by the matching value you pass. {site_name} and the other standard tokens are filled in for you.
A text message
If the site has a text-message provider set up, fire ppt_send_sms to send one:
do_action('ppt_send_sms', '+15551234567', 'Your booking is confirmed.');
Shipping rates
A plugin can supply live shipping rates: register the provider with ppt_shipping_providers and return rates from ppt_shipping_rate_quote. The pattern is the same as for payment gateways.