Skip to main content

Directory Theme class reference

33 min read

Nonprofit Theme — Class reference

The 50 classes that make _np behave differently from the shared Directory core. Generated from the theme source.

← Back to Nonprofit Theme features

AdminFundraising AdminFundraising.php​

PremiumPress ▸ Fundraising (F17, F20, F8, F10, spec section 8) — one screen, tabbed: Overview raised this month, active monthly supporters, pledged each month (per currency), gifts today; "Recalculate all totals" Donations every gift, filterable by campaign, fund, type, date, status; Refund (records it — the owner refunds in the gateway first); Add offline gift; CSV. "Messages" sub-view: approve / hide Monthly supporters each pledge and its state; "Pledged each month" in the header Donors grouped by email: total, first / last gift, monthly yes/no; CSV Fundraiser pages waiting pages: Approve / Reject (reason emailed) Funds add, rename, deactivate; delete only a fund with no gifts Settings receipts, the donation form, emails, moderation (owner only) Staff (editors) see everything but Settings, refunds and funds; the owner sees all. Every write goes through admin-post ppt_np_admin with a nonce and a capability check.

API
register()menu()url()waiting()render()donorRows()handle()saveSettings()
inc/_np/AdminFundraising.php

CampaignFields CampaignFields.php​

The listing editor's "Fundraising" card (spec 4.2) — one card whose fields follow the listing's TYPE (listing_np_type): Appeal goal, deadline (+ keep taking gifts after it), fund, suggested amounts with "what it pays for" labels, default frequency, peer-to-peer on/off; staff only: approval rule, urgent, offline total Fundraiser page target, deadline (its parent appeal is set by the /fundraise/ flow) Event start, end, sales close, venue, linked appeal, ticket types Volunteer role requirements, linked appeal, shifts Sermon media URL, speaker, scripture, date, series Saved on ppt_np_listing_saved (fired at the end of _np\ListingEditor::save()), so it runs after the core fields, in both the admin and the member editor. Only staff (administrators and editors) may change a listing's type or the staff-only fields. Rules (spec F3): goal 0–100,000,000; a deadline, when set, must be today or later (an unchanged past deadline is kept — the campaign has simply ended); a fund must exist. An appeal needs nothing beyond title, story and fund to go live.

API
register()isStaff()tickets()shifts()render()save()setUrgent()
inc/_np/CampaignFields.php

CampaignPage CampaignPage.php​

The campaign parts of the single listing page (F3, F4, F8, F9, F11, F22) — drawn through the shared template's fork partials in inc/_np/templates/parts/: after the title progress block: "$18,040 raised of $25,000 · 312 donors · 9 days left", the bar (capped at 100%, "Goal reached" above it), "Ended" after the deadline; the organiser line; a fundraiser page's "Part of: <appeal>" sidebar top the donation form (sticky on desktop; under the story on phones) after the overview the updates feed, and "Post an update" for whoever runs it after the sections the donor wall, the top fundraisers, the share bar Also: the site-wide urgent appeal bar (dismissible for 24 hours), the donor wall's "See all" (AJAX), the campaign update emails (batched 50 per hourly run), and the fixed page layout — the movable section board is switched off for this fork, so the campaign always reads progress → story → updates → donors. Event, volunteer-role and sermon pages get their boxes from EventTickets, Volunteers and Sermons through the ppt_np_sidebar / ppt_np_after_title filters.

API
register()progress()afterTitle()sidebar()afterOverview()afterSections()updateList()canPostUpdate()handlePostUpdate()addUpdate()sendQueued()unsubscribeUrl()maybeUnsubscribe()wallRows()recentGifts()wallItem()ajaxWall()shareBar()urgentId()urgentBar()styles()
inc/_np/CampaignPage.php

Campaigns Campaigns.php​

