Skip to content

Sponsors list block - #1122

Open
cjyabraham wants to merge 9 commits into
mainfrom
sponsors-list
Open

Sponsors list block#1122
cjyabraham wants to merge 9 commits into
mainfrom
sponsors-list

Conversation

@cjyabraham

@cjyabraham cjyabraham commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Test page
Test Google Sheet
Test Google Sheet CSV

Initial prompt:

Make a plan for a new Gutenberg block that lists sponsors. First, the editor will be able to specify a list of sponsors in a Google Sheet with the following columns:
- Sponsor Name - free text field
- Post ID - number corresponds with post ID for the corresponding Sponsor CPT post ID
- Level - free text field
- Description - free text field
- Categories - comma-delimited list of categories
- Booth Location - free text field

There may be many (up to 100) sponsors added to a particular block from the Google Sheet. The sheet will be published publicly and the url of the CSV provided to the block to import. To keep things simple, the data does not need to be editable within the Gutenberg editor. If edits need to happen, they’ll happen on the Sheet and the import button pressed again. Sponsors from the sheet will be matched to a corresponding Sponsor CPT post by ID and then the logo and url from that post is used for the front-end display. Skip unmatched rows and report them. 

For the actual listing on the front-end of the site, we want it to be filterable by category and level. The listing should show sponsor name, logo, level, booth location, and categories. The list is sorted alphabetically by sponsor name. Each sponsor can be clicked to display a popup with the full information about the sponsor including their website url.  

Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
Signed-off-by: Chris Abraham <cjyabraham@gmail.com>
Copilot AI balanced review requested due to automatic review settings July 21, 2026 18:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Comment on lines +50 to +51
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>
Copilot AI review requested due to automatic review settings August 11, 2026 17:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (_n plus sprintf) 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.length is zero and this branch hides ImportReport, 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() and get_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.js directly imports @wordpress/api-fetch, but this package does not declare it and currently relies on @wordpress/scripts installing/hoisting it transitively. A clean install can stop resolving the import when that transitive graph changes. Add @wordpress/api-fetch as 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>
Copilot AI review requested due to automatic review settings August 11, 2026 17:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (_n plus sprintf) 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-label does not reliably name this filter set because a plain div has the generic role, which cannot be named. Give the container role="group" (or use a fieldset with 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 when skippedCount is 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>
Copilot AI review requested due to automatic review settings August 11, 2026 18:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ImportReport because 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/i18n pluralization/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>
Copilot AI review requested due to automatic review settings August 11, 2026 19:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.length is zero, so this branch hides ImportReport and 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>
Copilot AI review requested due to automatic review settings August 11, 2026 19:55
Signed-off-by: Chris Abraham <cjyabraham@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_size truncates 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 false disables 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( ',', '"', '\\' );

Copilot AI review requested due to automatic review settings August 11, 2026 19:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_size truncates 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 alt value.
			'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 false suppresses them on every card; categories are only visible after opening the modal. Use the helper's default true value 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__in SQL 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>
Copilot AI review requested due to automatic review settings August 11, 2026 20:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 false suppresses 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 ImportReport rendered only in the non-empty branch, so editors cannot see the requested skipped-row reasons. Render the report in this empty state too, guarded by importedAt to 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`;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants