Sponsors list block - #1122
Conversation
Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
There was a problem hiding this comment.
Pull request overview
Adds a dynamic Gutenberg sponsor directory block backed by Google Sheets CSV data.
Changes:
- Adds authenticated CSV importing and validation.
- Renders filterable sponsor cards with modal details.
- Adds editor UI, frontend assets, filtering tests, and project configuration.
Reviewed changes
Copilot reviewed 16 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
.gitignore |
Allows plugin files while excluding dependencies. |
phpcs.xml |
Adds the plugin to PHP linting. |
sponsor-directory-block.php |
Bootstraps block and REST route. |
package.json |
Defines build, lint, and test tooling. |
includes/class-sponsor-directory-importer.php |
Fetches and validates CSV sponsor data. |
includes/render.php |
Server-renders filters, cards, and modals. |
src/block.json |
Defines block metadata and attributes. |
src/index.js |
Registers the dynamic block. |
src/edit.js |
Implements the CSV import editor UI. |
src/editor.scss |
Styles the editor interface. |
src/style.scss |
Styles the directory and modal. |
src/filter.js |
Implements filter matching. |
src/filter.test.js |
Tests filtering behavior. |
src/view.js |
Initializes frontend filtering. |
build/block.json |
Provides built block metadata. |
build/index.js |
Provides the compiled editor script. |
build/index.css |
Provides compiled editor styles. |
build/index.asset.php |
Declares editor dependencies. |
build/view.js |
Provides compiled frontend filtering. |
build/view.asset.php |
Declares frontend dependencies. |
build/style-index.css |
Provides compiled frontend styles. |
Files not reviewed (4)
- web/wp-content/plugins/sponsor-directory-block/build/index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/index.js: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/style-index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/view.js: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| array( | ||
| 'timeout' => 15, | ||
| 'redirection' => 3, | ||
| 'limit_response_size' => self::MAX_RESPONSE_SIZE, |
| count.textContent = | ||
| visibleCount === 1 ? '1 sponsor' : `${ visibleCount } sponsors`; |
| ob_start(); | ||
| ?> | ||
| <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>> | ||
| <div class="sponsor-directory__filters" aria-label="<?php esc_attr_e( 'Filter sponsors', 'sponsor-directory-block' ); ?>"> |
| array( | ||
| 'class' => sanitize_html_class( $class_name ), | ||
| 'loading' => 'lazy', | ||
| 'alt' => $name . ' logo', |
…retrieve all sponsors Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 22 changed files in this pull request and generated no new comments.
Files not reviewed (4)
- web/wp-content/plugins/sponsor-directory-block/build/index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/index.js: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/style-index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/view.js: Generated file
Suppressed comments (7)
web/wp-content/plugins/sponsor-directory-block/includes/class-sponsor-directory-importer.php:227
- Using backslash as
fgetcsv's escape character can corrupt valid Google Sheets CSV data—for example, a quoted free-text field containing a comma and ending in a backslash may consume the closing quote and following columns. Disable the proprietary escape mechanism for row parsing.
$row = $file->fgetcsv( ',', '"', '\\' );
web/wp-content/plugins/sponsor-directory-block/src/view.js:51
- This client-side update immediately replaces the localized PHP result count with hard-coded English and cannot support languages with different plural rules. Generate the label with
@wordpress/i18n(_nplussprintf) and rebuild the view bundle.
count.textContent =
visibleCount === 1 ? '1 sponsor' : `${ visibleCount } sponsors`;
web/wp-content/plugins/sponsor-directory-block/src/edit.js:182
- When an import succeeds but every row is invalid or unmatched,
sponsors.lengthis zero and this branch hidesImportReport, so none of the required skipped-row reasons are shown. Render the report in the empty state as well.
{ sponsors.length === 0 ? (
web/wp-content/plugins/sponsor-directory-block/includes/class-sponsor-directory-importer.php:322
- The importer appends every matching row without enforcing the 100-sponsor limit, while
lf_sponsor_directory_normalize_sponsors()later silently slices the stored attributes to 100. A 101-row import therefore reports and previews 101 sponsors but renders only 100. Enforce a shared limit during import and explicitly reject or report excess rows rather than silently dropping them at render time.
unset( $candidate['row'] );
$sponsors[] = $candidate;
web/wp-content/plugins/sponsor-directory-block/includes/class-sponsor-directory-importer.php:187
- Google Sheets emits RFC 4180-style CSV, but enabling PHP's proprietary backslash escape can misparse a quoted header whose closing quote is preceded by a literal backslash. Disable the escape character so headers are parsed according to the CSV format.
This issue also appears on line 227 of the same file.
$headers = $file->fgetcsv( ',', '"', '\\' );
web/wp-content/plugins/sponsor-directory-block/includes/class-sponsor-directory-importer.php:303
- The IDs-only query does not prime post metadata, and the following loop calls
has_post_thumbnail()andget_post_meta()for every matched sponsor. This creates an N+1 postmeta query pattern (up to 100 extra queries under the intended limit, and more while the import is uncapped). Prime the matched posts' metadata before iterating.
$matched_ids = array_fill_keys( array_map( 'intval', $matched_ids ), true );
web/wp-content/plugins/sponsor-directory-block/package.json:17
edit.jsdirectly imports@wordpress/api-fetch, but this package does not declare it and currently relies on@wordpress/scriptsinstalling/hoisting it transitively. A clean install can stop resolving the import when that transitive graph changes. Add@wordpress/api-fetchas a direct development dependency and update the lockfile.
"devDependencies": {
"@wordpress/block-editor": "^13",
…er page to retrieve all sponsors Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 22 changed files in this pull request and generated no new comments.
Files not reviewed (4)
- web/wp-content/plugins/sponsor-directory-block/build/index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/index.js: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/style-index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/view.js: Generated file
Suppressed comments (3)
web/wp-content/plugins/sponsor-directory-block/src/view.js:51
update()immediately replaces the server-rendered, translated plural string with hard-coded English, and languages with plural forms beyond singular/plural cannot translate the result count after filtering. Build this text with@wordpress/i18n(_nplussprintf) and regenerate the tracked view bundle.
count.textContent =
visibleCount === 1 ? '1 sponsor' : `${ visibleCount } sponsors`;
web/wp-content/plugins/sponsor-directory-block/includes/render.php:73
- The
aria-labeldoes not reliably name this filter set because a plaindivhas the generic role, which cannot be named. Give the containerrole="group"(or use afieldsetwith a legend) so assistive technology exposes “Filter sponsors” as the controls' group label.
<div class="sponsor-directory__filters" aria-label="<?php esc_attr_e( 'Filter sponsors', 'sponsor-directory-block' ); ?>">
web/wp-content/plugins/sponsor-directory-block/src/edit.js:175
- When an import matches zero sponsors—such as when every Post ID is unmatched—this branch hides
ImportReport, so the editor cannot see the skipped rows or reasons the import endpoint returned. Include the report whenskippedCountis nonzero (or render it independently of the sponsor preview).
{ sponsors.length === 0 ? (
- Add aria-label to sponsor card button for better screen reader support - Remove redundant sponsor name display in card - Update styles for improved layout and accessibility - Adjust JavaScript to show total sponsors and visible count dynamically Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 22 changed files in this pull request and generated no new comments.
Files not reviewed (4)
- web/wp-content/plugins/sponsor-directory-block/build/index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/index.js: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/style-index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/view.js: Generated file
Suppressed comments (5)
web/wp-content/plugins/sponsor-directory-block/src/edit.js:175
- After an import that successfully matches zero sponsors, this branch hides
ImportReportbecause the report is only rendered in the non-empty branch below. The editor therefore drops all skipped-row reasons, contrary to the requirement to report unmatched rows. Render the report after an import even when the sponsor list is empty.
{ sponsors.length === 0 ? (
web/wp-content/plugins/sponsor-directory-block/includes/render.php:133
- The card never visibly renders
sponsor.name; it is only present in the button's accessible label and the optional logo alt text. This misses the stated listing field and leaves a completely blank card when a sponsor has no logo, level, booth, or categories. Add the sponsor name to the visible card content.
<span class="sponsor-directory__card-content">
<?php lf_sponsor_directory_render_details( $sponsor ); ?>
web/wp-content/plugins/sponsor-directory-block/src/style.scss:255
- The importer explicitly allows sponsors without a featured image (with a warning), so the logo does not always identify the modal. Hiding the only rendered sponsor title leaves those modals with no visible identity and omits the sponsor name from the requested full-information view. Keep the modal title visible and account for it in the modal layout.
&-title {
// The logo already identifies the sponsor, so the title is for assistive tech only.
position: absolute;
web/wp-content/plugins/sponsor-directory-block/includes/class-sponsor-directory-importer.php:303
- Disabling the post-meta cache here causes the subsequent
has_post_thumbnail()/get_post_meta()warning checks to load metadata separately for every matched sponsor—up to roughly 100 extra database queries per import. Batch-prime metadata once after resolving the IDs.
'update_post_meta_cache' => false,
web/wp-content/plugins/sponsor-directory-block/src/view.js:56
- This client update immediately replaces the localized PHP count with hard-coded English, and every filter change remains untranslated. Use
@wordpress/i18npluralization/formatting for both count variants and rebuild the view assets so the block's text domain continues to work on localized sites.
visibleCount === totalCount
? `${ totalCount } ${
totalCount === 1 ? 'sponsor' : 'sponsors'
}`
: `Showing ${ visibleCount } of ${ totalCount } sponsors`;
Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 22 changed files in this pull request and generated no new comments.
Files not reviewed (4)
- web/wp-content/plugins/sponsor-directory-block/build/index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/index.js: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/style-index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/view.js: Generated file
Suppressed comments (4)
web/wp-content/plugins/sponsor-directory-block/src/view.js:56
- These frontend count strings bypass the plugin text domain and immediately replace the localized PHP count on every page load. Use WordPress i18n/plural helpers for both forms and rebuild the tracked assets so translated pages do not regress to English.
count.textContent =
visibleCount === totalCount
? `${ totalCount } ${
totalCount === 1 ? 'sponsor' : 'sponsors'
}`
: `Showing ${ visibleCount } of ${ totalCount } sponsors`;
web/wp-content/plugins/sponsor-directory-block/src/edit.js:181
- When an import succeeds but every row is skipped,
sponsors.lengthis zero, so this branch hidesImportReportand the editor never reports the unmatched rows. Show the report after an attempted import even when no sponsors matched.
{ sponsors.length === 0 ? (
web/wp-content/plugins/sponsor-directory-block/includes/render.php:135
- The card never renders the sponsor name as visible text and explicitly suppresses categories, although the requested listing includes both. This also leaves sponsors without featured images—which the importer permits with a warning—without a visible identifier. Render the name and include categories in the card details.
<span class="sponsor-directory__card-content">
<?php lf_sponsor_directory_render_details( $sponsor, 'sponsor-directory__details', false ); ?>
<?php if ( $sponsor['description'] ) : ?>
<span class="sponsor-directory__excerpt"><?php echo esc_html( wp_html_excerpt( $sponsor['description'], 100, '…' ) ); ?></span>
web/wp-content/plugins/sponsor-directory-block/includes/render.php:146
- The modal body has no visible sponsor name. Because missing logos only produce a warning and do not skip the sponsor, such a modal contains no visible identity at all. Add the sponsor name to the visible modal content.
<div class="sponsor-directory-modal__body">
<?php lf_sponsor_directory_render_details( $sponsor, 'sponsor-directory-modal__details' ); ?>
Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 22 changed files in this pull request and generated no new comments.
Files not reviewed (4)
- web/wp-content/plugins/sponsor-directory-block/build/index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/index.js: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/style-index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/view.js: Generated file
Suppressed comments (4)
web/wp-content/plugins/sponsor-directory-block/includes/class-sponsor-directory-importer.php:97
limit_response_sizetruncates an oversized response rather than making the request fail, but the truncated body is parsed and returned as a successful import. This can silently replace the saved snapshot with only the first portion of the sheet and never report the omitted rows. Reject a body that reaches the cap (and/or compare the response length header) before parsing it.
'timeout' => 15,
'redirection' => 3,
'limit_response_size' => self::MAX_RESPONSE_SIZE,
)
web/wp-content/plugins/sponsor-directory-block/src/view.js:56
- These hard-coded English strings immediately replace the translated PHP count on every page load and after each filter change. On localized sites, this leaves the otherwise internationalized block partly in English. Build these messages with WordPress i18n/plural APIs and regenerate the compiled view assets so the required dependency is declared.
count.textContent =
visibleCount === totalCount
? `${ totalCount } ${
totalCount === 1 ? 'sponsor' : 'sponsors'
}`
: `Showing ${ visibleCount } of ${ totalCount } sponsors`;
web/wp-content/plugins/sponsor-directory-block/includes/render.php:133
- The listing omits categories because
falsedisables them on every card, but the requested front-end listing explicitly includes each sponsor's categories. Render the shared details with categories enabled; they can still remain in the modal as well.
<?php lf_sponsor_directory_render_details( $sponsor, 'sponsor-directory__details', false ); ?>
web/wp-content/plugins/sponsor-directory-block/includes/class-sponsor-directory-importer.php:227
- Using backslash as
fgetcsv's escape character invokes PHP's non-RFC escaping. A valid Google Sheets quoted field ending in a backslash (for example, free-text description content) makes the closing quote look escaped and can merge subsequent columns, corrupting or skipping that sponsor row. Disable the proprietary escape mechanism for RFC 4180 CSV.
$row = $file->fgetcsv( ',', '"', '\\' );
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 22 changed files in this pull request and generated no new comments.
Files not reviewed (4)
- web/wp-content/plugins/sponsor-directory-block/build/index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/index.js: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/style-index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/view.js: Generated file
Suppressed comments (6)
web/wp-content/plugins/sponsor-directory-block/includes/class-sponsor-directory-importer.php:96
limit_response_sizetruncates the HTTP body rather than rejecting an oversized response. A CSV over 2 MB can therefore be parsed as a successful partial import, silently dropping later sponsors (and possibly accepting a cut-off final row). Fetch one extra byte and explicitly reject bodies above the limit before parsing.
'limit_response_size' => self::MAX_RESPONSE_SIZE,
web/wp-content/plugins/sponsor-directory-block/includes/render.php:184
- The same logo appears beside the sponsor name on the card and beneath the sponsor-named modal heading, so this alt text makes screen readers announce the name twice in both contexts. Treat these duplicated logos as decorative with an empty
altvalue.
'alt' => $name . ' logo',
web/wp-content/plugins/sponsor-directory-block/src/edit.js:181
- When an import matches zero sponsors, this branch hides
ImportReport, so unmatched rows and their reasons are not reported—the exact case where the report is most important. Render the report in the empty state after an import as well.
{ sponsors.length === 0 ? (
web/wp-content/plugins/sponsor-directory-block/includes/render.php:133
- The requested card listing includes each sponsor's categories, but passing
falsesuppresses them on every card; categories are only visible after opening the modal. Use the helper's defaulttruevalue here so the listing itself contains them.
<?php lf_sponsor_directory_render_details( $sponsor, 'sponsor-directory__details', false ); ?>
web/wp-content/plugins/sponsor-directory-block/src/view.js:56
update()runs immediately and replaces the server-rendered, translated_n()count with hard-coded English. This makes the result count English on every localized site, both initially and after filtering; build these messages through WordPress i18n (including plural handling) and regenerate the view bundle.
visibleCount === totalCount
? `${ totalCount } ${
totalCount === 1 ? 'sponsor' : 'sponsors'
}`
: `Showing ${ visibleCount } of ${ totalCount } sponsors`;
web/wp-content/plugins/sponsor-directory-block/includes/class-sponsor-directory-importer.php:291
- The 2 MB byte cap does not bound the number of rows. A compact sheet can produce tens of thousands of IDs here, resulting in a very large
post__inSQL query and equally large block attributes, despite the stated 100-sponsor scope. Enforce the 100-row/sponsor limit before constructing this query and report excess rows.
$post_ids = wp_list_pluck( $candidates, 'postId' );
$matched_ids = empty( $post_ids ) ? array() : get_posts(
…sor details Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 22 changed files in this pull request and generated no new comments.
Files not reviewed (4)
- web/wp-content/plugins/sponsor-directory-block/build/index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/index.js: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/style-index.css: Generated file
- web/wp-content/plugins/sponsor-directory-block/build/view.js: Generated file
Suppressed comments (3)
web/wp-content/plugins/sponsor-directory-block/includes/render.php:133
- The PR requires categories to appear on each front-end sponsor card, but passing
falsesuppresses them because the helper only emits categories when its third argument is truthy. Remove this argument so the listing includes categories as specified; the modal output remains unchanged.
<?php lf_sponsor_directory_render_details( $sponsor, 'sponsor-directory__details', false ); ?>
web/wp-content/plugins/sponsor-directory-block/src/edit.js:175
- When a valid import matches zero Sponsor posts, this branch hides the
ImportReportrendered only in the non-empty branch, so editors cannot see the requested skipped-row reasons. Render the report in this empty state too, guarded byimportedAtto avoid showing the default 0/0 report before the first import.
{ sponsors.length === 0 ? (
web/wp-content/plugins/sponsor-directory-block/src/view.js:56
update()runs immediately and replaces the server-rendered, translated_n()count with hard-coded English, so this block loses localization even before a filter is changed. Build both count formats with@wordpress/i18n(including plural handling) and regenerate the view bundle and asset dependencies.
count.textContent =
visibleCount === totalCount
? `${ totalCount } ${
totalCount === 1 ? 'sponsor' : 'sponsors'
}`
: `Showing ${ visibleCount } of ${ totalCount } sponsors`;
Test page
Test Google Sheet
Test Google Sheet CSV
Initial prompt: