Skip to content

Feature/user directory elementor widget#1825

Open
sapayth wants to merge 25 commits intoweDevsOfficial:developfrom
sapayth:feature/user_directory_elementor_widget
Open

Feature/user directory elementor widget#1825
sapayth wants to merge 25 commits intoweDevsOfficial:developfrom
sapayth:feature/user_directory_elementor_widget

Conversation

@sapayth
Copy link
Member

@sapayth sapayth commented Mar 4, 2026

depends on #1778

closes #1410, closes #1445, closes #1446

Summary

This PR adds an Elementor widget so the directory can be embedded anywhere using the Elementor page builder — no shortcodes required.

What's New

  • Elementor widget: A dedicated "User Directory" widget is available in Elementor's widget panel, allowing full visual control over placement and styling.

Technical Notes

  • New Elementor widget class: WeDevs\Wpuf\Integrations\Elementor\User_Directory_Widget

Summary by CodeRabbit

Release Notes

  • New Features
    • Added User Directory module for displaying customizable user listings and profiles
    • Introduced search, filtering, and sorting capabilities for user directories
    • Added shortcode and Elementor widget support for placing directories on any page
    • Enabled free module activation and deactivation from the admin interface
    • Multiple directory layout options with responsive design and pagination support

arifulhoque7 and others added 25 commits November 28, 2025 23:59
Implements a limited User Directory in the free version, including setup wizard UI, REST API, shortcode handler, and directory limit logic. Adds new assets, React components, admin menu, post type registration, and views for directory/profile layouts. Pro-only features are previewed with disabled states and upgrade prompts, following the Pro Preview pattern.
Added ! prefix to purple color classes in layout helpers and Tailwind config to ensure higher specificity. Updated safelist in Tailwind config to include both prefixed and non-prefixed classes, and adjusted button class usage for consistency.
Improves member count calculation in the REST API and displays live member counts in the directory list. Refines input styles for number fields, updates the toast notification component for better UX, and adds a Pro badge to the 'New Directory' button when the directory limit is reached.
Introduces a new 'Files' tab to the user profile with grouped file display and filtering, excluding private message attachments. Updates Gruntfile.js to add user directory build/watch tasks and ensures profile size is passed to the template. Also adds the corresponding template file for the new tab.
Introduced a wide range of new utility classes to form-builder.css for layout, spacing, sizing, display, border, background, and other style properties. These additions improve flexibility and consistency for admin UI components and support more granular styling options.
Moved user directory assets and configs to modules/user-directory, updated Gruntfile.js to reflect new paths, and adjusted build/watch tasks. Renamed Tailwind and Webpack configs, added PostCSS config, and updated package files to support the new module structure.
Updated sort option values from 'ID' to 'id' and adjusted related logic for consistency. Removed free/pro checks from handlers to simplify state updates. Improved UI/UX for avatar and sort options, added Pro-locked features with badges and tooltips, and enhanced descriptions for better clarity.
Changed primary color scheme from emerald to purple across helpers and Tailwind config. Updated profile layout-2 to add a cover photo section, adjust class names, and improve header overlap. Cleaned up the About tab by removing Pro upgrade prompts and related UI elements.
Changed Tailwind primary and hover colors from purple to emerald green in tailwind.config.js. Updated related CSS class in App.js to use emerald color for upgrade link, ensuring visual consistency with the new color scheme.
Updated avatar rendering to use the same logic as the directory listing, prioritizing custom profile photos, then Gravatar, and finally user initials as a fallback. Improved initials calculation and font sizing for better visual consistency. Simplified markup to show either the avatar image or initials, not both, and ensured consistent sizing and styling.
Add early return if there are no items to display in the pagination shortcode. Also update the current page styling to use layout-specific color classes for better theme consistency.
Refactored multiple files in the user-directory module, including Admin_Menu.php, Directory.php, Helpers.php, Post_Type.php, PrettyUrls.php, Shortcode.php, and profile layout. Updated Toast.js in the frontend and regenerated several minified CSS files to reflect style changes.
Updated both Directory API and Shortcode to skip applying the 'role__in' filter if 'all' is present in the roles array. This prevents unnecessary filtering when all roles should be included.
Updated default and saved settings to include all profile tabs for better Pro compatibility. Improved REST API to merge settings with defaults and existing values, and to sanitize and save all profile tab-related fields. Adjusted frontend JS and wizard defaults to reflect all tabs, and fixed minor issues with sort option selection and profile slug decoding.
Introduce a reusable SingleSelect dropdown and refactor User Directory UI/UX. Key changes:

- Add src/js/user-directory/components/common/SingleSelect.js: new reusable single-select dropdown with grouped options and ProBadge support.
- Refactor StepAdvanced to use SingleSelect for sort and gallery size, simplify Pro feature handling, change free avatar default from 192→128 and adjust avatar size isFree flags, and convert several hover-only Pro badges into clickable upgrade links.
- Update StepLayout to show Pro badge on hover with an upgrade link, simplify border/opacity logic and add local hover state.
- Update App.js ProBadge into a clickable upgrade link and reposition/remove some hover-only Pro UI (New Directory button now shows badge inline when limit reached; limit warning block removed/streamlined).
- package.json: ensure user-directory build/dev scripts run npm ci before npm run build/dev.
- Remove get_current_page() from modules/user-directory/Shortcode.php (cleanup).

These changes centralize dropdown logic, improve upgrade CTA consistency, and tidy UI behavior for Pro-locked features.
Replace the native <select> for the profile_base field with a SingleSelect component, passing profileBases as options and adapting the onChange to call the existing handleChange ({ target: { name, value } }). Remove the now-unused inputStyle constant and its comment block. This simplifies the markup and delegates select rendering/styling to the SingleSelect component.
Safelist new button and focus utilities in the user-directory Tailwind config and update DirectoryWizard and StepTabs components. DirectoryWizard: replace verbose button classnames with wpuf-btn-white, add focus ring utilities to Cancel/Prev/Next/Next (submit) buttons for better accessibility, adjust the footer container style to ensure correct left/right positioning and enforce background color. StepTabs: only render the ProBadge on hover to reduce visual clutter. These changes centralize button styles and improve keyboard focus visibility and layout consistency.
…ro-functionality' into feature/user_directory_elementor_widget