Campaign totals (F4): what an appeal or fundraiser page has raised and from how many donors, cached on the listing as ppt_np_raised / ppt_np_donors and recomputed — never edited by hand — on every completed gift, renewal, refund and offline edit. appeal raised = completed gifts whose campaign_id is the appeal (its own gifts AND its fundraiser pages' gifts) + paid ticket income of the events linked to it (ppt_np_campaign) + the owner's offline total fundraiser page raised = completed gifts made to the page itself Only the gift AMOUNT counts. Fee cover is money the donor added for the card fees, so a $50 gift with $1.75 fee cover raises the bar by exactly $50. Refunded rows are out.

API
recalc()ticketIncome()goal()raised()donors()campaignOf()acceptsGifts()hasEnded()daysLeft()siteTotals()
inc/_np/Campaigns.php

Checkout Checkout.php​

Donation checkout (F5, F6, F7) — one-off and monthly gifts, raised as ppt_orders with the GIVE- reference prefix. THE CLASS NAME IS LOAD-BEARING. Gateway webhooks only know Payments\CheckoutFlow; CheckoutFlow::orderOwner() finds the checkout that owns an order by trying \Ppt\\Checkout and comparing its REF_PREFIX, so a Stripe checkout.session.completed for a GIVE- order reaches markPaid() here, and an invoice.paid renewal reaches renewOrder(). Rename it and every webhook misses. Flow: 1. The donation form (renderForm(): the /donate/ page, the [ppt_donate] shortcode, the DonateForm block, the appeal page) posts to admin-post ppt_np_give. 2. handleGive() re-validates everything server-side — the amount, the fee cover (recomputed, never trusted), the fund, the gateway (for a monthly gift it must be one that can bill repeatedly) — raises a pending GIVE- order and hands it to the gateway through ppt_gateway_start (recurring + 30 days for monthly). 3. The gateway returns the donor to /callback/?order=GIVE-…; renderCallback() verifies through ppt_gateway_verify, completes, and thanks them. 4. completeOrder() is idempotent: CheckoutFlow::claimOrder() lets exactly one caller (return, webhook, a reload) through, and the gift row's unique (order_id, event_id) key backs it up — a second gift row is impossible. Monthly gifts: the first charge writes a pledge and a monthly_first gift; each renewal (renewOrder) writes a monthly_renewal gift with its own receipt; ending (endSubscription) stops the pledge. A daily cron marks a pledge whose payment is more than ppt_np_payment_issue_days overdue as "payment issue" and tells the donor once, and closes pending orders left for 48 hours. No coupon field and no tax line: a gift is not a sale.

API
register()isGiveRef()typeLabel()donateUrl()callbackUrl()retryUrl()findOrderByRef()renderForm()handleGive()prepare()renderDonatePage()createOrder()pay()handleRetryPay()completeOrder()markPaid()renewOrder()endSubscription()orderContext()ownPledge()handleCancelPledge()handleManagePledge()canManage()schedule()
inc/_np/Checkout.php

DemoAccounts DemoAccounts.php​

Nonprofit — what the one-click DEMO member walks into (spec s11). The nonprofit profile asks no account-type question, so the home-demo offers the owner (admin) login and ONE member login (Account\DemoLogin's test member). That member is furnished as both people the spec describes: DONOR 3 one-off gifts (to Winter Shelter, Weekend Food Boxes and the street trees, 30 / 14 / 3 days ago), 1 active $25 monthly gift with 5 renewals, 2 tickets to the supper and quiz, 1 booked volunteer shift — every gift with its receipt; FUNDRAISER runs "Maya Runs the Lakefront 10K" (borrowed for the session — its 14 gifts and its update come from _np\DemoGiving), so My fundraisers shows a live page with gifts and an update. Builds on the seeded demo charity (_np\DemoGiving); with no demo listings there is nothing to furnish. Every row is recorded and removed on unfurnish (DemoLogin's purge), which also hands the fundraiser page back. No mail is sent while furnishing.

API
register()record()furnish()unfurnish()
inc/_np/DemoAccounts.php

DemoGiving DemoGiving.php​

Demo giving data (F28): turns the 19 seeded npcharity sample listings into a working charity — called by Tools\SampleData::seed() right after it writes the listings, on every path that seeds (the Sample Data screen, the installer, a local design install). rows 1-13 appeals: goal = the row's price column, raised = goal x the row's rating column (percent) — ~25 one-off gifts each over the last 90 days, monthly renewals where a pledge points at it, the rest as an "offline" total. A few get a deadline; Flood Relief is the urgent appeal; two allow fundraisers. rows 14-15 supporters' fundraiser pages under appeals 1 and 3 (14 and 9 gifts), the first with one update posted. rows 16-17 events: a ticketed supper and quiz (adult / child tickets, linked to the soup kitchen appeal) and a free river clean-up RSVP — 60 tickets sold between them. rows 18-19 volunteer roles with four upcoming shifts each, 25 sign-ups in all. 40 monthly pledges (35 active, 3 payment issue, 2 cancelled), each a real GIVE- order on the Test gateway, with their first charge and renewals. The price and rating columns are cleared off the listings afterwards (a goal is not a price, a percent is not a star rating). Every donor email is @example.org. Deterministic (seeded random), idempotent per listing (ppt_np_demo_seeded), and a demo listing's rows go when the listing is deleted.

API
register()listings()seed()pledgeOrder()onDelete()clear()
inc/_np/DemoGiving.php

DonorDashboard DonorDashboard.php​

The donor dashboard (F15) — a "Giving" group in the account hub: My giving every gift (date, campaign, fund, amount, type, receipt), with the totals given this year and all time Monthly gifts each monthly gift: amount, what it supports, next date, status; Cancel (confirms first) and Update card when the gateway offers it Tickets event tickets bought (codes) Volunteering shifts signed up for My fundraisers the member's own fundraiser pages: raised, target, edit A member's records are matched by their account OR by their account's email, so gifts made as a guest before signing up are there too (see Giving::linkGuestRecords). Wired into the hub by two marked lines in Account/views/parts/hub-nav-data.php and hub-panels.php (the hub has no filter); every rule lives here.

API
gifts()pledges()tickets()shifts()fundraisers()hidesGroups()nav()panels()
inc/_np/DonorDashboard.php

Emails Emails.php​

The Nonprofit theme's emails (spec section 7, F25), added to the shared catalog under their own "Fundraising emails" group so the owner edits them on the normal Email settings screen — Content\EmailTemplates::send() refuses unknown keys. Wording rule: nothing here — or anywhere in this fork — makes any claim about how a gift is treated for tax. Rules differ by country; the owner writes that wording themselves if it applies to them. send() is the one way the fork sends: on the public demo network (*.premiummod.com) mail to anyone but the site admin is dropped, so a visitor trying the demo checkout never sends a "receipt" to a real inbox.

API
register()groups()catalog()send()suppressed()goalReached()
inc/_np/Emails.php

EventTickets EventTickets.php​

Event tickets (F12, F27) — galas, quiz nights and sponsored walks with ticket types. Event page under the title: when, where, and the appeal the ticket income goes to; in the sidebar: the ticket picker — each type with its price and "12 left", a quantity of 0 to 10 (never more than are left), the buyer's name and email, a coupon field when coupons are on, the payment methods when anything costs money. Sold-out types are disabled; after the sales close (ppt_np_sales_close, else the event start) the picker says so. /events/ upcoming events by date. Admin Fundraising ▸ Events & attendees: pick an event, see its attendees (name, email, type, qty, code, status), the ticket income, a CSV, and Refund per ticket line (frees the place; the owner refunds the money in the gateway first). Money: a ticket order is a GIVE- order of kind ticket through _np\Checkout, so every gateway, the retry page, the callback and webhooks work exactly as for a gift. One ppt_np_tickets row per ticket type in the order, pending until paid. A place counts as taken when its row is paid, or pending and under 30 minutes old — checked and written under a per-event lock, so two buyers can never take the last place twice. A free order (nothing to pay after any coupon) completes at once with no gateway. Paid ticket income counts towards the event's linked appeal (ppt_np_campaign).

API
register()start()salesClose()salesOpen()when()type()taken()remaining()priceLine()leftLine()upcomingIds()upcoming()afterTitle()sidebar()picker()createOrder()handle()complete()lines()thankYou()summary()refundLine()renderList()attendees()
inc/_np/EventTickets.php

Expiry Expiry.php​

PPT — listing expiry / "Listing lifetime". The Settings ▸ Listings "Listing lifetime" (days) governs how long a listing stays live. 0 = never expires. A value > 0 stamps an expiry timestamp on each listing when it's published; a daily cron then runs the configured "On expiry" action (listings_expiry_action: nothing / draft / pending / trash) once the time is up. The expiry timestamp meta is SHARED with the pricing-plan expiry (PricingPlans::LISTING_EXPIRES_META). A listing that carries a pricing plan is governed by that plan's own duration (set in the editor / at checkout), so the global lifetime only applies to listings WITHOUT a plan. Either way, the cron here enforces whatever expiry timestamp a listing ends up with, and both editors show the time remaining.

API
register()metaKey()schedule()unschedule()onTransition()applyLifetime()runCheck()timestamp()remaining()dateLabel()
inc/_np/Expiry.php

Fundraisers Fundraisers.php​

Peer-to-peer fundraiser pages (F10) and the leaderboard (F11). A supporter raises money for an appeal on a page of their own (a sponsored run, a birthday, a head shave). From an appeal with ppt_np_p2p on, "Start your own fundraiser" leads to /fundraise/?campaign=ID: title, story (pre-filled), a photo, a target and an optional end date. The page is a fundraiser-page listing at /listing/<slug>/ whose gifts go to the page AND count to the parent appeal, in the parent's fund. Rules: sign-in required; at most 5 open pages per supporter per appeal; target 10–1,000,000; the page closes when the parent ends (Campaigns::acceptsGifts); the appeal's approval rule (ppt_np_p2p_approval, default from Settings) decides whether a new page waits for staff. Approve / reject (with a reason) email the fundraiser.

API
register()startUrl()open()needsApproval()openCount()startPrompt()renderStart()handleStart()create()approve()reject()top()leaderboard()
inc/_np/Fundraisers.php

Funds Funds.php​

Named funds (F20): "Where needed most", "Missions", "Building project"… A gift is always to one fund; reports split by it. Stored as option ppt_np_funds, a list of {slug, label, active}. general always exists and cannot be deactivated; a deactivated fund leaves the donation forms but stays in every report, and a fund that has gifts is never deleted (see canDelete()).

API
all()active()exists()isActive()label()save()canDelete()delete()forListing()
inc/_np/Funds.php

Single-listing gallery styles. The admin picks one of four layouts on the Design ▸ Listings tab (stored in the ppt_design option via Branding), and the single-listing template renders the chosen layout for the listing's images. standard — large hero photo + thumbnail strip (click to swap) [default] grid — all photos in a tiled grid (first one featured) carousel — a swipeable/scroll-snap slider with prev/next tall — full-width photos stacked vertically (good for portraits)

API
styles()locked()current()isFullSpan()render()css()
inc/_np/Gallery.php

Geocoder Geocoder.php​

Bulk geocoder — fills in lat/lng for existing listings that have an address but no coordinates (e.g. listings created before the editor's map picker existed), so they get a precise map marker and the "Distance from me" feature. Uses the site's Maps provider (Settings ▸ API keys): Google / Mapbox geocoding APIs (their key), or OpenStreetMap Nominatim (keyless, rate-limited to ~1 req/sec with an identifying UA). A small box on the PPT Listings screen runs it in batches over AJAX. Listings that can't be geocoded are flagged (_ppt_geo_failed) so they aren't retried forever.

API
register()geocode()pendingCount()ajaxBatch()assets()box()
inc/_np/Geocoder.php

Giving Giving.php​

The giving ledger: gift rows, monthly pledges, receipt numbers, fee cover and which gateways may take a gift. No HTML here — Checkout drives the money flow and the screens, this owns the records. Two rules every caller relies on: - insertGift() is idempotent on (order_id, event_id) — the table's unique key — so a reloaded callback, a retried webhook or a racing return can never write a second row for one charge. It returns 0 when the row already existed. - amount is the gift; fee_cover is what the donor added to cover card fees. Only amount ever counts towards what a campaign has raised.

API
feeCover()money()moneyShort()formatSettings()gateways()recurringGateways()monthlyAvailable()nextReceiptNo()insertGift()gift()firstGiftOfOrder()updateGift()refundGift()insertPledge()pledge()pledgeByOrder()updatePledge()plusMonth()linkGuestRecords()
inc/_np/Giving.php

ImpactStories ImpactStories.php​

Impact stories and news (F14): ordinary blog posts that point at the work they describe. Editor a "Linked campaign or fund" box beside the post (post meta ppt_np_link_campaign = an appeal or fundraiser page, ppt_np_link_fund = a fund slug; a campaign wins when both are set). Post page a "Support this work" card after the story: the campaign's live progress and a Donate button that opens that campaign's form — or, for a fund, the donation form with the fund chosen. Blocks latest() and tagLabel() feed the ImpactStories block (Story / Update / News).

API
register()metaBox()campaignOptions()renderBox()save()link()card()appendCard()latest()tagLabel()
inc/_np/ImpactStories.php

ListingActions ListingActions.php​

Per-listing visitor actions shown on the single-listing section nav: - Add to favorites — toggles the listing in the member's saved list (user meta ppt_favorites); the account page shows them. Logged-in only; guests are routed to sign-in. - Report — flags the listing to the site admin. The reason is stored as a comment on the listing (type ppt_report, a custom approval status so it stays hidden from the front-end and the normal comment-moderation tabs) and also emailed to the admin. Reports are reviewed in the admin Comments screen's dedicated "Reports" tab. Open to guests and members.

API
register()favIds()favHas()favToggle()favListingIds()ajaxFav()ajaxReport()assets()favButton()reportButton()
inc/_np/ListingActions.php

ListingCard ListingCard.php​

The canonical listing card — the single, theme-wide way a business/listing is shown as a card. Every directory surface (search results, single-page "related", and the live listing-grid blocks) renders through here, so a listing looks the same everywhere and the demo-vs-live data split lives in ONE place. Data-source agnostic: fromPost() maps a real listing_type record and fromSample() maps a curated demo row into the same normalized shape, which render() draws. The markup is built on the theme design tokens, so the one card automatically adopts each design's colours/fonts. Its CSS is enqueued site-wide (assets/css/listing-card.css) — the card only emits markup. Normalized shape: name, link, img, cat, rating, desc, desc_long, price, city, dist, featured (bool), highlighted (bool), badge (string), id badge is an optional ribbon label (e.g. "New") for callers that need one; when empty it falls back to "Featured" if featured is set.

API
fromPost()campaignFields()serviceFields()fromSample()render()
inc/_np/ListingCard.php

ListingEditor ListingEditor.php​

Listing editor — the shared brain behind the two listing-edit screens: - Admin : PPT-styled screen at ?page=ppt_listings&edit=<id> (Chrome shell), rendered by Admin\Listings when the edit param is present. - Member : front-end screen at /account/listing/<id>/ inside the Member Hub, rendered by Account\MembersPage for a listing the member owns. Both POST to admin-post.php (action ppt_listing_save) and run through the one save() below, so the field set, sanitising and persistence live in a single place. The mode (admin|member) decides which extras are honoured: admins get status, author, slug, featured/verified and the "edit only" custom fields; members get a friendly subset scoped to their own listing. Fields = core (title, description, category, tags, featured image, gallery, FAQ) + every field defined in the Custom Fields admin (Admin\Fields / option ppt_fields), stored as post meta keyed by the field key — which is what the single-listing template already reads.

API
register()defaultBookingProvider()newUrl()handleNew()submissionsOpen()memberCanAdd()canEdit()adminEditUrl()memberEditUrl()applicableFields()locationKeys()coordKeys()mapPickerReady()mapConfig()locationFields()socialNetworks()socialValue()socialIcon()hoursDays()hoursValue()normTime()money()untitledTitle()plainText()
inc/_np/ListingEditor.php

ListingFaq ListingFaq.php​

Single-listing FAQ. Reads per-listing FAQ items from the faq post meta (an array of ['q' => …, 'a' => …]). A live listing shows only its own saved FAQ — when it has none the section is omitted rather than filled with fabricated entries. Demo/preview mode supplies sample FAQ via DemoContent so a fresh design isn't empty. Populate a listing's own FAQ by saving faq meta, inject a site-wide default set via the ppt_listing_faq_default filter, or adjust the final list with the ppt_listing_faq filter.

API
items()
inc/_np/ListingFaq.php

ListingHours ListingHours.php​

Business hours display for the single-listing sidebar. Reads the business_hours meta written by the listing editor (ListingEditor::HOURS_META) and renders a Google-Business-style day list with an "Open now / Closed now" badge computed in the site's timezone. Times are formatted with the site's time format.

API
has()isOpenNow()renderSidebar()
inc/_np/ListingHours.php

ListingLocation ListingLocation.php​

Single-listing "Location" section — the richer location block: the full formatted address, an interactive map (ListingMap, precise when the listing has coordinates), a "Get directions" button, and a "Distance from you" control (browser geolocation → straight-line distance to the listing). Self-contained: it prints its own scoped CSS + JS once, so single.php just calls render().

API
address()coords()has()render()
inc/_np/ListingLocation.php

ListingMap ListingMap.php​

The single-listing "Location" map. Uses the site's Maps provider setting (Settings ▸ API keys — the same ppt_maps_provider filter the search map reads). When the listing has stored coordinates (lat/lng, set by the editor's map picker) the map centres on them precisely and drops a named marker; otherwise it falls back to geocoding the address string: google / mapbox(fallback) → a keyless Google Maps embed osm → a Leaflet map (marker from coords, else Nominatim) Map libraries load from their CDNs — the owner-approved front-end map exception.

API
render()
inc/_np/ListingMap.php

ListingPricing ListingPricing.php​

Single-listing pricing plans. A listing owner enters one or more plans / packages on the submission form (a name, a price, and a short description); they're stored in the pricing_plans post meta as a list of ['name' => …, 'price' => float|null, 'desc' => …]. The LOWEST priced plan is mirrored into the searchable price meta (Ppt_np\SearchPage::PRICE_META) by the editor on save, so the price range filter, the price sort, and the result-card price all work — that's the link between "enter your pricing" and "find listings by price". Prices are stored in the site's base currency; render() converts + formats via Ppt\Content\Currencies for the viewer's chosen currency.

API
plans()demoPlans()has()lowestPrice()normalise()render()
inc/_np/ListingPricing.php

ListingSections ListingSections.php​

PPT — configurable single-listing sections. The admin (Design ▸ Listings ▸ "Listing page sections") gets a sortable list of on/off toggles for the optional feature blocks a listing page can carry — Booking, Business hours, Maps & Location, FAQ, Reviews. Turning one OFF removes it from BOTH the single listing page and the listing submission/edit form; the order of the list controls the order the (main-column) sections appear in. Config lives in the shared design option (Branding::OPTION = ppt_design) under the listing_sections key: { order:[keys…], enabled:{key:0|1} }. Anything not saved yet defaults to ON, in the catalog order — so existing sites are unchanged until the admin edits the list.

API
supported()catalog()config()order()enabled()orderedAmong()sanitize()
inc/_np/ListingSections.php

ListingTeam ListingTeam.php​

The listing page's "Team" section (Booking Theme, owner 2026-10-01). The listing's staff roster (Directory\ListingEditor::bookingStaff(): name, photo, role, short bio, stable key) drawn as an avatar grid — round photo, name, role, star rating with review count (or "New"), a Book button and a "Reviews" link. Like Fresha. - Book selects that person in the booking box ("Who with?"), reloads their free times and scrolls to it (booking.js, [data-bk-staff]). - "Reviews" filters the review list to that person (inline script below). - The bio shows when the card is hovered, focused or tapped. Ratings come from reviews that name the person (comment meta Reviews::STAFF_META): a review linked to the reviewer's completed booking carries that booking's staff member automatically and is marked verified; anyone else may pick "Who did you see?" on the form (counts, not verified). See Ppt_np\Reviews::linkBooking().

API
has()stats()render()
inc/_np/ListingTeam.php

ListingViews ListingViews.php​

PPT listing analytics — records a "hit" every time a single listing page is viewed, and serves that data back to the listing owner as a graph in the member area (/account ▸ My listings ▸ the analytics icon). Each view stores the listing id, the timestamp (UTC), and — when the visitor is logged in — their user id + display name, so an owner can see who has been looking. Bots/crawlers are skipped so counts reflect real visitors. The owner (and admins) can read the analytics for a listing over the AJAX endpoint, which returns a daily series (for the chart), rolling-window totals (7/30/60/90 days) and a list of recent named viewers. Storage: a custom table {prefix}ppt_listing_views (dbDelta on init).

API
register()table()install()maybeRecord()total()countSince()series()recentViewers()ajaxData()
inc/_np/ListingViews.php

LiveCampaigns LiveCampaigns.php​

The giving figures a homepage campaign card shows, handed to Blocks\Support\LiveItems. LiveItems fills a listings block's cards with the site's real listings (name, photo, link, cause, city) through its suffix map; the nonprofit cards also carry raised, goal, pct, donors and deadline, which only this fork can answer. This adds them to each live row (ppt_live_items_row) from the campaign's cached totals. A listing with no goal (an appeal without a target) gets them blank, and the cards drop their bar and figures for it. The pool itself is limited to open appeals (ppt_live_items_pool).

API
register()pool()row()
inc/_np/LiveCampaigns.php

LiveTotals LiveTotals.php​

Live totals (F24): an appeal's total climbs on the page without a reload. Any element carrying one of these attributes is refreshed every 60 seconds while the tab is visible, from one small AJAX call (ppt_np_totals, answers cached 30 seconds per campaign, so a busy page costs the server one read per campaign per half-minute): data-np-live="ID" the urgent bar's line ("$18,040 raised of $25,000") data-np-live-raised="ID" the raised sum ("$18,040") data-np-live-donors="ID" "312 donors" data-np-live-pct="ID" "72%" data-np-live-bar="ID" a bar: its width is set to the percent A gift made in another browser therefore shows within 90 seconds (30 s cache + 60 s poll).

API
register()figures()many()ajax()script()
inc/_np/LiveTotals.php

Media Media.php​

Listing images — the single, WordPress-native resolver for the PPT image system. A listing's photos are its WP featured image (primary) plus any image attachments parented to it (the gallery). No legacy ?imgid refs, no image-URL meta strings — everything flows through the media library.

API
primary()gallery()videos()media()counts()
inc/_np/Media.php

MobileGive MobileGive.php​

Phone-first giving and link previews (F22). Phones under 768px the donation form moves up to sit straight under the progress block (it lives in the sidebar, which phones otherwise draw after the story and the donor wall), and a sticky bar — "$18,040 raised · 72%" and a Donate button that jumps to the form — rides above the mobile Dock until the form itself is on screen. Moved back if the window grows past 768px. Previews a shared appeal or fundraiser page unfurls with its photo, title and "Raised $18,040 of $25,000" in front of its description: printed as Open Graph / Twitter tags when no SEO plugin is active, handed to Yoast or Rank Math through their description filters when one is. Sharing the share bar (CampaignPage::shareBar) copies the page's permalink.

API
register()current()raisedLine()description()head()pluginDesc()stickyBar()
inc/_np/MobileGive.php

NonprofitCategories NonprofitCategories.php​

The Nonprofit theme's shipped CATEGORIES — five parents, each with its own children: Charity & Causes · Church & Ministry · Schools & PTAs · Animal Rescue · Community & Personal In the Ppt_so\SoftwareCategories shape (slug => [label, parent slug], parents listed before their children). Categories are the shared listing_category (Ppt\PostTypes\Listings::TAX_CATEGORY); only the seed set is nonprofit-specific, so it lives in the fork. The seed is one-shot AND empty-only: a site that already has categories keeps them. Demo seeding calls ensureShipped() directly (Tools\SampleData::ensureProfileVocabulary). Every shipped term is tagged ppt_category_profile = nonprofit, and the campaign editor's picker and the search rail are filtered to this profile's terms, as _mv\MarketCategories does — so a previous profile's leftovers never appear here.

API
register()defaults()slug()seed()ensureShipped()isParentSlug()leaves()demoSlugFor()tagShipped()tagNew()filterPicker()filterSearch()
inc/_np/NonprofitCategories.php

NonprofitTaxonomies NonprofitTaxonomies.php​

The Nonprofit theme's two facet TAXONOMIES on the shared listing post type. listing_np_type (/type/…) — what kind of listing a campaign page is: Appeal, Fundraiser Page, Event, Volunteer Role or Sermon. One answer per listing. The five terms are fixed — the theme's code branches on their slugs — so they can be renamed but never deleted. listing_np_series (/series/…) — sermon series (Sunday Sermons, Advent…). Many per sermon; the owner adds more. Modelled on Ppt_so\SoftwareTaxonomies: registered on init 11 (after Category, so its facet stays first in the search rail), seeded once on init 12 and only into an empty taxonomy, and re-ensured by Tools\SampleData before demo content is written. Nonprofit only: registered from inc/_np/, which Theme::boot() only wires under the nonprofit profile.

API
register()types()seriesDefaults()map()typeOf()setType()singlePick()registerTaxonomies()seed()ensureShipped()flushOnce()
inc/_np/NonprofitTaxonomies.php

Receipts Receipts.php​

Printable gift receipts (F16) at /account/receipt/<giftId>/ — modelled on Account\Invoices: a standalone page with its own print styles and a "Print / Save as PDF" button (the browser's own PDF printer; no PDF library). Who may open one: - the donor (the gift's user, or a signed-in account with the gift's email), - an administrator or editor (staff see every donation), - anyone holding the signed link from the receipt email (?k=), valid for a year — how a guest donor, who has no account, reaches theirs. Every renewal of a monthly gift is its own row and so its own receipt. The page says nothing about tax: the owner's own footer text is the only wording beyond the facts.

API
register()rewrite()queryVar()url()signedUrl()validKey()canView()maybeRender()html()
inc/_np/Receipts.php

RelatedListings RelatedListings.php​

"Related listings" — a row of up to four extra listings shown as FULL listing cards (the same canonical search-result card) underneath the main single listing. Each card links through to that listing. In demo / preview mode it's populated from the previewed design's own on-brand cards (each carrying a virtual demo single URL), so a showroom single page always reads as rich; on a live site it collects other published listings in the same category, and hides itself when there aren't enough to be worthwhile. The card itself comes from the active fork's ListingCard (Theme::forkClass), so the strip always matches that theme's search cards exactly — one shared class serves every fork template that doesn't override it (_ll has its own).

API
items()render()css()
inc/_np/RelatedListings.php

ReviewExtras ReviewExtras.php​

Reviews upgrade (Booking Theme, owner 2026-10-01, after the competitor teardown). - Aspect scores: three optional 1–5 ratings next to the overall stars — Service, Cleanliness, Value by default, renamable in Settings > Bookings. The summary shows each aspect's average. - Photos: a member may attach up to 4 photos to a review. Uploaded straight away (members only), kept UNATTACHED (post_parent 0) so they never join the listing's own gallery, claimed by the review on submit, and deleted with it. A "Client photos" strip sits over the list and opens a viewer. - Filters + sort: chips (All / Verified / With photos), star rating, service and team member selects, sort Newest / Highest / Lowest, 10 at a time with "Show more". Client-side over the rendered list. - Owner replies: the listing's owner (or an admin) posts ONE public reply under each review, editable/removable, shown as "Response from <listing>". Stored as a child comment of type review_reply, so it never counts towards ratings. Ppt_np\Reviews calls into this at its render points and on save.

API
register()enabled()aspects()save()onDelete()ajaxPhoto()ajaxReply()canReply()reply()aspectStats()renderAspectSummary()renderFormFields()renderListHead()cardAttrs()renderCardExtras()replyHtml()assets()
inc/_np/ReviewExtras.php

Reviews Reviews.php​

Listing reviews — built on native WordPress comments so they moderate and store like any comment, but stamped with the comment type "review" (the identifier that distinguishes them from ordinary blog comments in the admin) and a 1–5 star rating saved as comment meta. Reviews are always enabled on the listing CPT. Rendered in the single-listing "Reviews" (summary + breakdown + cards) and "Add Review" (star input + comment form) sections.

API
register()open()stampType()validateReview()saveRating()reviewableBooking()staffName()onCommentChange()onStatusChange()syncPostRating()count()all()stats()stars()renderSummary()renderList()renderForm()
inc/_np/Reviews.php

Routes Routes.php​

The Nonprofit theme's pages and entry points: - its virtual page slots (added through ppt_page_slugs, so they exist on this profile only): /donate/, /fundraise/, /events/, /volunteer/ and /sermons/; - content(): the one place Frontend\Pages::content() asks this fork for a page — the donation retry at /checkout/?give=…, the GIVE- return at /callback/, and the fork's own slots; - the [ppt_donate campaign="ID" fund="slug"] shortcode.

API
register()pages()slugs()designPages()designIcons()content()shortcode()
inc/_np/Routes.php

Schema Schema.php​

The Nonprofit theme's four tables, created with dbDelta once per schema version (option ppt_np_db, the Bookings::DB_OPT pattern): {prefix}ppt_np_gifts one row per successful CHARGE — a one-off gift, the first charge of a monthly gift and every renewal after it, plus owner-entered offline gifts. Unique on (order_id, event_id): a renewal reuses its order id with the gateway's own payment id, so a retried webhook can never write a second row. Rows with no order (offline, demo) carry a unique event_id of their own for the same reason. {prefix}ppt_np_pledges one row per monthly gift (the recurring order). {prefix}ppt_np_tickets one row per ticket line of an event order. {prefix}ppt_np_shifts one row per volunteer sign-up. Money columns are decimal(12,2) in the gift's own currency (char(3)).

API
register()gifts()pledges()tickets()shifts()install()ensure()
inc/_np/Schema.php

SearchPage SearchPage.php​

Server-rendered search / archive for the listing post type — the clean PPT rebuild of DT10's PPT\Custom\Search\SearchPage. Runs the normal WordPress main query (WP_Query) via official hooks (pre_get_posts + template_include) so it is SEO-friendly and every core filter applies. Contexts it owns: - keyword search (/?s=…) - the listing post-type archive (/listing/) - listing category / tag archives Filters (GET, all optional, combinable): s keyword tax-listing_category category term id price1 / price2 min / max price (numeric, price meta) sort featured | popular | newest | oldest | rating | price_low | price_high | title paged pagination

API
register()showEnded()selectedTypes()campaignTaxQuery()openMetaQuery()endedToggle()endingOrderClauses()raisedOrderClauses()shapeQuery()nearCoords()radiusKm()distanceFrom()distanceLabel()radiusClausesMain()radiusClauses()featuredOrderClauses()popularOrderClauses()isListingContext()postType()isMapTakeover()categoryTax()terms()taxQueryFromRequest()listingTaxonomies()
inc/_np/SearchPage.php

Sermons Sermons.php​

Sermons and series (F18). Sermon page under the title: the player (oEmbed first — YouTube, Vimeo …; else an <audio> player for MP3/M4A/OGG/WAV or a <video> player for MP4/WebM; else a plain "Listen / watch" link), then speaker · scripture · date and the series; in the sidebar: a "Give" card to the general fund. /sermons/ latest first (by sermon date), filtered by series (?series=slug) and speaker (?speaker=name). The fields are saved by CampaignFields (media URL validated as http/https there).

API
register()fields()metaLine()player()ids()latestId()speakers()afterTitle()sidebar()archiveUrl()renderArchive()styles()
inc/_np/Sermons.php

ServiceTimes ServiceTimes.php​

Service times (F19): the weekly timetable a church or faith group shows on its site. Admin Fundraising ▸ Settings ▸ "Service times": up to 12 rows of day, time, name and a location note (option ppt_np_service_times). Footer one short line through ppt_footer_links ("Services: Sun 9:00, Sun 10:30, Wed 7:00"), linking to the contact page — only when rows exist. Blocks the ServiceTimes block and the Commons design's visit box read rows().

API
register()days()rows()save()settingsCard()footerLink()
inc/_np/ServiceTimes.php

Settings Settings.php​

The Nonprofit theme's own settings (spec section 8), one option per key, each with its factory default. Read through get(); the Fundraising ▸ Settings screen writes them. No payment-gateway settings live here on purpose: the site uses whichever gateway plugins the owner enables on the normal Payments screen.

API
defaults()get()on()num()orgName()amounts()
inc/_np/Settings.php

SingleListing SingleListing.php​

Single-listing page for the listing_type post type — the clean PPT rebuild of DT10's PPT\Custom\Single\SingleListing. Owns template_include for singular listings and renders its own token-styled template inside the normal main loop. No legacy $CORE / membership plumbing; standard WordPress access rules apply.

API
register()demoBookingProvider()template()related()
inc/_np/SingleListing.php

SiteCopy SiteCopy.php​

Nonprofit wording on shared chrome that has no per-profile copy of its own. - The single page's archive crumb reads the profile's plural ("Campaigns") rather than the shared template's fixed "Listings" (ppt_single_crumb_label). - The header's default call to action is giving, not signing up: a header variant with no CTA of its own gets "Donate" pointing at the donate page slot (/donate/). A CTA a design names itself ("Volunteer", "Plan your visit") is left exactly as the design wrote it. Done on the RESOLVED spec (ppt_header_spec), so it holds for every header source (design, site override, no design yet) without touching the shared ppt_header_cta_url, which also feeds the "register" links. The button stays for signed-in visitors too (ppt_header_cta_always).

API
register()donateCta()
inc/_np/SiteCopy.php

Sold Sold.php​

"Sold" status — a shared, cross-theme concept: a listing/item can be marked as sold, which shows a "Sold" ribbon on its card and single page, and (when the admin opts in) drops it from search results. Deliberately thin and fork-agnostic, mirroring the other shared directory helpers: the state is a single post-meta flag (self::META), toggled from the admin and member listing editors. Any fork can adopt it — the visible ribbon is wired per fork (see each fork's ListingCard + the global single template), while the search-hide and the save both run from here so they behave the same everywhere. self::applies() gates which profiles currently expose the toggle.

API
isSold()set()saveFromForm()applies()label()hideFromSearch()searchMetaClause()
inc/_np/Sold.php

TermMeta TermMeta.php​

Per-term IMAGE + ICON for the listing taxonomies. Adds two custom fields to the native WordPress term add/edit screens (Categories, Tags and any custom listing taxonomy): a media-library image picker and a swatch picker over the theme icon library. The values are stored as term meta and consumed by the live-category blocks (via Blocks\Support\Categories) so an admin can give each category a branded photo and icon instead of the auto-derived listing photo / fallback glyph. Admin-only UI; the read helpers (image()/imageId()/icon()) are safe to call anywhere. Taxonomies are resolved live from the listing post type, so new custom taxonomies get the fields automatically.

API
register()hookTaxonomies()taxonomies()imageId()image()icon()assets()addFields()editFields()save()
inc/_np/TermMeta.php

Uploads Uploads.php​

Listing media uploads — the AJAX backend for the Uppy uploader on the listing editor (replacing the WordPress media frame). Uppy's XHRUpload posts one file per request to ppt_listing_upload; each becomes a normal WordPress attachment (so Media::gallery()/Media::videos() and the single-listing template keep working). Files are left unattached (post_parent = 0) until the listing form is saved, which parents the final set (ListingEditor::save()). A companion ppt_listing_media_remove deletes a freshly uploaded, not-yet- saved attachment when the user removes it in the editor, so abandoned uploads don't pile up in the media library.

API
register()imageMimes()videoMimes()ajaxUpload()ajaxRemove()ajaxPoster()ajaxPersist()
inc/_np/Uploads.php

Volunteers Volunteers.php​

Volunteer sign-ups and shift rotas (F13). Role page under the title: the requirements line; in the sidebar: every upcoming shift ("Sat 14 Nov, 9:00 to 12:00 · 3 of 6 places left", full shifts disabled) and a sign-up form (name, email, phone; filled in when signed in). /volunteer/ open roles; also the cancel page — the link in every email opens /volunteer/?cancel=<token>, which asks once ("Free up my place") so a mail scanner following the link cannot cancel anyone. Reminders an hourly cron sends np_shift_reminder once (the reminded flag is claimed with a conditional UPDATE) to each confirmed volunteer whose shift starts 20 to 28 hours from now. Admin Fundraising ▸ Volunteers: pick a role, see the roster per shift, remove a volunteer, download a CSV. Rules: a sign-up is checked and written under a per-role lock, so the confirmed count never passes needed; one confirmed sign-up per email per shift; past shifts take no sign-ups.

API
register()schedule()shift()startTs()label()confirmed()placesLeft()upcomingShifts()openRoleIds()upcoming()cancelUrl()afterTitle()sidebar()picker()signup()byToken()cancel()handle()handleCancel()sendReminders()renderList()roster()adminTab()handleAdmin()
inc/_np/Volunteers.php