# Conflicts:
#	assets/css/admin/subscriptions.min.css
#	assets/css/ai-form-builder.min.css
#	assets/css/forms-list.min.css
#	assets/css/frontend-subscriptions.min.css
@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

Walkthrough

This PR introduces a comprehensive free-version User Directory module for WP User Frontend. It adds admin UI for creating and managing directories, REST API endpoints for CRUD and user search, frontend rendering via shortcodes and Elementor widgets, Tailwind-based styling, and supporting build infrastructure. The module includes profile layouts, pagination, search, sorting, and role-based filtering capabilities.

Changes

Cohort / File(s) Summary
Build Configuration
Gruntfile.js, package.json, modules/user-directory/webpack.config.js, modules/user-directory/tailwind.config.js, modules/user-directory/postcss.config.js, modules/user-directory/package.json, postcss.user-directory.config.js, tailwind.config.js
Webpack, Tailwind, and PostCSS build setup for user-directory module; new npm build scripts and Grunt tasks for watching and compiling user-directory assets.
Admin UI Components
src/js/user-directory/App.js, src/js/user-directory/components/DirectoryList.js, src/js/user-directory/components/DirectoryWizard.js, src/js/user-directory/components/steps/*
React-based multi-step directory creation wizard, directory list management, and step-by-step form handling (Basics, Layout, Profile, Tabs, Advanced).
Common UI Components
src/js/user-directory/components/common/*
Reusable React components (Header, LayoutCard, SingleSelect, MultiSelect, Toast, Tooltip, DeleteConfirmModal) with Pro badge integration and accessibility features.
Module Core
modules/user-directory/User_Directory.php, modules/user-directory/Post_Type.php, modules/user-directory/Shortcode.php, modules/user-directory/Api/Directory.php
Orchestrator class, custom post type registration, shortcode rendering (listing and profile views), REST API controller with CRUD and search endpoints.
Admin & Integration
modules/user-directory/Admin_Menu.php, modules/user-directory/Integrations/Elementor/Elementor.php, modules/user-directory/Integrations/Elementor/User_Directory_Widget.php, includes/Free/Free_Loader.php, includes/Integrations.php
Admin menu UI, Elementor integration with custom widget, free module toggle handling, and Free Loader updates for conditional module loading.
Frontend Rendering
modules/user-directory/PrettyUrls.php, modules/user-directory/DirectoryStyles.php, modules/user-directory/Helpers.php
URL rewriting for profiles, dynamic CSS injection, and comprehensive helper functions for avatars, profile data, layouts, and social links.
Frontend Templates
modules/user-directory/views/directory/layout-3.php, modules/user-directory/views/directory/template-parts/*, modules/user-directory/views/profile/layout-2.php, modules/user-directory/views/profile/template-parts/*
PHP templates for directory listing grid, user cards, search/sort controls, pagination, profile pages with tabs, and reusable tab partials (About, Posts, Comments, Files).
Frontend Assets
assets/js/wpuf-user-directory-frontend.js, assets/js/ud-search-shortcode.js, assets/js/wpuf-user-directory-free.asset.php, assets/js/wpuf-user-directory-free.js.LICENSE.txt, assets/css/wpuf-user-directory-frontend.css, assets/css/elementor-user-directory.css, src/js/user-directory/styles/main.css, src/js/user-directory/index.js, src/js/user-directory/utils/avatarSizeHelper.js
Frontend JS for search, pagination, form submission; CSS for user cards, grids, profile sections, Elementor editor states; admin style compilation; utility helpers.
Module System
includes/functions/modules.php, assets/js/admin/wpuf-module.js, assets/css/admin/wpuf-module.css
Free module registry, activation/deactivation functions, AJAX toggle support for free modules, and admin UI styling updates.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • weDevsOfficial/wpuf-pro#715: Implements the multi-step User Directory creation and management workflow (App, DirectoryWizard, Step components, REST routes, admin React UI) that matches the feature request.

Possibly related PRs

Suggested labels

needs: dev review

Poem

🐰 A burrow of users, now displayed with flair,
Directories listed, profiles laid bare,
With avatars bouncing and cards in a grid,
Search and sort magic—what a wonderful bid!
React wizards conjure, and templates entice,
This rabbit hops gladly through user directories thrice! 🎉

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟡 Minor comments (19)
src/js/user-directory/components/common/Toast.js-72-79 (1)

72-79: ⚠️ Potential issue | 🟡 Minor

Duplicate dismiss logic risks double onClose invocation.

The close button duplicates the auto-dismiss logic. If a user clicks close while the auto-dismiss timer is in flight, onClose may fire twice. Extract the dismiss logic into a shared handler and guard against duplicate calls.

🔧 Proposed fix reusing the shared handler
                     <button
-                        onClick={() => {
-                            setIsLeaving(true);
-                            setTimeout(() => {
-                                setIsVisible(false);
-                                onClose && onClose();
-                            }, 300);
-                        }}
+                        onClick={handleClose}
                         className="wpuf-ml-4 wpuf-flex-shrink-0 wpuf-inline-flex wpuf-bg-transparent wpuf-border-0 wpuf-p-0 wpuf-text-white hover:wpuf-text-gray-200 focus:wpuf-outline-none wpuf-cursor-pointer"
                     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/Toast.js` around lines 72 - 79,
Extract the dismiss sequence into a single handler (e.g., dismissToast) and
replace the duplicated inline logic in the close button and the auto-dismiss
timer with calls to that handler; inside dismissToast call setIsLeaving(true),
start the 300ms timeout to setIsVisible(false) and call onClose(), and guard
against double invocation with a local ref/flag (e.g., dismissedRef or
isDismissing) so subsequent calls are no-ops; update places that previously
called setIsLeaving/setIsVisible/onClose directly to call dismissToast instead
and ensure the flag is checked/set atomically to prevent duplicate onClose
calls.
src/js/user-directory/components/common/LayoutCard.js-73-81 (1)

73-81: ⚠️ Potential issue | 🟡 Minor

Avoid # as fallback for upgradeUrl.

When upgradeUrl is not provided, the link defaults to #, which scrolls to the page top—confusing for users. Consider hiding the link entirely or using a more meaningful fallback.

🛠️ Proposed fix to conditionally render the upgrade link
-                    <a
-                        href={upgradeUrl || '#'}
-                        target="_blank"
-                        rel="noopener noreferrer"
-                        className="wpuf-px-4 wpuf-py-2 wpuf-bg-indigo-600 wpuf-text-white wpuf-text-xs wpuf-font-medium wpuf-rounded-md hover:wpuf-bg-indigo-700 wpuf-no-underline wpuf-transition-colors"
-                        onClick={(e) => e.stopPropagation()}
-                    >
-                        {i18n?.upgrade_to_pro || __('Upgrade to Pro', 'wp-user-frontend')}
-                    </a>
+                    {upgradeUrl && (
+                        <a
+                            href={upgradeUrl}
+                            target="_blank"
+                            rel="noopener noreferrer"
+                            className="wpuf-px-4 wpuf-py-2 wpuf-bg-indigo-600 wpuf-text-white wpuf-text-xs wpuf-font-medium wpuf-rounded-md hover:wpuf-bg-indigo-700 wpuf-no-underline wpuf-transition-colors"
+                            onClick={(e) => e.stopPropagation()}
+                        >
+                            {i18n?.upgrade_to_pro || __('Upgrade to Pro', 'wp-user-frontend')}
+                        </a>
+                    )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/LayoutCard.js` around lines 73 - 81,
The anchor in LayoutCard currently falls back to href="#" when upgradeUrl is
falsy which causes unwanted scrolling; change the JSX to conditionally render
the upgrade link only when upgradeUrl is truthy (or explicitly hide it) instead
of using "#" as a fallback, keeping the same attributes (target, rel, className,
onClick) and i18n label; locate the anchor in the LayoutCard component and
replace the unconditional <a href={upgradeUrl || '#'} ...> with a conditional
render that only outputs the anchor when upgradeUrl is provided.
src/js/user-directory/utils/avatarSizeHelper.js-14-14 (1)

14-14: ⚠️ Potential issue | 🟡 Minor

Misleading comment: layout-6 has the largest avatar size, not medium.

The comment says "Grid layout - medium avatars" but 265 is the largest size in the map. This could confuse future maintainers.

-        'layout-6': '265'   // Grid layout - medium avatars
+        'layout-6': '265'   // Grid layout - large avatars
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/utils/avatarSizeHelper.js` at line 14, The comment for
the 'layout-6' entry in avatarSizeHelper.js is misleading—'layout-6': '265' is
the largest avatar size, not "medium"; update the comment next to the 'layout-6'
mapping (in the avatar size map or function that returns sizes) to accurately
reflect that this is the largest grid avatar size (e.g., "Grid layout - largest
avatars") so future maintainers aren’t confused.
src/js/user-directory/components/common/Tooltip.js-3-5 (1)

3-5: ⚠️ Potential issue | 🟡 Minor

Unstable tooltip ID on each render and deprecated substr usage.

  1. tooltipId is regenerated on every render, which can cause accessibility issues with screen readers when the tooltip becomes visible (the ID changes between renders).
  2. String.prototype.substr() is deprecated; use substring() or slice() instead.
🔧 Proposed fix using useMemo for stable ID
-import { useState } from '@wordpress/element';
+import { useState, useMemo } from '@wordpress/element';

 const Tooltip = ( { content, children, className = '' } ) => {
     const [visible, setVisible] = useState(false);
-    const tooltipId = `wpuf-tooltip-${Math.random().toString(36).substr(2, 9)}`;
+    const tooltipId = useMemo(
+        () => `wpuf-tooltip-${Math.random().toString(36).slice(2, 11)}`,
+        []
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/Tooltip.js` around lines 3 - 5,
Tooltip currently generates tooltipId on every render and uses deprecated
String.prototype.substr; change tooltipId to a stable value created once per
component lifetime (e.g., useRef or useMemo) and replace substr(2, 9) with
slice(2, 11) or substring(2, 11) to avoid deprecated API; update the Tooltip
component to compute tooltipId via useRef/useMemo so it doesn't change across
renders and use slice/substring for the character extraction.
src/js/user-directory/components/common/DeleteConfirmModal.js-47-58 (1)

47-58: ⚠️ Potential issue | 🟡 Minor

Localize action button labels.

At Line 50 and Line 57, Cancel/Delete are hardcoded and skip translation.

Proposed fix
                     <button
                         onClick={onCancel}
                         className="wpuf-w-[101px] wpuf-h-[50px] wpuf-rounded-md wpuf-border wpuf-border-gray-300 wpuf-bg-white wpuf-text-gray-700 wpuf-font-medium hover:wpuf-bg-gray-50 wpuf-pt-[13px] wpuf-pb-[13px] wpuf-pl-[25px] wpuf-pr-[23px] wpuf-text-[16px] wpuf-leading-[24px]">
-                        Cancel
+                        {__( 'Cancel', 'wp-user-frontend' )}
                     </button>
@@
                     <button
                         onClick={onConfirm}
                         className="wpuf-w-[151px] wpuf-h-[50px] wpuf-rounded-md wpuf-bg-[`#EF4444`] wpuf-text-white wpuf-font-medium wpuf-shadow-sm hover:wpuf-bg-red-600 wpuf-pt-[13px] wpuf-pb-[13px] wpuf-pl-[25px] wpuf-pr-[25px] wpuf-text-[16px] wpuf-leading-[24px]"
                         style={{ boxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05)' }}>
-                        Delete
+                        {__( 'Delete', 'wp-user-frontend' )}
                     </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/DeleteConfirmModal.js` around lines
47 - 58, Replace the hardcoded button labels "Cancel" and "Delete" in the
DeleteConfirmModal component with localized strings; update the two buttons (the
one with onClick={onCancel} and the one with onClick={onConfirm}) to use your
app's i18n function (e.g., t('cancel') and t('delete')) or accept translated
label props, and ensure you import or obtain the translator (e.g.,
useTranslation or passed-in t) at the top of DeleteConfirmModal so both labels
are rendered via the translation function instead of literal text.
modules/user-directory/views/directory/template-parts/pagination-shortcode.php-41-43 (1)

41-43: ⚠️ Potential issue | 🟡 Minor

Clamp and default current_page before pagination math.

At Line 41, direct casting from $pagination['current_page'] can produce out-of-range values and invalid prev/next URLs. Normalize it to [1..$total] and default safely when missing.

Proposed fix
-$current = (int) $pagination['current_page'];
-$total   = (int) $pagination['total_pages'];
+$total   = max( 1, (int) $pagination['total_pages'] );
+$current = isset( $pagination['current_page'] ) ? (int) $pagination['current_page'] : 1;
+$current = max( 1, min( $current, $total ) );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/user-directory/views/directory/template-parts/pagination-shortcode.php`
around lines 41 - 43, Normalize and clamp the current page before using it in
pagination math: ensure $total is set from $pagination['total_pages'] then set
$current from $pagination['current_page'] with a safe default (1) and clamp it
to the range 1..$total to avoid out-of-range prev/next URLs; update the logic
around the $current and $total variables in pagination-shortcode.php so all
downstream calculations use the clamped $current value.
src/js/user-directory/components/common/Header.js-17-17 (1)

17-17: ⚠️ Potential issue | 🟡 Minor

Build upgradeUrl with encoded query params.

At Line 17, manual concatenation with raw utm can produce malformed URLs if utm contains reserved characters.

Proposed fix
-    const upgradeUrl = (wpuf.upgradeUrl || 'https://wedevs.com/wp-user-frontend-pro/pricing/') + '?utm_source=' + utm + '&utm_medium=wpuf-header';
+    const upgradeBase = wpuf.upgradeUrl || 'https://wedevs.com/wp-user-frontend-pro/pricing/';
+    const upgrade = new URL( upgradeBase );
+    upgrade.searchParams.set( 'utm_source', utm );
+    upgrade.searchParams.set( 'utm_medium', 'wpuf-header' );
+    const upgradeUrl = upgrade.toString();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/Header.js` at line 17, The upgradeUrl
is constructed by string concatenation and can break when utm contains reserved
characters; update the build for upgradeUrl (the const upgradeUrl that uses
wpuf.upgradeUrl and utm) to assemble query parameters using URLSearchParams or
encodeURIComponent for utm and other params so values are percent-encoded, e.g.
derive base = wpuf.upgradeUrl ||
'https://wedevs.com/wp-user-frontend-pro/pricing/' then append a properly
encoded query string (utm_source and utm_medium) instead of concatenating raw
utm.
src/js/user-directory/components/common/SingleSelect.js-40-43 (1)

40-43: ⚠️ Potential issue | 🟡 Minor

Handle falsy-but-valid selected values correctly.

Line 40 treats values like 0 as unselected, so the placeholder can show even when a real option is selected.

Proposed fix
-        if (!value) return placeholder || __('Select...', 'wp-user-frontend');
+        if (value === undefined || value === null || value === '') {
+            return placeholder || __('Select...', 'wp-user-frontend');
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/SingleSelect.js` around lines 40 -
43, The current early-return treats any falsy value (e.g., 0) as unselected;
update the conditional in the label-rendering logic so it only returns the
placeholder when value is null or undefined (not when value === 0 or other valid
falsy values). In other words, change the check around the value variable used
before calling options.find (the block referencing options.find, placeholder and
__('Select...', 'wp-user-frontend')) to explicitly test value === null || value
=== undefined, then proceed to find the matching option and return option.label
or placeholder as before.
modules/user-directory/views/directory/template-parts/social-icons.php-33-33 (1)

33-33: ⚠️ Potential issue | 🟡 Minor

Harden external links opened in new tabs.

Lines [33], [42], [51], and [60] should include noreferrer alongside noopener for better tabnabbing/privacy hardening.

Proposed fix
-<a href="<?php echo esc_url( $facebook_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon">
+<a href="<?php echo esc_url( $facebook_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon">
...
-<a href="<?php echo esc_url( $twitter_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon">
+<a href="<?php echo esc_url( $twitter_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon">
...
-<a href="<?php echo esc_url( $linkedin_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon">
+<a href="<?php echo esc_url( $linkedin_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon">
...
-<a href="<?php echo esc_url( $instagram_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon">
+<a href="<?php echo esc_url( $instagram_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon">

Also applies to: 42-42, 51-51, 60-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/views/directory/template-parts/social-icons.php` at
line 33, Update the external social link anchors (the <a> elements that use
esc_url($facebook_url), esc_url($twitter_url), esc_url($linkedin_url),
esc_url($instagram_url) and have class "wpuf-social-icon") to include
"noreferrer" in the rel attribute alongside "noopener" (i.e., change
rel="noopener" to rel="noopener noreferrer") for all four anchor tags so
external links opened in new tabs are hardened for tabnabbing/privacy.
includes/Integrations/Elementor/User_Directory_Widget.php-146-147 (1)

146-147: ⚠️ Potential issue | 🟡 Minor

Add rel attributes to links opened with target="_blank".

Lines [146-147] and [1339-1340] should include rel="noopener noreferrer" for tabnabbing/privacy hardening.

Proposed fix
-__( 'No user directory found. <a href="%s" target="_blank">Create one</a> first.', 'wp-user-frontend' ),
+__( 'No user directory found. <a href="%s" target="_blank" rel="noopener noreferrer">Create one</a> first.', 'wp-user-frontend' ),
...
-__( '<a href="%s" target="_blank">Create a user directory</a> in WP User Frontend settings.', 'wp-user-frontend' ),
+__( '<a href="%s" target="_blank" rel="noopener noreferrer">Create a user directory</a> in WP User Frontend settings.', 'wp-user-frontend' ),

Also applies to: 1339-1340

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Integrations/Elementor/User_Directory_Widget.php` around lines 146 -
147, Update the two hardcoded anchor tags that open in a new tab to include
rel="noopener noreferrer": find the translation strings passed to __() in
includes/Integrations/Elementor/User_Directory_Widget.php (the strings
containing '<a href="%s" target="_blank">Create one</a>' and the other
occurrence later in the file) and add rel="noopener noreferrer" inside the <a>
tag so they become '<a href="%s" target="_blank" rel="noopener noreferrer">...'.
Ensure both occurrences are updated so the anchor markup returned by the __()
calls includes the rel attribute.
modules/user-directory/views/profile/template-parts/user-avatar.php-18-24 (1)

18-24: ⚠️ Potential issue | 🟡 Minor

Harden $user and $size inputs before rendering.

Lines [18-24] should verify WP_User type and clamp size to a positive integer to prevent notices and invalid dimensions.

Proposed fix
-if ( ! $user ) {
+if ( empty( $user ) || ! ( $user instanceof WP_User ) ) {
     return;
 }
 
 // Get avatar size from parameter or use default
-$size = isset( $size ) ? intval( $size ) : 128;
+$size = isset( $size ) ? max( 1, (int) $size ) : 128;
 $wrapper_class = isset( $wrapper_class ) ? $wrapper_class : '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/views/profile/template-parts/user-avatar.php` around
lines 18 - 24, Ensure the template hardens $user and $size before rendering:
verify $user is a WP_User (use instanceof WP_User or resolve it with get_user_by
when $user may be an ID) and return early if it cannot be resolved; coerce $size
to an int and clamp it to a positive value (e.g., size = max(1, intval($size)))
to avoid zero/negative dimensions; also ensure $wrapper_class has a safe default
(e.g., empty string) and consider sanitizing it before output; update the checks
in user-avatar.php around the $user/$size/$wrapper_class handling to implement
these validations.
modules/user-directory/views/directory/template-parts/sort-field.php-18-19 (1)

18-19: ⚠️ Potential issue | 🟡 Minor

Initialize $all_data before nested key access.

Lines [18-19] assume $all_data exists and is an array. Add a local fallback to avoid notices in edge include paths.

Proposed fix
+ $all_data = ( isset( $all_data ) && is_array( $all_data ) ) ? $all_data : [];
  $orderby = ! empty( $all_data['orderby'] ) ? $all_data['orderby'] : 'id';
  $order = ! empty( $all_data['order'] ) ? $all_data['order'] : 'desc';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/views/directory/template-parts/sort-field.php` around
lines 18 - 19, Ensure $all_data is defined as an array before accessing its
keys: add a local fallback that checks if $all_data exists and is an array
(otherwise set it to an empty array) immediately before the lines that compute
$orderby and $order so the expressions using $all_data['orderby'] and
$all_data['order'] cannot trigger notices; keep the rest of the logic that sets
$orderby and $order unchanged.
assets/js/wpuf-user-directory-frontend.js-47-64 (1)

47-64: ⚠️ Potential issue | 🟡 Minor

removeUrlParam() drops hash fragments and can rewrite URLs incorrectly.

Lines [47-64] should use URL parsing instead of string split logic so #fragment and edge-case query strings are preserved.

Proposed fix
 function removeUrlParam(url, param) {
-    var urlParts = url.split('?');
-    
-    if (urlParts.length < 2) {
-        return url;
-    }
-
-    var params = new URLSearchParams(urlParts[1]);
-    params.delete(param);
-    
-    var newParams = params.toString();
-    
-    if (newParams) {
-        return urlParts[0] + '?' + newParams;
-    }
-    
-    return urlParts[0];
+    var parsedUrl = new URL(url, window.location.origin);
+    parsedUrl.searchParams.delete(param);
+    return parsedUrl.toString();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/wpuf-user-directory-frontend.js` around lines 47 - 64, The
removeUrlParam function currently splits on '?' which drops hash fragments and
mis-handles edge-case query strings; replace the manual split with the URL API:
parse the input with new URL(url, window.location.href) to support absolute and
relative URLs, use urlObj.searchParams.delete(param) to remove the parameter,
and return urlObj.href (or urlObj.toString()) so the original pathname,
preserved query ordering, and hash fragment remain intact; if URL construction
throws, fall back to returning the original url unchanged.
src/js/user-directory/components/DirectoryWizard.js-131-134 (1)

131-134: ⚠️ Potential issue | 🟡 Minor

Potential issue with JSON parsing on error responses.

If the server returns a non-JSON error response (e.g., HTML error page), response.json() will throw, and this exception won't be caught gracefully since it's inside the try block but before the main error handling.

🛠️ Suggested improvement
             if (!response.ok) {
-                const data = await response.json();
-                throw new Error(data.message || __('Something went wrong', 'wp-user-frontend'));
+                let errorMessage = __('Something went wrong', 'wp-user-frontend');
+                try {
+                    const data = await response.json();
+                    errorMessage = data.message || errorMessage;
+                } catch (parseError) {
+                    // Response wasn't JSON, use default message
+                }
+                throw new Error(errorMessage);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/DirectoryWizard.js` around lines 131 - 134,
The error handling path currently assumes response.json() will succeed; update
the non-ok branch in DirectoryWizard.js so you attempt to parse JSON inside its
own try/catch and fall back to await response.text() (or a generic message) when
JSON parsing fails, then throw a new Error that uses the parsed message/text or
a fallback __('Something went wrong', 'wp-user-frontend'); locate the existing
if (!response.ok) block and replace the single response.json() call with this
safe-parse-and-fallback logic to avoid unhandled exceptions when the server
returns non-JSON error bodies.
src/js/user-directory/components/steps/StepBasics.js-67-81 (1)

67-81: ⚠️ Potential issue | 🟡 Minor

Missing cleanup for debounce timeout on unmount.

The searchTimeoutRef timeout is not cleared when the component unmounts, which could cause a memory leak or state update on an unmounted component.

🛠️ Suggested fix
     // Close dropdown when clicking outside
     useEffect(() => {
         const handleClickOutside = (event) => {
             if (userDropdownRef.current && !userDropdownRef.current.contains(event.target)) {
                 setShowUserDropdown(false);
             }
         };

         document.addEventListener('mousedown', handleClickOutside);
         return () => {
             document.removeEventListener('mousedown', handleClickOutside);
+            // Clear any pending search timeout
+            if (searchTimeoutRef.current) {
+                clearTimeout(searchTimeoutRef.current);
+            }
         };
     }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/steps/StepBasics.js` around lines 67 - 81,
The debounced timeout stored in searchTimeoutRef used by handleSearchChange is
never cleared on unmount; add a useEffect in the StepBasics component that on
mount returns a cleanup function which checks searchTimeoutRef.current and calls
clearTimeout(searchTimeoutRef.current) (and optionally sets it to null) to avoid
leaks or setState-after-unmount when searchUsers runs; keep existing
handleSearchChange, searchUsers and setSearchTerm logic unchanged.
assets/js/ud-search-shortcode.js-52-63 (1)

52-63: ⚠️ Potential issue | 🟡 Minor

Missing error handling for non-JSON responses and no request timeout.

The fetch call doesn't handle cases where the server returns non-JSON content (e.g., HTML error page) and has no timeout, which could leave the UI in a loading state indefinitely.

🛠️ Suggested improvement
-        fetch(apiUrl + '?' + params.toString(), {
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 30000);
+        
+        fetch(apiUrl + '?' + params.toString(), {
             credentials: 'same-origin',
+            signal: controller.signal,
         })
-            .then(res => res.json())
+            .then(res => {
+                clearTimeout(timeoutId);
+                if (!res.ok) {
+                    throw new Error('Network response was not ok');
+                }
+                const contentType = res.headers.get('content-type');
+                if (!contentType || !contentType.includes('application/json')) {
+                    throw new Error('Response was not JSON');
+                }
+                return res.json();
+            })
             .then(data => {
                 if (data && data.success) {
                     onSuccess(data);
                 } else {
                     onError(data);
                 }
             })
-            .catch(onError);
+            .catch(err => {
+                clearTimeout(timeoutId);
+                onError(err);
+            });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/ud-search-shortcode.js` around lines 52 - 63, The fetch call using
apiUrl + '?' + params.toString() should handle non-JSON responses and enforce a
timeout: wrap the fetch in an AbortController with a setTimeout to abort after a
chosen timeout, pass controller.signal to fetch, and clear the timer on
completion; after receiving the Response check response.ok and attempt to parse
JSON inside a try/catch (if parsing fails or response.ok is false, create a
descriptive error object and call onError), otherwise call onSuccess with the
parsed data; ensure the existing .catch still calls onError for network/abort
errors.
modules/user-directory/views/profile/layout-2.php-95-95 (1)

95-95: ⚠️ Potential issue | 🟡 Minor

Harden new-tab links with rel="noopener noreferrer".

Both anchors open new tabs and should include rel to prevent opener access.

Proposed fix
-                                <a href="<?php echo esc_url( $contact_item['value'] ); ?>" target="_blank" class="!wpuf-text-sm !wpuf-text-gray-900 !wpuf-font-medium hover:!wpuf-text-emerald-600">
+                                <a href="<?php echo esc_url( $contact_item['value'] ); ?>" target="_blank" rel="noopener noreferrer" class="!wpuf-text-sm !wpuf-text-gray-900 !wpuf-font-medium hover:!wpuf-text-emerald-600">
@@
-                    <a href="<?php echo esc_url( $private_message_link ); ?>" target="_blank" class="!wpuf-h-11 !wpuf-w-11 !wpuf-bg-emerald-600 !wpuf-text-white !wpuf-rounded-lg hover:!wpuf-bg-emerald-700 !wpuf-transition-colors !wpuf-flex !wpuf-items-center !wpuf-justify-center !wpuf-no-underline">
+                    <a href="<?php echo esc_url( $private_message_link ); ?>" target="_blank" rel="noopener noreferrer" class="!wpuf-h-11 !wpuf-w-11 !wpuf-bg-emerald-600 !wpuf-text-white !wpuf-rounded-lg hover:!wpuf-bg-emerald-700 !wpuf-transition-colors !wpuf-flex !wpuf-items-center !wpuf-justify-center !wpuf-no-underline">

Also applies to: 122-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/views/profile/layout-2.php` at line 95, The anchor
that renders contact links (the <a> with href="<?php echo esc_url(
$contact_item['value'] ); ?>" and target="_blank") must be hardened by adding
rel="noopener noreferrer"; update that anchor (and the other similar anchor at
the other occurrence) to include rel="noopener noreferrer" whenever
target="_blank" is present so the rendered link prevents opener access.
modules/user-directory/views/profile/template-parts/file-2.php-219-219 (1)

219-219: ⚠️ Potential issue | 🟡 Minor

Add rel for new-tab file links.

This link opens in a new tab and should include rel="noopener noreferrer" for tabnabbing protection.

Proposed fix
-                            <a href="<?php echo esc_url( $file_url ); ?>" target="_blank" class="!wpuf-block">
+                            <a href="<?php echo esc_url( $file_url ); ?>" target="_blank" rel="noopener noreferrer" class="!wpuf-block">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/views/profile/template-parts/file-2.php` at line 219,
The anchor that opens $file_url in a new tab (the <a ... target="_blank"
class="!wpuf-block"> element) must include rel="noopener noreferrer" to prevent
tabnabbing; update that anchor in file-2.php to add rel="noopener noreferrer"
alongside the existing attributes.
modules/user-directory/Helpers.php-618-623 (1)

618-623: ⚠️ Potential issue | 🟡 Minor

Normalize file/files tab key mapping.

Label map uses files while other defaults/configs use file, causing inconsistent tab labels and lookups.

Proposed fix
     $labels = [
         'about'    => __( 'About', 'wp-user-frontend' ),
         'posts'    => __( 'Posts', 'wp-user-frontend' ),
         'comments' => __( 'Comments', 'wp-user-frontend' ),
+        'file'     => __( 'Files', 'wp-user-frontend' ),
         'files'    => __( 'Files', 'wp-user-frontend' ),
         'activity' => __( 'Activity', 'wp-user-frontend' ),
     ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/Helpers.php` around lines 618 - 623, The labels map in
Helpers.php uses the key 'files' which is inconsistent with the rest of the
codebase that expects 'file'; update the $labels array to use 'file' instead of
'files' (in the associative array defined around $labels) so tab label lookups
and defaults align, and scan related usages for $labels, get_tab_label or
similar helpers to ensure they reference 'file' consistently.

ℹ️ Review info
Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 322b89b7-09c7-438d-903b-ded508f62716

📥 Commits

Reviewing files that changed from the base of the PR and between 5f313fa and fe97092.

⛔ Files ignored due to path filters (22)
  • assets/css/wpuf-user-directory-free.css.map is excluded by !**/*.map
  • assets/images/user-directory/confetti.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-1.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-2.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-3.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-4.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-5.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-6.png is excluded by !**/*.png
  • assets/images/user-directory/profile-layout-1.png is excluded by !**/*.png
  • assets/images/user-directory/profile-layout-2.png is excluded by !**/*.png
  • assets/images/user-directory/profile-layout-3.png is excluded by !**/*.png
  • assets/images/user-directory/round-grids.png is excluded by !**/*.png
  • assets/images/user-directory/sidecards.png is excluded by !**/*.png
  • assets/images/user-directory/square-grids.png is excluded by !**/*.png
  • assets/images/user-directory/table.png is excluded by !**/*.png
  • assets/images/user-directory/thumb-male-1.svg is excluded by !**/*.svg
  • assets/images/user-directory/thumb-male-2.svg is excluded by !**/*.svg
  • assets/images/user-directory/thumb-male-3.svg is excluded by !**/*.svg
  • assets/images/user-directory/wide-sidecards.png is excluded by !**/*.png
  • assets/js/wpuf-user-directory-free.js.map is excluded by !**/*.map
  • modules/user-directory/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (68)
  • Gruntfile.js
  • assets/css/admin/subscriptions.min.css
  • assets/css/admin/wpuf-module.css
  • assets/css/ai-form-builder.min.css
  • assets/css/elementor-user-directory.css
  • assets/css/forms-list.min.css
  • assets/css/frontend-subscriptions.min.css
  • assets/css/wpuf-user-directory-free.css
  • assets/css/wpuf-user-directory-frontend.css
  • assets/js/admin/wpuf-module.js
  • assets/js/ud-search-shortcode.js
  • assets/js/wpuf-user-directory-free.asset.php
  • assets/js/wpuf-user-directory-free.js
  • assets/js/wpuf-user-directory-free.js.LICENSE.txt
  • assets/js/wpuf-user-directory-frontend.js
  • includes/Assets.php
  • includes/Free/Free_Loader.php
  • includes/Integrations.php
  • includes/Integrations/Elementor/Elementor.php
  • includes/Integrations/Elementor/User_Directory_Widget.php
  • includes/functions/modules.php
  • modules/user-directory/Admin_Menu.php
  • modules/user-directory/Api/Directory.php
  • modules/user-directory/DirectoryStyles.php
  • modules/user-directory/Helpers.php
  • modules/user-directory/Post_Type.php
  • modules/user-directory/PrettyUrls.php
  • modules/user-directory/Shortcode.php
  • modules/user-directory/User_Directory.php
  • modules/user-directory/package.json
  • modules/user-directory/postcss.config.js
  • modules/user-directory/tailwind.config.js
  • modules/user-directory/views/admin-page.php
  • modules/user-directory/views/directory/layout-3.php
  • modules/user-directory/views/directory/template-parts/pagination-shortcode.php
  • modules/user-directory/views/directory/template-parts/row-3.php
  • modules/user-directory/views/directory/template-parts/search-field.php
  • modules/user-directory/views/directory/template-parts/social-icons.php
  • modules/user-directory/views/directory/template-parts/sort-field.php
  • modules/user-directory/views/profile/layout-2.php
  • modules/user-directory/views/profile/template-parts/about-2.php
  • modules/user-directory/views/profile/template-parts/comments-2.php
  • modules/user-directory/views/profile/template-parts/file-2.php
  • modules/user-directory/views/profile/template-parts/posts-2.php
  • modules/user-directory/views/profile/template-parts/user-avatar.php
  • modules/user-directory/webpack.config.js
  • package.json
  • postcss.user-directory.config.js
  • src/js/user-directory/App.js
  • src/js/user-directory/components/DirectoryList.js
  • src/js/user-directory/components/DirectoryWizard.js
  • src/js/user-directory/components/common/DeleteConfirmModal.js
  • src/js/user-directory/components/common/Header.js
  • src/js/user-directory/components/common/LayoutCard.js
  • src/js/user-directory/components/common/MultiSelect.js
  • src/js/user-directory/components/common/SingleSelect.js
  • src/js/user-directory/components/common/Toast.js
  • src/js/user-directory/components/common/Tooltip.js
  • src/js/user-directory/components/steps/StepAdvanced.js
  • src/js/user-directory/components/steps/StepBasics.js
  • src/js/user-directory/components/steps/StepLayout.js
  • src/js/user-directory/components/steps/StepProfile.js
  • src/js/user-directory/components/steps/StepTabs.js
  • src/js/user-directory/index.js
  • src/js/user-directory/styles/main.css
  • src/js/user-directory/utils/avatarSizeHelper.js
  • tailwind.config.js
  • wpuf-functions.php

Comment on lines +13 to +14
const SEARCH_DEBOUNCE = 300;
let debounceTimeout = null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Shared debounce state causes race conditions with multiple directories.

The debounceTimeout variable is declared at the module scope and shared across all directory instances on a page. If a user types in one directory's search and then quickly types in another, the debounce will be cancelled for the first directory.

Move the debounce timeout into initUserDirectorySearch to scope it per instance:

🐛 Proposed fix
 (function(window, document) {
     'use strict';

     const SEARCH_DEBOUNCE = 300;
-    let debounceTimeout = null;

     function fetchUsers({
         // ... unchanged
     }) {
         // ... unchanged
     }

     function initUserDirectorySearch(container, blockId, pageId) {
         let currentRequestId = 0;
+        let debounceTimeout = null;

         // ... rest of function

         // Search input handler
         input.addEventListener('input', function(e) {
             const value = e.target.value.trim();
             if (debounceTimeout) clearTimeout(debounceTimeout);
             debounceTimeout = setTimeout(() => {
                 performSearch(value, 1);
             }, SEARCH_DEBOUNCE);
         });

         // ... rest of function
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/ud-search-shortcode.js` around lines 13 - 14, The module-scoped
debounceTimeout (declared alongside SEARCH_DEBOUNCE) is shared across all
directory instances causing cross-instance cancellation; move the
debounceTimeout variable into the initUserDirectorySearch function so each call
gets its own timeout state, update uses inside initUserDirectorySearch (and any
inner handlers like the input event listener) to reference the instance-scoped
debounceTimeout, and remove/replace the module-level debounceTimeout declaration
to avoid the race condition.

Comment on lines +453 to +456
$page = ! empty( $request['page'] ) ? absint( $request['page'] ) : 1;
$orderby = ! empty( $request['orderby'] ) ? sanitize_text_field( $request['orderby'] ) : 'ID';
$order = ! empty( $request['order'] ) ? strtoupper( sanitize_text_field( $request['order'] ) ) : 'DESC';
$max_item = ! empty( $request['max_item'] ) ? absint( $request['max_item'] ) : 12;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard page/per_page bounds in search pagination.

page=0 yields negative offsets, and per_page=0 can trigger division by zero at Line [518].

Proposed fix
-        $page         = ! empty( $request['page'] ) ? absint( $request['page'] ) : 1;
+        $page         = ! empty( $request['page'] ) ? max( 1, absint( $request['page'] ) ) : 1;
@@
-        $settings_per_page = absint( $settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) );
-        $per_page = $max_item > 0 ? $max_item : $settings_per_page;
+        $settings_per_page = max( 1, absint( $settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) ) );
+        $per_page          = max( 1, $max_item > 0 ? $max_item : $settings_per_page );

Also applies to: 473-475, 518-518

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/Api/Directory.php` around lines 453 - 456, The
pagination variables allow zero values which lead to negative offsets and
divide-by-zero; ensure $page and $max_item (aka per_page) are clamped to a safe
range after sanitization: set $page = max(1, absint($request['page'] ?? 1)) and
set $max_item = max(1, absint($request['max_item'] ?? 12)) (optionally enforce
an upper cap like 100), and apply the same clamping logic wherever
$page/$max_item are computed/used (including the other occurrences that compute
$page, $orderby, $order and the division/offset calculation) so offsets and
divisions cannot receive zero or negative values.

Comment on lines +288 to +289
$per_page = absint( $settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) );
$offset = ( $paged - 1 ) * $per_page;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Clamp per_page before pagination math.

$per_page can be 0 from saved settings, which makes Line [333] divide by zero.

Proposed fix
-        $per_page = absint( $settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) );
+        $per_page = max( 1, absint( $settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) ) );
         $offset   = ( $paged - 1 ) * $per_page;

Also applies to: 333-333

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/Shortcode.php` around lines 288 - 289, The issue is
that $per_page can be 0 from saved settings which leads to invalid pagination
math when computing $offset and later divisions; modify the logic around
$per_page in the Shortcode class (the code setting $per_page and using it to
compute $offset and any subsequent division) to clamp it to a minimum of 1 (e.g.
replace the direct absint(...) result with a sanitized value like $per_page =
max(1, absint(...)) or otherwise ensure $per_page >= 1) before any use in
$offset = ($paged - 1) * $per_page or other calculations so division-by-zero
cannot occur.

Comment on lines +201 to +205
$file_url = wp_get_attachment_url( $file->ID );
$file_type = get_post_mime_type( $file->ID );
$file_extension = strtoupper( pathinfo( $file_url, PATHINFO_EXTENSION ) );
$is_image = strpos( $file_type, 'image/' ) === 0;
$is_pdf = $file_type === 'application/pdf';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard against missing attachment URLs before calling pathinfo().

If wp_get_attachment_url() returns false, pathinfo() will throw and break profile rendering.

Proposed fix
-                        $file_url = wp_get_attachment_url( $file->ID );
-                        $file_type = get_post_mime_type( $file->ID );
-                        $file_extension = strtoupper( pathinfo( $file_url, PATHINFO_EXTENSION ) );
+                        $file_url = wp_get_attachment_url( $file->ID );
+                        if ( ! $file_url ) {
+                            continue;
+                        }
+                        $file_type = (string) get_post_mime_type( $file->ID );
+                        $file_extension = strtoupper( pathinfo( $file_url, PATHINFO_EXTENSION ) );
@@
-                                        <?php echo esc_html( $file->post_title ?: pathinfo( $file_url, PATHINFO_FILENAME ) ); ?>
+                                        <?php echo esc_html( $file->post_title ?: pathinfo( $file_url, PATHINFO_FILENAME ) ); ?>

Also applies to: 245-246

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/views/profile/template-parts/file-2.php` around lines
201 - 205, Guard against wp_get_attachment_url() returning false before calling
pathinfo by checking the $file_url truthiness; in the block that sets $file_url,
$file_type, $file_extension, $is_image, $is_pdf (and the similar block at the
other occurrence) only call pathinfo() when $file_url is non-false, otherwise
set $file_extension to an empty string (or null) and adjust $is_image/$is_pdf
using $file_type alone or default false; update the assignments around the
wp_get_attachment_url($file->ID) call and the $file_extension =
strtoupper(pathinfo(...)) usage to perform this guard so profile rendering won’t
break when the attachment URL is missing.

Comment on lines +1 to +2
import { useState, useRef, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the entire file to verify React usage and imports
cat -n src/js/user-directory/components/common/MultiSelect.js | head -50

Repository: weDevsOfficial/wp-user-frontend

Length of output: 2395


React is undefined where React.cloneElement is called.

Lines 29-37 reference React.cloneElement, but React is never imported. This will throw a ReferenceError at runtime when option icons are rendered.

Import cloneElement from @wordpress/element and use it directly:

Proposed fix
-import { useState, useRef, useEffect } from '@wordpress/element';
+import { useState, useRef, useEffect, cloneElement } from '@wordpress/element';
@@
-                return React.cloneElement(option.icon, {
-                    children: React.cloneElement(option.icon.props.children, {
+                return cloneElement(option.icon, {
+                    children: cloneElement(option.icon.props.children, {
@@
-            return React.cloneElement(option.icon, {
-                children: React.cloneElement(option.icon.props.children, {
+            return cloneElement(option.icon, {
+                children: cloneElement(option.icon.props.children, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/MultiSelect.js` around lines 1 - 2,
The code calls React.cloneElement in MultiSelect.js (around the option icon
rendering) but React is never imported; import cloneElement from
'@wordpress/element' (add it to the existing import list with
useState/useRef/useEffect) and replace React.cloneElement(...) calls with direct
cloneElement(...) calls so the option icons render without a ReferenceError.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants