Skip to content

Feat/user directory free pro functionality#1778

Open
arifulhoque7 wants to merge 25 commits intoweDevsOfficial:developfrom
arifulhoque7:FEAT/user-directory-free-pro-functionality
Open

Feat/user directory free pro functionality#1778
arifulhoque7 wants to merge 25 commits intoweDevsOfficial:developfrom
arifulhoque7:FEAT/user-directory-free-pro-functionality

Conversation

@arifulhoque7
Copy link
Contributor

@arifulhoque7 arifulhoque7 commented Dec 26, 2025

PR Title. PRO PR

Close 1379
Feature: Add User Directory Module to WP User Frontend Free


Description

This PR introduces a complete User Directory feature to the free version of WP User Frontend, synced with the Pro version's functionality and design patterns.


Summary of Changes

🎯 Core Feature Implementation

New Module: User Directory (modules/user-directory/)

  • Complete directory listing with search, sort, and pagination
  • User profile pages with tabbed content (About, Posts, Comments, Files)
  • Setup wizard for easy directory configuration
  • REST API for AJAX-powered search and pagination
  • Pretty URL support for user profiles (/directory/username)

📁 Files Added (77 files, +22,194 lines)

PHP Classes:

File Description
modules/user-directory/User_Directory.php Main module class and initialization
modules/user-directory/Admin_Menu.php Admin menu and settings page
modules/user-directory/Shortcode.php Shortcode rendering for directory and profiles
Helpers.php Helper functions for avatars, URLs, tabs, contact info
Directory.php REST API for AJAX search/pagination
modules/user-directory/Post_Type.php Custom post type for directory settings
modules/user-directory/PrettyUrls.php Rewrite rules for clean profile URLs
modules/user-directory/DirectoryStyles.php Dynamic CSS generation
includes/functions/modules.php Module helper functions

Templates:

File Description
views/directory/layout-3.php Grid card layout for directory listing
row-3.php Individual user card template
views/directory/template-parts/pagination-shortcode.php Pagination component
views/directory/template-parts/search-field.php Search input component
views/directory/template-parts/sort-field.php Sort dropdown component
views/directory/template-parts/social-icons.php Social media icons display
layout-2.php User profile layout with tabs
views/profile/template-parts/about-2.php About tab content
views/profile/template-parts/posts-2.php Posts tab with pagination
views/profile/template-parts/comments-2.php Comments tab with pagination
views/profile/template-parts/file-2.php Files tab (Pro feature preview)
user-avatar.php Avatar with initials fallback

React Admin UI (src/js/user-directory/):

File Description
index.js Entry point
App.js Main application component
components/DirectoryList.js Directory listing management
components/DirectoryWizard.js Setup wizard component
components/steps/StepBasics.js Basic settings step
components/steps/StepLayout.js Layout selection step
components/steps/StepProfile.js Profile settings step
components/steps/StepTabs.js Tab configuration step
components/steps/StepAdvanced.js Advanced settings step
components/common/Header.js Admin page header
components/common/LayoutCard.js Layout preview cards
components/common/MultiSelect.js Multi-select dropdown
components/common/Toast.js Toast notifications
components/common/DeleteConfirmModal.js Delete confirmation modal

Build Configuration:

File Description
webpack.user-directory.config.js Webpack config for admin JS
tailwind.user-directory.config.js Tailwind config with wpuf- prefix
postcss.user-directory.config.js PostCSS configuration

Assets:

  • assets/css/wpuf-user-directory-free.css - Admin styles (7,868 lines)
  • assets/css/wpuf-user-directory-frontend.css - Frontend styles (550 lines)
  • assets/js/wpuf-user-directory-free.js - Compiled admin bundle
  • assets/js/wpuf-user-directory-frontend.js - Frontend scripts
  • assets/js/ud-search-shortcode.js - AJAX search functionality
  • assets/images/user-directory/ - Layout preview images and icons

Key Features

1. Directory Listing

  • Layout: Grid card style (Layout 3)
  • Search: Real-time AJAX search by name/email
  • Sort: Order by name, registration date, ID
  • Pagination: AJAX-powered with preserved state
  • Avatar: Custom photo, Gravatar, or initials fallback

2. User Profile

  • Tabs: About, Posts, Comments, Files
  • Contact Info: Email, Website, Phone with styled icons
  • Social Links: Support for common social platforms
  • Bio: Expandable biography with "Show More/Less"
  • Actions: Message button (if PM module active), Edit Profile

3. Setup Wizard

  • Step-by-step directory creation
  • Layout preview cards
  • Role filtering
  • Tab customization
  • Avatar size configuration

4. Profile URL Handling

  • Pretty URLs: /directory/username or /directory/123
  • Proper base_url handling for AJAX pagination context
  • Shortcode detection for correct URL generation
  • Preserved pagination state on "Back to Directory"

Commits

Hash Message
f531687f Add free User Directory feature with setup wizard
c1a5ef37 Update color classes to use ! prefix for importance
547c5309 Enhance user directory UI and member count logic
8442e2db Add file tab to user profile and update build tasks
4f0a05e6 Add new utility classes for WPUF admin styles

Testing Checklist

  • Create new directory via setup wizard
  • Directory listing displays users correctly
  • Search filters users in real-time
  • Sort dropdown changes order
  • Pagination works with AJAX
  • "View Profile" link works after pagination
  • Profile page displays all tabs correctly
  • Posts/Comments tabs show user content with pagination
  • Files tab shows Pro upgrade message
  • Avatar displays correctly (image or initials)
  • "Back to Directory" preserves pagination state
  • Pretty URLs work for profiles
  • Mobile responsive design

Screenshots

Add screenshots of:

  1. Admin setup wizard
  2. Directory listing page
  3. User profile page
  4. Mobile view

Related Issues

Breaking Changes

None. This is a new feature addition.

Dependencies

  • Tailwind CSS (with wpuf- prefix)
  • WordPress REST API
  • React (for admin UI)

Summary by CodeRabbit

  • New Features

    • Free User Directory: admin CRUD UI, REST endpoints, shortcode/block listings, pretty profile URLs, responsive directory grids & profile pages with tabs, frontend live search with URL sync, pagination, sorting, and a new React-based admin app with templates and styles.
    • Admin modules split into Free and Pro sections with activation toggles.
  • Bug Fixes

    • Scoped hover overlay styling and more reliable admin toggle with rollback on error.
  • Chores

    • Build pipeline updated with user-directory build/watch tasks and npm scripts.

arifulhoque7 and others added 14 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.
@arifulhoque7 arifulhoque7 requested a review from sapayth December 26, 2025 05:25
@arifulhoque7 arifulhoque7 self-assigned this Dec 26, 2025
@arifulhoque7 arifulhoque7 added needs: testing needs: dev review This PR needs review by a developer labels Dec 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a Free User Directory: admin React CRUD app, REST endpoints (CRUD, search, user_count), shortcode rendering and templates, pretty‑URL rewrites, helper utilities, free-module registry and AJAX toggle, frontend search/pagination, and module-specific build tooling.

Changes

Cohort / File(s) Summary
Build & Tooling
Gruntfile.js, package.json, postcss.user-directory.config.js, tailwind.config.js, modules/user-directory/postcss.config.js, modules/user-directory/tailwind.config.js, modules/user-directory/webpack.config.js, modules/user-directory/package.json
Added user-directory build/watch targets, npm scripts, PostCSS/Tailwind/webpack configs, safelist/content paths, and integrated build:user-directory into main build flow.
Module Core & Registration
modules/user-directory/User_Directory.php, modules/user-directory/Post_Type.php, modules/user-directory/PrettyUrls.php, includes/functions/modules.php, includes/Free/Free_Loader.php
New User_Directory module singleton; registers post type, pretty URL rules, free module registry and activation/deactivation helpers; loader integration and AJAX toggle endpoint with nonce/permission checks.
Admin App & UI
modules/user-directory/Admin_Menu.php, modules/user-directory/views/admin-page.php, src/js/user-directory/..., src/js/user-directory/components/..., src/js/user-directory/styles/main.css
New admin React app (App, DirectoryList, DirectoryWizard, steps, common components), admin menu/page, styles, REST integration, Pro gating and free‑limit UX.
REST API & Server Logic
modules/user-directory/Api/Directory.php, modules/user-directory/DirectoryStyles.php, modules/user-directory/Helpers.php
New REST controller with register_routes and endpoints for list/create/get/update/delete/search/user_count; directory styling/title/body class handling; many helpers for avatars, URLs, pagination and profile data.
Frontend Shortcode & Templates
modules/user-directory/Shortcode.php, modules/user-directory/views/directory/*, modules/user-directory/views/profile/*
Shortcode handlers for listings and profiles; templates and template-parts for layouts, rows, search, sort, shortcode-aware pagination, profile tabs and profile views.
Frontend JS & CSS
assets/js/ud-search-shortcode.js, assets/js/wpuf-user-directory-frontend.js, assets/css/wpuf-user-directory-frontend.css, assets/css/admin/wpuf-module.css, src/js/user-directory/styles/main.css
Added frontend search/pagination JS with History API and debounced searches, new frontend CSS for listings/profiles, and scoped admin CSS adjustments.
Admin Module Management
assets/js/admin/wpuf-module.js, assets/css/admin/wpuf-module.css, assets/js/wpuf-user-directory-free.asset.php, assets/js/wpuf-user-directory-free.js.LICENSE.txt
Scoped pro-preview hover styling, added client toggle handler for free modules (AJAX toggle), and added asset manifest/license for free module.
Templates & Views
modules/user-directory/views/directory/template-parts/*, modules/user-directory/views/profile/template-parts/*, modules/user-directory/views/directory/layout-3.php, modules/user-directory/views/profile/layout-2.php
New template parts for search, sort, rows, shortcode pagination, social icons, profile avatar, posts/comments/files tabs, and empty/no-results states.
Bootstrap Inclusion
wpuf-functions.php
Includes includes/functions/modules.php to load module helper functions.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • sapayth

Poem

🐇 I hopped through code, a carrot in my paw,
New directories bloom and routes I saw.
React fields sparkle, Tailwind trims the green,
Search hums soft, admin wizards preen.
A tiny hop — a feature born, serene.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Feat/user directory free pro functionality' directly describes the main change: adding User Directory functionality to the free version of WP User Frontend.
Docstring Coverage ✅ Passed Docstring coverage is 91.20% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@arifulhoque7 arifulhoque7 mentioned this pull request Dec 26, 2025
13 tasks
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: 14

Note

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

🟡 Minor comments (17)
src/js/user-directory/components/common/Tooltip.js-3-6 (1)

3-6: Unstable tooltip ID regenerates on every render.

tooltipId is generated inline and will change on each re-render, causing unnecessary DOM updates and potential accessibility issues. Also, substr() is deprecated.

🔎 Proposed fix using useMemo
-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).substring(2, 11)}`,
+        []
+    );

Committable suggestion skipped: line range outside the PR's diff.

modules/user-directory/package.json-8-24 (1)

8-24: Pin dependency versions for reproducible builds.

Using caret (^) ranges can lead to different versions being installed over time. While the Tailwind version here (^3.4.17) differs from the root config (3.3.5), this is not a breaking change—Tailwind 3.4 is a backward-compatible minor release. However, consider pinning versions to ensure consistent builds across environments and prevent drift from non-deterministic installations.

src/js/user-directory/components/DirectoryList.js-22-321 (1)

22-321: Add error handling to clipboard operations and complete i18n coverage

  • Line 29–33: navigator.clipboard.writeText() lacks error handling. Unhandled promise rejections will break the copy flow silently. Add .catch() or wrap in try/catch to handle failures gracefully (e.g., display a fallback message).
  • Line 200, 214, 219: "Loading…", "Copied!", and "Copy shortcode" are user-visible but missing __() wrapping. Wrap each in __('string', 'wp-user-frontend') to match the component's i18n pattern.
  • Line 53: Guard directories.forEach() with if (!Array.isArray(directories)) return; to prevent runtime errors if the prop is null or undefined during initial load.
src/js/user-directory/components/common/DeleteConfirmModal.js-49-58 (1)

49-58: Translate button text for internationalization.

Lines 50 and 57 have hard-coded English text for the "Cancel" and "Delete" buttons. These should be wrapped with the __() function for consistency with the rest of the component and to support translations.

🔎 Suggested 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>
modules/user-directory/views/profile/template-parts/posts-2.php-65-66 (1)

65-66: Escape permalink URL.

get_permalink() output should be escaped with esc_url().

🔎 Proposed fix
-                            <a href="<?php echo get_permalink(); ?>" 
+                            <a href="<?php echo esc_url( get_permalink() ); ?>" 
modules/user-directory/views/profile/template-parts/posts-2.php-54-57 (1)

54-57: Post title output needs escaping.

wp_trim_words() returns an unescaped string that should be escaped before output.

🔎 Proposed fix
                             <div class="!wpuf-text-base !wpuf-font-normal !wpuf-text-gray-900 !wpuf-leading-none">
-                                <?php echo wp_trim_words( get_the_title(), 10 ); ?>
+                                <?php echo esc_html( wp_trim_words( get_the_title(), 10 ) ); ?>
                             </div>
modules/user-directory/views/directory/template-parts/row-3.php-63-69 (1)

63-69: Incorrect escaping function for mailto link.

esc_url() is not appropriate for email addresses in mailto: links. Use esc_attr() instead, as esc_url() may mangle the email format.

🔎 Proposed fix
         <?php if ( $user_email ) : ?>
             <p class="wpuf-text-[14px] wpuf-font-normal wpuf-text-gray-600 !wpuf-m-0">
-                <a href="mailto:<?php echo esc_url( $user_email ); ?>" target="_blank" rel="noopener" class="wpuf-text-gray-500 hover:wpuf-text-gray-900">
+                <a href="mailto:<?php echo esc_attr( $user_email ); ?>" target="_blank" rel="noopener" class="wpuf-text-gray-500 hover:wpuf-text-gray-900">
                     <?php echo esc_html( $user_email ); ?>
                 </a>
             </p>
         <?php endif; ?>
modules/user-directory/views/profile/template-parts/user-avatar.php-6-6 (1)

6-6: Inconsistent version annotation.

The docblock states @since 4.2.0, but based on the PR summary and other files, this module is being introduced in version 4.3.0.

🔎 Proposed fix
- * @since 4.2.0
+ * @since 4.3.0
src/js/user-directory/components/DirectoryWizard.js-145-147 (1)

145-147: handleSaveAndExit doesn't prevent double submission.

If the user clicks "Save and Exit" while already saving, handleSubmit will be called again. The loading state is set inside handleSubmit, not before the await.

🔎 Proposed fix
 const handleSaveAndExit = async () => {
+    if (loading) return;
     await handleSubmit();
 };
src/js/user-directory/components/steps/StepBasics.js-67-81 (1)

67-81: Clean up debounce timeout on unmount.

The debounce timeout stored in searchTimeoutRef is not cleared when the component unmounts, which could cause a state update on an unmounted component.

🔎 Proposed fix — add cleanup effect
+    // Cleanup debounce timeout on unmount
+    useEffect(() => {
+        return () => {
+            if (searchTimeoutRef.current) {
+                clearTimeout(searchTimeoutRef.current);
+            }
+        };
+    }, []);
+
     // Close dropdown when clicking outside
     useEffect(() => {
assets/js/ud-search-shortcode.js-194-200 (1)

194-200: Hardcoded English strings are not translatable.

The "No users found" messages are hardcoded in JavaScript. Use the localized strings from wpufUserDirectorySearch or provide fallbacks.

🔎 Example approach
 const heading = document.createElement('h3');
 heading.style.cssText = 'font-size: 1rem; font-weight: 600; color: #111827; margin-bottom: 0.5rem;';
-heading.textContent = 'No users found matching your search criteria.';
+heading.textContent = (typeof wpufUserDirectorySearch !== 'undefined' && wpufUserDirectorySearch.i18n && wpufUserDirectorySearch.i18n.noUsersFound) 
+    ? wpufUserDirectorySearch.i18n.noUsersFound 
+    : 'No users found matching your search criteria.';
src/js/user-directory/components/steps/StepAdvanced.js-9-12 (1)

9-12: Inconsistency: Free avatar sizes don't match Admin_Menu.php.

The freeAvatarSizes array only includes '192', but Admin_Menu.php (lines 403-412) defines both 192 and 128 as free (is_free: true). This inconsistency could cause confusion where users see 128×128 as available in the backend but locked in the wizard.

🔎 Proposed fix
-    const freeAvatarSizes = ['192']; // Only 192 is free
+    const freeAvatarSizes = ['128', '192']; // 128 and 192 are free per Admin_Menu.php
src/js/user-directory/components/steps/StepAdvanced.js-134-142 (1)

134-142: Typo: Avatar size '265' should be '256'.

The avatar sizes array includes '265' which appears to be a typo. The Admin_Menu.php file (line 429-432) defines '256' as an avatar size option.

🔎 Proposed fix
         { value: '160', label: '160×160', isFree: false },
         { value: '192', label: '192×192', isFree: true },
-        { value: '265', label: '265×265', isFree: false },
+        { value: '256', label: '256×256', isFree: false },
     ];
modules/user-directory/views/profile/layout-2.php-88-91 (1)

88-91: Unescaped HTML output for contact icons.

The $contact_item['icon'] is output without escaping via echo. While this HTML is generated internally in Helpers.php, it bypasses WordPress escaping functions. Consider using wp_kses_post() or ensure the icon HTML is sanitized at the source.

Proposed fix
                         <div class="!wpuf-relative">
-                            <?php echo $contact_item['icon']; ?>
+                            <?php echo wp_kses_post( $contact_item['icon'] ); ?>
                         </div>
modules/user-directory/Api/Directory.php-638-640 (1)

638-640: Profile tabs array lacks deep sanitization.

The profile_tabs array is stored directly without recursive sanitization. Malicious input could persist if the array contains nested values.

Proposed fix
         // Profile tabs (complex array)
         if ( isset( $params['profile_tabs'] ) && is_array( $params['profile_tabs'] ) ) {
-            $settings['profile_tabs'] = $params['profile_tabs'];
+            $settings['profile_tabs'] = map_deep( $params['profile_tabs'], 'sanitize_text_field' );
         }
modules/user-directory/DirectoryStyles.php-333-334 (1)

333-334: Sanitize $_SERVER['REQUEST_URI'] before use.

Direct access to $_SERVER['REQUEST_URI'] should be sanitized.

Proposed fix
-        $current_uri = trim( $_SERVER['REQUEST_URI'], '/' );
+        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+        $current_uri = isset( $_SERVER['REQUEST_URI'] ) ? trim( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), '/' ) : '';
modules/user-directory/Shortcode.php-493-505 (1)

493-505: Remove unused get_current_page() method.

Static analysis correctly identifies this private method as unused. It appears to be dead code that should be removed to improve maintainability.

Proposed fix
-    /**
-     * Get current page number (matching Pro logic)
-     *
-     * @since 4.3.0
-     *
-     * @return int
-     */
-    private function get_current_page() {
-        $current_page = 1;
-
-        // Support both ?page=2 and /page/2/
-        if ( get_query_var( 'paged' ) ) {
-            $current_page = (int) get_query_var( 'paged' );
-        } elseif ( ! empty( $_GET['page'] ) ) {
-            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
-            $current_page = (int) $_GET['page'];
-        }
-
-        return max( 1, $current_page );
-    }
🧹 Nitpick comments (53)
tailwind.config.js (1)

17-19: Redundant content path.

The path './modules/user-directory/views/**/*.php' is already covered by './modules/user-directory/**/*.php'. Consider removing the redundant entry.

🔎 Proposed fix
         // Free User Directory module templates
         './modules/user-directory/**/*.php',
-        './modules/user-directory/views/**/*.php',
assets/js/admin/wpuf-module.js (1)

25-62: Consider adding defensive checks for the global object.

The handler assumes wpuf_free_modules is always defined. If the script runs before localization or in an unexpected context, this could throw an error.

🔎 Proposed defensive check
     // Handle free module toggle
     $( '.wpuf-toggle-free-module' ).on( 'change', function () {
+        if ( typeof wpuf_free_modules === 'undefined' ) {
+            console.error( 'WPUF Free modules configuration not loaded' );
+            return;
+        }
+
         var $toggle = $( this );
         var moduleId = $toggle.data( 'module' );
src/js/user-directory/components/common/Header.js (1)

10-11: Potentially unused React import.

With react-jsx-runtime in dependencies (per the asset file), the explicit React import may be unnecessary. Only useEffect is used.

🔎 Proposed fix
-import React, { useEffect } from 'react';
+import { useEffect } from '@wordpress/element';
modules/user-directory/views/profile/template-parts/file-2.php (2)

231-241: Unescaped SVG output.

$display_icon and $type_icons[$type] are echoed without escaping. While these are hardcoded SVGs, it's inconsistent with the escaping discipline elsewhere in the template.

Since these are trusted hardcoded values, use wp_kses_post() or explicitly mark as safe:

-<div class="!wpuf-text-2xl !wpuf-mb-1"><?php echo $display_icon; ?></div>
+<div class="!wpuf-text-2xl !wpuf-mb-1"><?php echo wp_kses_post( $display_icon ); ?></div>

281-316: Consider moving inline JavaScript to an enqueued script.

Inline JS for tab switching could be moved to the module's frontend JS file for better separation of concerns and caching benefits.

src/js/user-directory/components/steps/StepProfile.js (1)

4-157: Consider centralizing Pro badge and asset URL logic

The StepProfile overlay in Lines 99–107 duplicates the Pro badge image URL resolution pattern used in other components (e.g., App.js, StepAdvanced.js). Extracting a shared ProBadge helper (or reusing the existing one) would reduce drift if asset URL logic or badge styling changes, and keep Free/Pro branding consistent across the wizard.

src/js/user-directory/styles/main.css (1)

196-302: Reduce duplication and narrow .wpuf-overflow-hidden override if possible

  • .wpuf-z-\[9999\] and .wpuf-overflow-hidden are defined twice (Lines 197–204 and 295–302). Keeping only one definition for each will simplify maintenance.
  • Overriding the Tailwind-style .wpuf-overflow-hidden utility globally to overflow: visible !important changes the semantic meaning of that class everywhere on this admin page. If any other component relies on real hidden overflow, this could cause unexpected visual glitches. Where feasible, consider scoping the override to a more specific container (e.g., a table wrapper class) rather than the generic utility.
modules/user-directory/postcss.config.js (1)

1-6: PostCSS config is correct and minimal

Config wiring for tailwindcss and autoprefixer is standard and should work fine with the module-specific build. If other modules use an identical pattern, consider a shared base PostCSS config to reduce duplication, but not required for correctness here.

src/js/user-directory/components/common/LayoutCard.js (1)

14-95: Localize the "PRO" badge label for consistency with component patterns

The badge text on line 63 is hardcoded as PRO while other user-facing strings in the component use the localization pattern i18n?.property || __('Fallback', 'wp-user-frontend') (lines 71, 80). For consistency and to support translations, consider wrapping it: i18n?.pro_label || __('Pro', 'wp-user-frontend').

modules/user-directory/views/directory/template-parts/sort-field.php (2)

14-14: Consider adding a prefix to uniqid() for better uniqueness.

Line 14 uses uniqid() without a prefix to generate the block ID. While collisions are unlikely, adding a prefix improves uniqueness and makes IDs more identifiable in the DOM.

🔎 Suggested improvement
-$block_id = isset( $block_id ) ? $block_id : 'wpuf-directory-' . uniqid();
+$block_id = isset( $block_id ) ? $block_id : 'wpuf-directory-' . uniqid('sort-', true);

31-47: Consider extracting common select styling to reduce duplication.

The dropdown styling (including the inline SVG background) is duplicated across both select elements. Consider defining a shared CSS class or variable to improve maintainability.

src/js/user-directory/components/common/Toast.js (1)

12-12: Consider adding prop validation for the type parameter.

The type prop defaults to 'success' but accepts any string. Invalid types will fall back to undefined styles. Consider adding PropTypes or validating the type to prevent silent failures when an invalid type is passed.

🔎 Suggested validation
 const Toast = ({ message, type = 'success', duration = 3000, onClose }) => {
+    const validTypes = ['success', 'error', 'warning', 'info'];
+    const safeType = validTypes.includes(type) ? type : 'success';
     const [isVisible, setIsVisible] = useState(true);
     const [isLeaving, setIsLeaving] = useState(false);

Then use safeType instead of type in typeStyles[type] and icons[type].

modules/user-directory/tailwind.config.js (1)

14-75: Review the safelist for necessary entries only.

The safelist contains 60+ utility classes, which increases the CSS bundle size. Ensure all these classes are actually needed and cannot be detected through content scanning. Classes that appear dynamically in PHP or JS should remain in the safelist, but hardcoded classes in templates should be automatically detected.

modules/user-directory/views/directory/template-parts/social-icons.php (1)

33-71: Consider adding aria-labels for improved accessibility.

The social media links lack descriptive aria-labels, which would help screen reader users identify the platform. Consider adding aria-label attributes like "Visit Facebook profile" to each anchor tag.

🔎 Example for Facebook link
-<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" class="wpuf-social-icon" aria-label="<?php esc_attr_e( 'Visit Facebook profile', 'wp-user-frontend' ); ?>">

Apply similar changes to Twitter, LinkedIn, and Instagram links.

src/js/user-directory/components/common/DeleteConfirmModal.js (1)

14-61: Consider adding modal accessibility enhancements.

The modal could benefit from the following accessibility improvements:

  • Add ARIA attributes: role="dialog", aria-modal="true", aria-labelledby pointing to the title
  • Implement keyboard support: ESC key to close, focus trap to prevent tabbing outside the modal
  • Consider allowing overlay click to dismiss (common UX pattern)
modules/user-directory/views/directory/template-parts/search-field.php (1)

17-21: Potential undefined variable access and default layout inconsistency.

The template accesses $_GET['search_by'] safely with sanitization. However:

  1. The default layout 'layout-1' on line 20 differs from the helper function's default of 'layout-3' in Helpers.php (line 47). This inconsistency could cause unexpected styling.

  2. Variables $block_id and $page_id (used on lines 29-30, 46-47) are used without existence checks, which could trigger PHP notices if the template is included without these being set.

🔎 Proposed fix
 // Get current search_by parameter from URL
-$current_search_by = ! empty( $_GET['search_by'] ) ? sanitize_text_field( $_GET['search_by'] ) : '';
+$current_search_by = isset( $_GET['search_by'] ) ? sanitize_text_field( wp_unslash( $_GET['search_by'] ) ) : '';
 
 // Get layout-specific colors
-$layout = isset( $layout ) ? $layout : 'layout-1';
+$layout = isset( $layout ) ? $layout : 'layout-3';
 $colors = wpuf_ud_get_layout_colors( $layout );
+
+// Ensure block_id and page_id are defined
+$block_id = isset( $block_id ) ? $block_id : '';
+$page_id = isset( $page_id ) ? $page_id : '';
modules/user-directory/views/directory/template-parts/row-3.php (3)

37-40: Avatar HTML output should be explicitly allowed or escaped.

The $avatar variable contains raw HTML from wpuf_ud_get_user_avatar_html(). While the helper function performs internal escaping, echoing raw HTML without wp_kses_post() or similar could be a concern if the helper is modified later.

Consider using wp_kses_post() for defense-in-depth:

         <div class="wpuf-flex-shrink-0">
-            <?php echo $avatar; ?>
+            <?php echo wp_kses_post( $avatar ); ?>
         </div>

116-125: Missing wp_unslash() on $_GET values.

When accessing $_GET superglobals, WordPress recommends using wp_unslash() before sanitize_text_field() to properly handle slashes added by PHP's magic quotes emulation.

🔎 Proposed fix
         } elseif ( isset( $_GET['page'] ) ) {
-            $current_page = intval( $_GET['page'] );
+            $current_page = absint( wp_unslash( $_GET['page'] ) );
         }

97-159: Profile URL generation logic is duplicated.

The profile URL generation with state preservation (lines 100-153) appears to be duplicated across multiple row templates (row-1.php, row-3.php, etc. as mentioned in comments). Consider extracting this into a dedicated helper function in Helpers.php to reduce duplication and improve maintainability.

assets/js/wpuf-user-directory-frontend.js (2)

22-31: Consider using event delegation for dynamically loaded elements.

The search form handler uses direct binding (.on('submit', ...)), which won't work for forms loaded via AJAX. If the search form can be dynamically replaced, consider delegating to a static parent.

-        $('.wpuf-ud-search-form').on('submit', function(e) {
+        $(document).on('submit', '.wpuf-ud-search-form', function(e) {

33-37: Pagination click handler may not persist after AJAX reload.

Similar to the search form, pagination links may be replaced after AJAX requests. Using event delegation ensures the handler works after content updates.

-        $('.wpuf-ud-pagination a').on('click', function() {
-            var $grid = $(this).closest('.wpuf-user-directory').find('.wpuf-ud-grid');
+        $(document).on('click', '.wpuf-ud-pagination a', function() {
+            var $grid = $(this).closest('.wpuf-user-directory').find('.wpuf-ud-grid');
modules/user-directory/views/profile/template-parts/posts-2.php (2)

4-6: Add wp_unslash() to $_GET access.

WordPress recommends using wp_unslash() when accessing superglobals.

🔎 Proposed fix
 // Get current page from query string for posts pagination
-$posts_page     = isset( $_GET['posts_page'] ) ? max( 1, intval( $_GET['posts_page'] ) ) : 1;
+$posts_page     = isset( $_GET['posts_page'] ) ? max( 1, absint( wp_unslash( $_GET['posts_page'] ) ) ) : 1;
 $posts_per_page = 10;

116-125: Avoid defining functions inside templates.

Defining wpuf_ud_build_posts_page_url inside a template with function_exists check is fragile. If multiple templates need this, define it once in Helpers.php. If it's template-specific, use a closure or inline the logic.

Consider moving this function to modules/user-directory/Helpers.php alongside the existing wpuf_ud_build_page_url function for consistency and reusability.

src/js/user-directory/components/steps/StepLayout.js (3)

35-43: Layout display names should use i18n.

The layoutDisplayNames object contains hardcoded English strings. These should be translated using the __() function for consistency with the rest of the component.

🔎 Proposed fix
     // Custom display names for layouts
     const layoutDisplayNames = {
-        'layout-1': 'Classic Table',
-        'layout-2': 'Rounded Sidecard (2x)',
-        'layout-3': 'Round Grid (3x)',
-        'layout-4': 'Square Grid (3x)',
-        'layout-5': 'Round Grid (4x)',
-        'layout-6': 'Square Grid (4x)'
+        'layout-1': __('Classic Table', 'wp-user-frontend'),
+        'layout-2': __('Rounded Sidecard (2x)', 'wp-user-frontend'),
+        'layout-3': __('Round Grid (3x)', 'wp-user-frontend'),
+        'layout-4': __('Square Grid (3x)', 'wp-user-frontend'),
+        'layout-5': __('Round Grid (4x)', 'wp-user-frontend'),
+        'layout-6': __('Square Grid (4x)', 'wp-user-frontend')
     };

51-60: Remove empty style properties and fix letterSpacing value.

There's an empty line in the style object (line 53), and letterSpacing: '0%' is non-standard. CSS letter-spacing typically uses px, em, or normal.

🔎 Proposed fix
                 <h2 className="wpuf-text-center"
                     style={{
-                        
                         fontWeight: 400,
                         fontSize: '20px',
                         lineHeight: '36px',
-                        letterSpacing: '0%',
+                        letterSpacing: 'normal',
                         textAlign: 'center',
                         color: '#000000'
                     }}

114-116: Add fallback for missing asset URL.

If both wpuf_ud_free and wpuf_ud are undefined, the image src will be /images/pro-badge.svg, which may fail to load. Consider adding a more robust fallback.

-                                            src={(window.wpuf_ud_free?.asset_url || window.wpuf_ud?.asset_url || '') + '/images/pro-badge.svg'} 
+                                            src={(window.wpuf_ud_free?.asset_url || window.wpuf_ud?.asset_url || '/wp-content/plugins/wp-user-frontend/modules/user-directory/assets') + '/images/pro-badge.svg'} 

Alternatively, consider showing a text-only "Pro" badge if the image fails to load.

modules/user-directory/views/directory/template-parts/pagination-shortcode.php (2)

28-29: Default layout inconsistency.

The default layout 'layout-1' differs from the helper function's default of 'layout-3' in Helpers.php. Consider aligning these for consistency.

 // Get layout-specific colors
-$layout = isset( $layout ) ? $layout : 'layout-1';
+$layout = isset( $layout ) ? $layout : 'layout-3';

99-106: Escape URL outputs from wpuf_ud_build_page_url.

While wpuf_ud_build_page_url uses esc_url() internally (per the relevant code snippet), it's a best practice to escape at the point of output for defense-in-depth.

The URLs on lines 100, 129, and 139 should use esc_url():

-            <a href="<?php echo wpuf_ud_build_page_url( $base_url, $query_args, $current - 1 ); ?>"
+            <a href="<?php echo esc_url( wpuf_ud_build_page_url( $base_url, $query_args, $current - 1 ) ); ?>"

Note: If wpuf_ud_build_page_url already returns an escaped URL, this is redundant but harmless.

Also applies to: 128-132, 138-145

modules/user-directory/views/profile/template-parts/comments-2.php (1)

5-5: Sanitize and validate $_GET input.

The comments_page parameter is read directly from $_GET. While intval() provides type safety, consider using absint() and add explicit sanitization for defense in depth.

🔎 Proposed fix
-$comments_page = isset( $_GET['comments_page'] ) ? max( 1, intval( $_GET['comments_page'] ) ) : 1;
+$comments_page = isset( $_GET['comments_page'] ) ? max( 1, absint( wp_unslash( $_GET['comments_page'] ) ) ) : 1;
modules/user-directory/views/profile/template-parts/user-avatar.php (2)

50-51: $can_edit is computed but unused.

The variable is set but never referenced in the template output. Either remove it or implement the intended edit functionality.


31-45: Consider extracting initials logic to a shared helper.

The comment notes this is "same logic as directory." Consider using a centralized helper like wpuf_ud_get_user_initials() in Helpers.php to avoid duplication and ensure consistency.

assets/css/wpuf-user-directory-frontend.css (1)

13-15: Consider consolidating duplicate font-family declarations.

The same system font stack is declared for both .wpuf-user-directory and .wpuf-profile. Consider using a shared class or CSS custom property to maintain consistency and reduce repetition.

🔎 Example using CSS custom property
+:root {
+    --wpuf-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+}
+
 .wpuf-user-directory {
-    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+    font-family: var(--wpuf-font-family);
 }
 
 .wpuf-profile {
     max-width: 800px;
     margin: 0 auto;
     padding: 1rem;
-    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+    font-family: var(--wpuf-font-family);
 }

Also applies to: 270-275

src/js/user-directory/components/steps/StepBasics.js (1)

112-169: Consider extracting repeated inline styles to CSS classes or constants.

The same style object pattern (maxWidth, height, borderRadius, padding, etc.) is duplicated across multiple input fields. Extracting to a shared constant or Tailwind utility classes would improve maintainability.

src/js/user-directory/components/DirectoryWizard.js (2)

119-126: Prefer wp.apiFetch over native fetch for WordPress REST calls.

Using wp.apiFetch provides automatic nonce handling, middleware support, and consistent error formatting. The current implementation manually handles nonce and URL construction, which wp.apiFetch handles automatically.

🔎 Example refactor
+import apiFetch from '@wordpress/api-fetch';
+
 const handleSubmit = async () => {
     setLoading(true);
     setError('');
     
     try {
-        const restUrl = config.rest_url || '/wp-json/';
-        const url = formData.ID
-            ? `${restUrl}wpuf/v1/user_directory/${formData.ID}`
-            : `${restUrl}wpuf/v1/user_directory`;
-        
-        const response = await fetch(url, {
-            method: formData.ID ? 'PUT' : 'POST',
-            headers: {
-                'Content-Type': 'application/json',
-                'X-WP-Nonce': config.rest_nonce || '',
-            },
-            body: JSON.stringify(formData),
+        const response = await apiFetch({
+            path: formData.ID
+                ? `/wpuf/v1/user_directory/${formData.ID}`
+                : '/wpuf/v1/user_directory',
+            method: formData.ID ? 'PUT' : 'POST',
+            data: formData,
         });

266-267: Fragile DOM coupling for sidebar detection.

Reading document.body.classList.contains('folded') tightly couples this component to WordPress admin's sidebar implementation. Consider using a CSS-only solution or passing sidebar state via props/context.

assets/js/ud-search-shortcode.js (1)

52-63: Add handling for network errors and non-JSON responses.

The current error handling assumes all responses are JSON. Network failures or 5xx errors with HTML bodies will cause res.json() to throw.

🔎 Proposed fix
-fetch(apiUrl + '?' + params.toString(), {
-    credentials: 'same-origin',
-})
-    .then(res => res.json())
-    .then(data => {
-        if (data && data.success) {
-            onSuccess(data);
-        } else {
-            onError(data);
-        }
-    })
-    .catch(onError);
+fetch(apiUrl + '?' + params.toString(), {
+    credentials: 'same-origin',
+})
+    .then(res => {
+        if (!res.ok) {
+            throw new Error(`HTTP ${res.status}`);
+        }
+        return res.json();
+    })
+    .then(data => {
+        if (data && data.success) {
+            onSuccess(data);
+        } else {
+            onError(data);
+        }
+    })
+    .catch(onError);
src/js/user-directory/components/common/MultiSelect.js (2)

1-4: Inconsistent indentation on line 4.

There are extra leading spaces before const MultiSelect on line 4. This appears to be a formatting issue.

🔎 Proposed fix
 import { useState, useRef, useEffect } from '@wordpress/element';
 import { __ } from '@wordpress/i18n';

-    const MultiSelect = ({ options, value, onChange, placeholder, sortable = false, searchable = true, selectedLabel = 'items' }) => {
+const MultiSelect = ({ options, value, onChange, placeholder, sortable = false, searchable = true, selectedLabel = 'items' }) => {

26-43: Fragile icon cloning assumes specific structure.

The icon cloning logic assumes option.icon.props.children exists and is a single clonable element. If the icon has multiple children or a different structure, this will fail silently or throw an error.

Consider adding defensive checks:

const getOptionIcon = (key, isSelected = false) => {
    const option = options[key];
    if (!option?.icon) return null;
    
    // Return icon as-is if cloning isn't safe
    const iconChildren = option.icon.props?.children;
    if (!iconChildren || typeof iconChildren !== 'object') {
        return option.icon;
    }
    
    // Proceed with cloning...
};
modules/user-directory/PrettyUrls.php (2)

146-171: Consider caching or optimizing the page query for large sites.

get_directory_pages() queries all published pages (posts_per_page => -1) and iterates through each to check for shortcodes. On sites with many pages, this could become a performance bottleneck since it runs on every init hook.

Consider using transient caching for the directory pages list:

private function get_directory_pages() {
    $cache_key = 'wpuf_ud_directory_pages';
    $cached = get_transient( $cache_key );
    
    if ( false !== $cached ) {
        return $cached;
    }
    
    $pages = get_posts( [
        'post_type'      => 'page',
        'post_status'    => 'publish',
        'posts_per_page' => -1,
        'fields'         => 'ids', // Optimize initial query
    ] );
    
    // ... rest of logic
    
    set_transient( $cache_key, $directory_pages, HOUR_IN_SECONDS );
    return $directory_pages;
}

Then invalidate the cache in maybe_flush_rules().


182-199: flush_rewrite_rules() on every relevant page save may cause performance issues.

Calling flush_rewrite_rules() is an expensive operation that writes to the database. While the guard for autosave and post type is good, this still fires on every save of a page containing the shortcode.

Consider deferring the flush or using a transient flag to batch flushes:

public function maybe_flush_rules( $post_id ) {
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    $post = get_post( $post_id );

    if ( ! $post || 'page' !== $post->post_type ) {
        return;
    }

    if ( has_shortcode( $post->post_content, 'wpuf_user_listing' ) ||
         has_shortcode( $post->post_content, 'wpuf_user_listing_id' ) ) {
        // Set a flag to flush on next admin_init instead of immediately
        update_option( 'wpuf_ud_flush_rewrite_rules', true );
    }
}

Then flush during admin_init if the flag is set.

src/js/user-directory/App.js (2)

99-127: Consider replacing alert() with toast notifications for better UX.

The delete error handling uses browser alert() which blocks the UI and provides a jarring experience. The DirectoryWizard component appears to use a success toast pattern that could be adopted here for consistency.

🔎 Example toast pattern
const [toast, setToast] = useState({ show: false, message: '', type: 'error' });

// In handleDelete error cases:
setToast({ show: true, message: 'Failed to delete directory', type: 'error' });

// In JSX, render a toast component
{toast.show && <Toast message={toast.message} type={toast.type} onClose={() => setToast({ show: false })} />}

16-24: ProBadge component is duplicated across files.

This ProBadge component definition appears similar to one in DirectoryList.js per the provided snippets. Consider extracting it to a shared location like components/common/ProBadge.js for consistency and maintainability.

src/js/user-directory/components/steps/StepAdvanced.js (2)

116-122: ProBadge component is duplicated across multiple files.

This is the third instance of an inline ProBadge component in this PR (also in App.js and referenced in DirectoryList.js). Consider extracting to a shared component.

Create src/js/user-directory/components/common/ProBadge.js:

import React from 'react';

const ProBadge = ({ small = false, className = '' }) => (
    <img
        src={(window.wpuf_ud_free?.asset_url || window.wpuf_ud?.asset_url || window.wpuf_admin_script?.asset_url || '') + '/images/pro-badge.svg'}
        alt="Pro"
        className={className}
        style={{ 
            width: small ? '30px' : '39px', 
            height: small ? '17px' : '22px',
            display: 'inline-block',
            verticalAlign: 'middle'
        }}
    />
);

export default ProBadge;

67-94: Large inline style objects could be extracted to CSS.

The inputStyle and numberInputStyle objects contain substantial styling that could be moved to CSS classes for better maintainability and consistency with the Tailwind approach used elsewhere in the codebase.

src/js/user-directory/components/steps/StepTabs.js (3)

65-65: Unused state setter can be simplified.

The enabledTabs state setter is discarded but the state itself never changes. Consider using useMemo instead of useState for a derived value that doesn't need to be updated.

Proposed fix
-    const [enabledTabs] = useState(() => getEnabledTabsArray(formData.profile_tabs));
+    const enabledTabs = React.useMemo(() => getEnabledTabsArray(formData.profile_tabs), [formData.profile_tabs]);

167-187: Add keyboard accessibility for drag-and-drop reordering.

The draggable tabs lack keyboard support for reordering. Screen reader users and keyboard-only users cannot reorder tabs. Consider adding aria-label, role="listitem", and keyboard handlers for arrow key navigation.


338-338: Fixed width may cause layout issues on smaller screens.

The content type buttons container uses a fixed width: '590px' which may cause horizontal overflow on smaller viewports.

Proposed fix
-                <div className="wpuf-mt-4 wpuf-p-4 wpuf-bg-white wpuf-border wpuf-border-gray-200 wpuf-rounded-lg" style={{ width: '590px' }}>
+                <div className="wpuf-mt-4 wpuf-p-4 wpuf-bg-white wpuf-border wpuf-border-gray-200 wpuf-rounded-lg wpuf-max-w-full" style={{ width: '590px', maxWidth: '100%' }}>
modules/user-directory/views/profile/layout-2.php (2)

154-156: str_word_count may not work correctly for non-Latin text.

The word counting uses str_word_count() which is designed for Latin alphabets and may produce incorrect counts for CJK or other scripts. Consider using a more robust approach for internationalization.

Alternative approach
// More i18n-friendly word count
$word_count = count( preg_split( '/\s+/u', trim( $user_bio ), -1, PREG_SPLIT_NO_EMPTY ) );

458-537: Global JavaScript functions may conflict with other plugins.

The functions wpuf_ud_goBack() and wpuf_toggleBio_layout2() are defined in the global scope. Consider namespacing these under a wpuf object to prevent potential conflicts.

Proposed approach
// Define under namespace
window.wpuf_ud = window.wpuf_ud || {};
window.wpuf_ud.goBack = function() { /* ... */ };
window.wpuf_ud.toggleBio = function(bioId) { /* ... */ };

Then update the button onclick handlers accordingly.

modules/user-directory/DirectoryStyles.php (1)

314-319: Add nonce verification comment for direct $_GET access.

While nonce verification isn't practical for URL parameters used in page detection, adding a PHPCS ignore comment would clarify this is intentional and improve code clarity.

Proposed fix
         foreach ( $profile_params as $param ) {
+            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- URL params for profile detection
             if ( ! empty( $_GET[$param] ) ) {
                 // Additional validation: ensure we're on a directory page
                 return $this->is_directory_page();
             }
         }
modules/user-directory/User_Directory.php (2)

109-113: Public constructor undermines singleton pattern.

The constructor is public while the class uses a singleton pattern via get_instance(). This allows bypassing the singleton by calling new User_Directory() directly. Consider making the constructor private.

Proposed fix
-    public function __construct() {
+    private function __construct() {
         $this->define_constants();
         $this->includes();
         $this->init_hooks();
     }

223-232: Consider using wp_count_posts() for efficiency.

Using get_posts with posts_per_page => -1 loads all directory posts into memory just to count them. For a simple count, wp_count_posts() is more efficient.

Proposed fix
     public static function get_directory_count() {
-        $directories = get_posts( [
-            'post_type'      => self::POST_TYPE,
-            'post_status'    => 'any',
-            'posts_per_page' => -1,
-            'fields'         => 'ids',
-        ] );
-
-        return count( $directories );
+        $counts = wp_count_posts( self::POST_TYPE );
+        $total = 0;
+        foreach ( $counts as $status => $count ) {
+            $total += (int) $count;
+        }
+        return $total;
     }
modules/user-directory/Shortcode.php (1)

463-484: extract() usage is documented but consider alternatives.

While the PHPCS ignore comment is present, extract() makes it harder to trace variable origins in templates. Consider passing $data directly and accessing keys explicitly in templates for better maintainability.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7d7e76 and a9ce975.

⛔ 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 (62)
  • Gruntfile.js
  • assets/css/admin/subscriptions.min.css
  • assets/css/admin/wpuf-module.css
  • assets/css/ai-form-builder.min.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/Free/Free_Loader.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/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
🧰 Additional context used
🧬 Code graph analysis (23)
modules/user-directory/views/directory/template-parts/pagination-shortcode.php (2)
assets/js/ud-search-shortcode.js (2)
  • pagination (76-76)
  • layout (84-84)
modules/user-directory/Helpers.php (3)
  • wpuf_ud_get_profile_layout_colors (61-63)
  • wpuf_ud_get_layout_colors (48-50)
  • wpuf_ud_build_page_url (76-119)
src/js/user-directory/utils/avatarSizeHelper.js (1)
assets/js/ud-search-shortcode.js (1)
  • layout (84-84)
assets/js/wpuf-user-directory-frontend.js (1)
assets/js/ud-search-shortcode.js (1)
  • params (36-36)
modules/user-directory/views/profile/template-parts/file-2.php (1)
Lib/WeDevs_Settings_API.php (1)
  • get_option (649-658)
src/js/user-directory/index.js (1)
src/js/user-directory/App.js (1)
  • App (26-216)
src/js/user-directory/App.js (4)
src/js/user-directory/components/DirectoryList.js (2)
  • ProBadge (13-20)
  • DirectoryList (22-326)
src/js/user-directory/components/DirectoryWizard.js (3)
  • loading (36-36)
  • error (37-37)
  • DirectoryWizard (34-327)
src/js/user-directory/components/common/Header.js (2)
  • Header (13-85)
  • wpuf (15-15)
src/js/user-directory/components/common/DeleteConfirmModal.js (1)
  • DeleteConfirmModal (12-63)
modules/user-directory/views/directory/template-parts/row-3.php (1)
modules/user-directory/Helpers.php (2)
  • wpuf_ud_get_user_avatar_html (186-239)
  • wpuf_ud_get_profile_url (251-366)
src/js/user-directory/components/DirectoryList.js (3)
src/js/user-directory/App.js (1)
  • ProBadge (17-24)
src/js/user-directory/components/steps/StepAdvanced.js (1)
  • ProBadge (116-122)
src/js/user-directory/components/steps/StepTabs.js (1)
  • defaultTabs (6-10)
src/js/user-directory/components/steps/StepProfile.js (3)
src/js/user-directory/components/steps/StepLayout.js (4)
  • freeLayout (7-7)
  • handleChange (9-28)
  • layoutOptions (31-33)
  • isProLayout (46-46)
src/js/user-directory/components/steps/StepBasics.js (1)
  • handleChange (34-37)
src/js/user-directory/components/DirectoryWizard.js (1)
  • formData (35-35)
modules/user-directory/PrettyUrls.php (4)
modules/user-directory/User_Directory.php (2)
  • User_Directory (24-320)
  • __construct (109-113)
modules/user-directory/Admin_Menu.php (1)
  • __construct (31-34)
modules/user-directory/Post_Type.php (1)
  • __construct (31-33)
modules/user-directory/Shortcode.php (1)
  • __construct (31-33)
src/js/user-directory/components/steps/StepAdvanced.js (3)
src/js/user-directory/components/steps/StepLayout.js (1)
  • handleChange (9-28)
src/js/user-directory/components/steps/StepProfile.js (1)
  • handleChange (8-17)
src/js/user-directory/components/common/Tooltip.js (1)
  • Tooltip (3-34)
modules/user-directory/views/profile/template-parts/user-avatar.php (1)
modules/user-directory/Helpers.php (1)
  • wpuf_ud_get_avatar_url (131-173)
modules/user-directory/views/directory/template-parts/search-field.php (1)
modules/user-directory/Helpers.php (1)
  • wpuf_ud_get_layout_colors (48-50)
modules/user-directory/User_Directory.php (2)
includes/Free/Free_Loader.php (2)
  • admin_menu (190-203)
  • includes (78-87)
wpuf-functions.php (1)
  • wpuf_is_pro_active (5045-5047)
modules/user-directory/views/profile/layout-2.php (3)
modules/user-directory/Helpers.php (3)
  • wpuf_ud_get_profile_data (379-523)
  • wpuf_ud_get_cover_photo_data (735-764)
  • wpuf_ud_get_tab_label (617-632)
src/js/user-directory/App.js (1)
  • config (36-36)
wpuf-functions.php (1)
  • wpuf_get_option (1546-1554)
modules/user-directory/Api/Directory.php (2)
modules/user-directory/User_Directory.php (3)
  • User_Directory (24-320)
  • has_reached_limit (206-214)
  • get_default_settings (286-319)
wpuf-functions.php (1)
  • wpuf_is_pro_active (5045-5047)
modules/user-directory/DirectoryStyles.php (2)
modules/user-directory/PrettyUrls.php (1)
  • __construct (31-55)
modules/user-directory/Shortcode.php (1)
  • __construct (31-33)
src/js/user-directory/components/steps/StepTabs.js (2)
src/js/user-directory/App.js (1)
  • config (36-36)
assets/js/wpuf-user-directory-free.js (1)
  • e (2-2)
modules/user-directory/views/profile/template-parts/posts-2.php (2)
assets/js/ud-search-shortcode.js (1)
  • pagination (76-76)
tests/e2e/utils/generate-summary.js (1)
  • total (217-217)
modules/user-directory/views/directory/layout-3.php (1)
assets/js/ud-search-shortcode.js (1)
  • layout (84-84)
includes/functions/modules.php (1)
Lib/WeDevs_Settings_API.php (1)
  • get_option (649-658)
src/js/user-directory/components/steps/StepLayout.js (5)
src/js/user-directory/components/steps/StepProfile.js (4)
  • freeLayout (6-6)
  • handleChange (8-17)
  • layoutOptions (20-22)
  • isProLayout (25-25)
src/js/user-directory/components/steps/StepAdvanced.js (1)
  • handleChange (14-65)
src/js/user-directory/components/steps/StepBasics.js (1)
  • handleChange (34-37)
src/js/user-directory/utils/avatarSizeHelper.js (2)
  • getOptimalAvatarSize (7-17)
  • getOptimalAvatarSize (7-17)
src/js/user-directory/components/DirectoryWizard.js (1)
  • formData (35-35)
includes/Free/Free_Loader.php (2)
wpuf-functions.php (1)
  • wpuf_is_pro_active (5045-5047)
includes/functions/modules.php (4)
  • wpuf_free_is_module_active (67-69)
  • wpuf_free_get_modules (23-34)
  • wpuf_free_activate_module (93-110)
  • wpuf_free_deactivate_module (121-136)
🪛 PHPMD (2.15.0)
modules/user-directory/Api/Directory.php

131-131: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)


513-513: Avoid unused local variables such as '$user'. (undefined)

(UnusedLocalVariable)


515-515: Avoid unused local variables such as '$all_data'. (undefined)

(UnusedLocalVariable)


534-534: Avoid unused local variables such as '$pagination'. (undefined)

(UnusedLocalVariable)


550-550: Avoid unused local variables such as '$layout'. (undefined)

(UnusedLocalVariable)

modules/user-directory/DirectoryStyles.php

80-80: Avoid unused parameters such as '$parsed_block'. (undefined)

(UnusedFormalParameter)


80-80: Avoid unused parameters such as '$block'. (undefined)

(UnusedFormalParameter)


412-412: Avoid unused parameters such as '$post'. (undefined)

(UnusedFormalParameter)

modules/user-directory/Shortcode.php

493-505: Avoid unused private methods such as 'get_current_page'. (undefined)

(UnusedPrivateMethod)

modules/user-directory/Helpers.php

25-25: Avoid unused parameters such as '$layout'. (undefined)

(UnusedFormalParameter)


645-645: Avoid unused parameters such as '$type'. (undefined)

(UnusedFormalParameter)


721-721: Avoid unused parameters such as '$user'. (undefined)

(UnusedFormalParameter)


776-776: Avoid unused parameters such as '$user_id'. (undefined)

(UnusedFormalParameter)


776-776: Avoid unused parameters such as '$meta_key'. (undefined)

(UnusedFormalParameter)


793-793: Avoid unused parameters such as '$field'. (undefined)

(UnusedFormalParameter)


793-793: Avoid unused parameters such as '$user'. (undefined)

(UnusedFormalParameter)


793-793: Avoid unused parameters such as '$user_id'. (undefined)

(UnusedFormalParameter)


793-793: Avoid unused parameters such as '$layout'. (undefined)

(UnusedFormalParameter)


817-817: Avoid unused parameters such as '$class'. (undefined)

(UnusedFormalParameter)


832-832: Avoid unused parameters such as '$field'. (undefined)

(UnusedFormalParameter)


832-832: Avoid unused parameters such as '$user_id'. (undefined)

(UnusedFormalParameter)


832-832: Avoid unused parameters such as '$layout'. (undefined)

(UnusedFormalParameter)


848-848: Avoid unused parameters such as '$field'. (undefined)

(UnusedFormalParameter)


848-848: Avoid unused parameters such as '$user_id'. (undefined)

(UnusedFormalParameter)


848-848: Avoid unused parameters such as '$template_data'. (undefined)

(UnusedFormalParameter)


848-848: Avoid unused parameters such as '$color'. (undefined)

(UnusedFormalParameter)

'use strict';

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 | 🟠 Major

Shared debounceTimeout variable causes cross-listing interference.

The debounceTimeout variable is declared at the module scope but used across all listing instances. If multiple directories exist on a page, typing in one will cancel the debounce timer of another.

🔎 Proposed fix — move timeout inside initUserDirectorySearch
-(function(window, document) {
-    'use strict';
-
-    const SEARCH_DEBOUNCE = 300;
-    let debounceTimeout = null;
+(function(window, document) {
+    'use strict';
+
+    const SEARCH_DEBOUNCE = 300;

     function initUserDirectorySearch(container, blockId, pageId) {
         let currentRequestId = 0;
+        let debounceTimeout = null;
         
         // ... rest of function
         
         input.addEventListener('input', function(e) {
             const value = e.target.value.trim();
             if (debounceTimeout) clearTimeout(debounceTimeout);
             debounceTimeout = setTimeout(() => {
                 performSearch(value, 1);
             }, SEARCH_DEBOUNCE);
         });

Also applies to: 335-338

🤖 Prompt for AI Agents
In assets/js/ud-search-shortcode.js around line 14 (and similarly lines
335-338), the debounceTimeout variable is declared at module scope causing
debounce timers to collide across multiple directory instances; move the
debounceTimeout declaration into the initUserDirectorySearch function (so each
instance gets its own timeout variable), update any references inside that
function to use the local variable, and remove or rename the module-level
variable to avoid shared state between listings.

Comment on lines +95 to +106
// Public search endpoint (no auth required for frontend AJAX)
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/search',
[
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'search_users' ],
'permission_callback' => '__return_true', // Public endpoint
],
]
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for rate limiting or throttling mechanisms
rg -n "rate.?limit|throttl|rateLimit|throttle|limit.*request" --type=php -g '!vendor/*' -g '!node_modules/*' -i

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1112


🏁 Script executed:

# Also check for any middleware or protection mechanisms
rg -n "middleware|guard|protect|security" --type=php modules/user-directory/ -i

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1051


🏁 Script executed:

# Check if there's any documentation about this endpoint
find . -name "README*" -o -name "*.md" | xargs rg -l "rate.?limit|search.*endpoint|directory.*search" -i 2>/dev/null | head -20

Repository: weDevsOfficial/wp-user-frontend

Length of output: 57


🏁 Script executed:

# Look for any WordPress hooks or filters that might handle rate limiting
rg -n "wp_rest_|rest_.*filter|do_action.*rest" --type=php modules/user-directory/ -i

Repository: weDevsOfficial/wp-user-frontend

Length of output: 2726


🏁 Script executed:

# Get the search_users method implementation
sed -n '241,280p' modules/user-directory/Api/Directory.php

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1414


🏁 Script executed:

# Check if there are any filters or actions applied in the callback
rg -n "search_users|apply_filters.*search|do_action.*search" modules/user-directory/ -A 5

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1382


🏁 Script executed:

# Check the full Directory.php file to understand the scope better
wc -l modules/user-directory/Api/Directory.php

Repository: weDevsOfficial/wp-user-frontend

Length of output: 119


🏁 Script executed:

# Get the full search_users method implementation
sed -n '440,520p' modules/user-directory/Api/Directory.php

Repository: weDevsOfficial/wp-user-frontend

Length of output: 3941


Public search endpoint lacks rate limiting and should be protected.

The /search endpoint is public (__return_true permission) which is necessary for frontend AJAX, but has no rate limiting. The search_users() method accepts pagination parameters (page, max_item) without any throttling mechanism, making it vulnerable to request flooding or user data scraping attacks. Implement rate limiting at the application level (e.g., via filters or a middleware layer) or document that reliance on server/CDN-level protection is required.

Comment on lines +288 to +289
// Flush rewrite rules for pretty URLs
flush_rewrite_rules();
Copy link

@coderabbitai coderabbitai bot Dec 26, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid flush_rewrite_rules() on every directory creation.

Calling flush_rewrite_rules() on every directory creation is expensive and can cause performance issues. Consider flushing only when the first directory is created, or defer to a scheduled event.

Proposed fix
-        // Flush rewrite rules for pretty URLs
-        flush_rewrite_rules();
+        // Schedule rewrite rules flush for next page load (avoids performance hit)
+        if ( ! get_option( 'wpuf_ud_flush_rewrite_rules' ) ) {
+            update_option( 'wpuf_ud_flush_rewrite_rules', true );
+        }

Then handle the flush on init hook:

add_action( 'init', function() {
    if ( get_option( 'wpuf_ud_flush_rewrite_rules' ) ) {
        flush_rewrite_rules();
        delete_option( 'wpuf_ud_flush_rewrite_rules' );
    }
}, 99 );

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In modules/user-directory/Api/Directory.php around lines 288-289, calling
flush_rewrite_rules() on every directory creation is expensive; remove the
direct flush_rewrite_rules() call and instead set a transient/option (e.g.
wpuf_ud_flush_rewrite_rules) when a directory is created to mark that rewrites
need flushing, then add an init hook (priority ~99) that checks that option,
calls flush_rewrite_rules() once, and deletes the option after flushing so the
expensive operation runs only once (or via a scheduled event) rather than on
every creation.

Copy link
Member

Choose a reason for hiding this comment

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

@arifulhoque7 bhai. flush_rewrite_rules() is an expensive function call. Minimize it as much as possible

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +65 to +90
$is_shortcode = empty( $block_id ) || strpos( $block_id, 'shortcode_' ) === 0;

if ( $is_shortcode ) {
// Use shortcode-specific pagination template
$layout = ! empty( $directory_layout ) ? $directory_layout : 'layout-3';

// Prepare query args for pagination links
$current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$parsed_url = wp_parse_url( $current_url );
$base_url = $parsed_url['path'] ?? '';

$query_args = [];
if ( ! empty( $parsed_url['query'] ) ) {
parse_str( $parsed_url['query'], $query_args );
}

// Ensure directory_layout is included in pagination links
$query_args['directory_layout'] = $layout;
unset( $query_args['page'] ); // Will be set per link

include WPUF_UD_FREE_TEMPLATES . '/directory/template-parts/pagination-shortcode.php';
} else {
// Use default block pagination template
$layout = ! empty( $directory_layout ) ? $directory_layout : 'layout-3';
include WPUF_UD_FREE_TEMPLATES . '/directory/template-parts/pagination-shortcode.php';
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Redundant conditional — both branches execute the same code.

The $is_shortcode check differentiates between shortcode and block contexts, but both branches set $layout identically and include the same pagination-shortcode.php template. Either consolidate the logic or implement the intended block-specific pagination.

🔎 Proposed simplification
 <?php
-    // Use different pagination template based on context
-    // Check if this is a shortcode context (not a block context)
-    $is_shortcode = empty( $block_id ) || strpos( $block_id, 'shortcode_' ) === 0;
-    
-    if ( $is_shortcode ) {
-        // Use shortcode-specific pagination template
-        $layout = ! empty( $directory_layout ) ? $directory_layout : 'layout-3';
-        
-        // Prepare query args for pagination links
-        $current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
-        $parsed_url = wp_parse_url( $current_url );
-        $base_url = $parsed_url['path'] ?? '';
-        
-        $query_args = [];
-        if ( ! empty( $parsed_url['query'] ) ) {
-            parse_str( $parsed_url['query'], $query_args );
-        }
-        
-        // Ensure directory_layout is included in pagination links
-        $query_args['directory_layout'] = $layout;
-        unset( $query_args['page'] ); // Will be set per link
-        
-        include WPUF_UD_FREE_TEMPLATES . '/directory/template-parts/pagination-shortcode.php';
-    } else {
-        // Use default block pagination template
-        $layout = ! empty( $directory_layout ) ? $directory_layout : 'layout-3';
-        include WPUF_UD_FREE_TEMPLATES . '/directory/template-parts/pagination-shortcode.php';
-    }
+    $layout = ! empty( $directory_layout ) ? $directory_layout : 'layout-3';
+    
+    // Prepare query args for pagination links
+    $current_url = home_url( add_query_arg( null, null ) );
+    $parsed_url = wp_parse_url( $current_url );
+    $base_url = $parsed_url['path'] ?? '';
+    
+    $query_args = [];
+    if ( ! empty( $parsed_url['query'] ) ) {
+        parse_str( $parsed_url['query'], $query_args );
+    }
+    
+    // Ensure directory_layout is included in pagination links
+    $query_args['directory_layout'] = $layout;
+    unset( $query_args['page'] ); // Will be set per link
+    
+    include WPUF_UD_FREE_TEMPLATES . '/directory/template-parts/pagination-shortcode.php';
 ?>
🤖 Prompt for AI Agents
In modules/user-directory/views/directory/layout-3.php around lines 65 to 90,
the $is_shortcode conditional is redundant because both branches set $layout the
same way and include the same pagination-shortcode.php; remove the if/else and
consolidate into a single block that sets $layout = ! empty($directory_layout) ?
$directory_layout : 'layout-3', prepares the
$current_url/$parsed_url/$query_args logic only if needed (or always, if
pagination relies on it), ensures $query_args['directory_layout'] is set and
'page' unset, then include the pagination-shortcode.php once; alternatively, if
block-specific behavior was intended, replace the else branch with the correct
block-specific pagination template and logic instead of duplicating the
shortcode branch.

Comment on lines +57 to +90
if ( ! function_exists( 'wpuf_get_image_size_dimensions_file2' ) ) {
function wpuf_get_image_size_dimensions_file2( $size ) {
global $_wp_additional_image_sizes;

// If it's already a number, use it directly
if ( is_numeric( $size ) ) {
return array( 'size' => intval( $size ), 'wp_size' => 'custom' );
}

// Default WordPress sizes
$default_sizes = array(
'thumbnail' => array( 'width' => get_option( 'thumbnail_size_w', 150 ), 'height' => get_option( 'thumbnail_size_h', 150 ) ),
'medium' => array( 'width' => get_option( 'medium_size_w', 300 ), 'height' => get_option( 'medium_size_h', 300 ) ),
'medium_large' => array( 'width' => get_option( 'medium_large_size_w', 768 ), 'height' => get_option( 'medium_large_size_h', 0 ) ),
'large' => array( 'width' => get_option( 'large_size_w', 1024 ), 'height' => get_option( 'large_size_h', 1024 ) ),
'full' => array( 'width' => 1536, 'height' => 1536 ) // Fallback for full size
);

// Check default sizes first
if ( isset( $default_sizes[ $size ] ) ) {
$width = $default_sizes[ $size ]['width'];
return array( 'size' => $width > 0 ? $width : 150, 'wp_size' => $size );
}

// Check additional custom sizes
if ( isset( $_wp_additional_image_sizes[ $size ] ) ) {
$width = $_wp_additional_image_sizes[ $size ]['width'];
return array( 'size' => $width > 0 ? $width : 150, 'wp_size' => $size );
}

// Fallback to thumbnail if size not found
return array( 'size' => get_option( 'thumbnail_size_w', 150 ), 'wp_size' => 'thumbnail' );
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Avoid defining global functions inside templates.

Defining wpuf_get_image_size_dimensions_file2 as a global function inside a template is an anti-pattern. The function_exists check suggests awareness of potential conflicts, but the suffixed name is a code smell.

Move this to modules/user-directory/Helpers.php as a static method:

// In Helpers.php
public static function get_image_size_dimensions( $size ) {
    // ... implementation
}

Then call it as:

$size_info = \WeDevs\Wpuf\Modules\User_Directory\Helpers::get_image_size_dimensions( $size_value );

@Rubaiyat-E-Mohammad
Copy link
Contributor

Directory members are not showing in list (frontend) @arifulhoque7 vai

521296897-c0d5129f-fcc8-46ac-a9b7-580b7c412f55.mov

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.
@arifulhoque7
Copy link
Contributor Author

arifulhoque7 commented Dec 26, 2025

Directory members are not showing in list (frontend) @arifulhoque7 vai

@Rubaiyat-E-Mohammad vai, now check please. It should be fixed

image

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: 3

♻️ Duplicate comments (2)
modules/user-directory/Api/Directory.php (2)

95-106: Previous concern about rate limiting on public search endpoint remains valid.

As noted in the previous review, this public endpoint (__return_true permission) lacks rate limiting protection. While public access is necessary for frontend AJAX functionality, consider implementing application-level throttling or documenting that server/CDN-level protection is required.


288-289: Previous concern about expensive flush_rewrite_rules() call remains valid.

As noted in the previous review, calling flush_rewrite_rules() on every directory creation is expensive. Consider implementing the suggested deferred flush pattern using an option flag checked on init.

🧹 Nitpick comments (2)
modules/user-directory/Shortcode.php (2)

147-155: Consider adding validation for non-numeric directory IDs.

The render_shortcode_with_id method accepts any value from the shortcode attributes array and passes it to render_from_stored_settings. While absint() handles this at line 167, consider adding early validation here to provide clearer behavior for invalid inputs.

🔎 Suggested improvement
 public function render_shortcode_with_id( $atts ) {
     if ( is_array( $atts ) && ! empty( $atts ) ) {
         $directory_id = reset( $atts );
+        
+        if ( ! is_numeric( $directory_id ) ) {
+            return '';
+        }

         return $this->render_from_stored_settings( $directory_id );
     }

     return '';
 }

446-448: Circular reference for template compatibility is acceptable but could be clearer.

The self-referential assignment $profile_data['template_data'] = $profile_data creates a circular structure for Pro template compatibility. This works but a brief inline comment explaining the reason would improve maintainability.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9ce975 and f3c8911.

📒 Files selected for processing (2)
  • modules/user-directory/Api/Directory.php
  • modules/user-directory/Shortcode.php
🧰 Additional context used
🧬 Code graph analysis (2)
modules/user-directory/Api/Directory.php (2)
modules/user-directory/User_Directory.php (3)
  • User_Directory (24-320)
  • has_reached_limit (206-214)
  • get_default_settings (286-319)
wpuf-functions.php (1)
  • wpuf_is_pro_active (5045-5047)
modules/user-directory/Shortcode.php (1)
modules/user-directory/User_Directory.php (3)
  • User_Directory (24-320)
  • get_first_directory (241-251)
  • get_default_settings (286-319)
🪛 PHPMD (2.15.0)
modules/user-directory/Api/Directory.php

131-131: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)


516-516: Avoid unused local variables such as '$user'. (undefined)

(UnusedLocalVariable)


518-518: Avoid unused local variables such as '$all_data'. (undefined)

(UnusedLocalVariable)


537-537: Avoid unused local variables such as '$pagination'. (undefined)

(UnusedLocalVariable)


553-553: Avoid unused local variables such as '$layout'. (undefined)

(UnusedLocalVariable)

modules/user-directory/Shortcode.php

496-508: Avoid unused private methods such as 'get_current_page'. (undefined)

(UnusedPrivateMethod)

🔇 Additional comments (6)
modules/user-directory/Shortcode.php (2)

31-67: LGTM!

Good use of the filter pattern to allow Pro version to override shortcode registration. The hook structure is clean and extensible.


261-314: LGTM!

The directory rendering logic is well-structured with proper sanitization, sensible defaults, and good Pro compatibility. The role filtering correctly handles the 'all' case, and the search implementation follows WordPress patterns.

modules/user-directory/Api/Directory.php (4)

152-202: LGTM!

The get_items implementation properly handles pagination, search, and post data reset. Response structure is consistent and well-formed.


396-429: LGTM!

The get_user_count method correctly handles role filtering, user exclusions, and max item caps. The use of count_total is appropriate for counting without fetching user data.


510-532: Static analysis false positives - variables are used by included templates.

The variables $user, $all_data, $pagination, and $layout flagged as unused by PHPMD are actually consumed by the included template files (row-3.php and pagination-shortcode.php). This is a common pattern in WordPress template rendering.


669-716: LGTM!

The enforce_free_limits method properly constrains settings for free users while providing filter hooks for Pro to bypass restrictions. The validation of allowed values for avatar and profile sizes is well-implemented.

Comment on lines +533 to +553
public function get_profile_url( $user, $settings = [] ) {
$profile_base = $settings['profile_base'] ?? 'username';
$current_url = get_permalink();

if ( 'user_id' === $profile_base ) {
$slug = $user->ID;
} else {
$slug = $user->user_login;
}

// Try pretty URL first
$pretty_url = trailingslashit( $current_url ) . 'user/' . $slug;

// Check if pretty URLs are enabled
if ( get_option( 'permalink_structure' ) ) {
return esc_url( $pretty_url );
}

// Fallback to query parameter
return esc_url( add_query_arg( 'wpuf_user', $user->ID, $current_url ) );
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential URL encoding issue with special characters in usernames.

When profile_base is 'username', the user_login is used directly in the URL without encoding. Usernames containing special characters (spaces, non-ASCII) could produce malformed URLs. Consider using rawurlencode().

🔎 Proposed fix
 public function get_profile_url( $user, $settings = [] ) {
     $profile_base = $settings['profile_base'] ?? 'username';
     $current_url  = get_permalink();

     if ( 'user_id' === $profile_base ) {
         $slug = $user->ID;
     } else {
-        $slug = $user->user_login;
+        $slug = rawurlencode( $user->user_login );
     }

     // Try pretty URL first
     $pretty_url = trailingslashit( $current_url ) . 'user/' . $slug;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function get_profile_url( $user, $settings = [] ) {
$profile_base = $settings['profile_base'] ?? 'username';
$current_url = get_permalink();
if ( 'user_id' === $profile_base ) {
$slug = $user->ID;
} else {
$slug = $user->user_login;
}
// Try pretty URL first
$pretty_url = trailingslashit( $current_url ) . 'user/' . $slug;
// Check if pretty URLs are enabled
if ( get_option( 'permalink_structure' ) ) {
return esc_url( $pretty_url );
}
// Fallback to query parameter
return esc_url( add_query_arg( 'wpuf_user', $user->ID, $current_url ) );
}
public function get_profile_url( $user, $settings = [] ) {
$profile_base = $settings['profile_base'] ?? 'username';
$current_url = get_permalink();
if ( 'user_id' === $profile_base ) {
$slug = $user->ID;
} else {
$slug = rawurlencode( $user->user_login );
}
// Try pretty URL first
$pretty_url = trailingslashit( $current_url ) . 'user/' . $slug;
// Check if pretty URLs are enabled
if ( get_option( 'permalink_structure' ) ) {
return esc_url( $pretty_url );
}
// Fallback to query parameter
return esc_url( add_query_arg( 'wpuf_user', $user->ID, $current_url ) );
}
🤖 Prompt for AI Agents
In modules/user-directory/Shortcode.php around lines 533 to 553, the code uses
$user->user_login directly in the URL which can produce malformed URLs for
usernames with spaces or non-ASCII characters; change construction so the slug
is rawurlencode($user->user_login) when profile_base !== 'user_id' (leave
numeric ID branch untouched), then build the pretty_url as before and return
esc_url(...) — this ensures special characters are percent-encoded before
escaping and output.

@Rubaiyat-E-Mohammad
Copy link
Contributor

Rubaiyat-E-Mohammad commented Dec 26, 2025

  • Clicking on view profile is not redirecting to profile page @arifulhoque7 vai
Screen.Recording.2025-12-26.at.2.30.59.PM.mov
  • Searching a nonexistent user and clearing the search box shows an empty directory

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.
@arifulhoque7
Copy link
Contributor Author

  • Clicking on view profile is not redirecting to profile page @arifulhoque7 vai

Screen.Recording.2025-12-26.at.2.30.59.PM.mov

  • Searching a nonexistent user and clearing the search box shows an empty directory

done @Rubaiyat-E-Mohammad vai

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: 4

♻️ Duplicate comments (5)
assets/js/ud-search-shortcode.js (1)

10-14: Shared debounceTimeout variable causes cross-listing interference.

This issue was previously identified: the module-scoped debounceTimeout at line 14 will interfere when multiple directory instances exist on the same page. Each listing's debounce timer will cancel others'.

modules/user-directory/Shortcode.php (2)

498-510: Remove unused private method get_current_page().

This method is dead code as previously identified. The directory rendering uses $_GET['udpage'] directly at line 266, not this method.

Based on static analysis.


535-555: Potential URL encoding issue with special characters in usernames.

As previously identified, when profile_base is 'username', the user_login is used directly in the URL without encoding at line 542. Usernames with special characters could produce malformed URLs.

modules/user-directory/Api/Directory.php (2)

95-106: Public search endpoint lacks rate limiting.

As previously identified, the /search endpoint at line 103 uses __return_true for permissions (necessary for frontend AJAX) but has no rate limiting mechanism. This could enable request flooding or data scraping.


291-292: Avoid flush_rewrite_rules() on every directory creation.

As previously flagged, calling flush_rewrite_rules() on every directory creation is expensive and can cause performance issues.

🧹 Nitpick comments (9)
src/js/user-directory/components/steps/StepTabs.js (2)

33-64: Consider extracting tab initialization logic.

The useState initialization for allTabs contains complex logic that could be extracted into a separate helper function for improved readability and testability.

💡 Example refactor
+const initializeTabs = (defaultTabs, formData) => {
+    const savedOrder = Array.isArray(formData.profile_tabs_order) ? formData.profile_tabs_order : [];
+    const enabledTabs = Array.isArray(formData.profile_tabs) ? formData.profile_tabs 
+        : (formData.profile_tabs && typeof formData.profile_tabs === 'object')
+            ? Object.keys(formData.profile_tabs).filter(key => formData.profile_tabs[key])
+            : ['about'];
+    
+    const tabsMap = {};
+    Object.keys(defaultTabs).forEach(key => {
+        tabsMap[key] = {
+            key,
+            label: typeof defaultTabs[key] === 'string' ? defaultTabs[key] : defaultTabs[key]?.label || key,
+            enabled: enabledTabs.includes(key)
+        };
+    });
+    
+    const orderedTabs = [];
+    if (savedOrder.length > 0) {
+        savedOrder.forEach(key => {
+            if (tabsMap[key]) {
+                orderedTabs.push(tabsMap[key]);
+                delete tabsMap[key];
+            }
+        });
+    }
+    Object.values(tabsMap).forEach(tab => orderedTabs.push(tab));
+    
+    return orderedTabs;
+};
+
 const StepTabs = ({ formData, setFormData, config }) => {
     const defaultTabs = config?.profile_tabs || { /* ... */ };
-    const [allTabs, setAllTabs] = useState(() => {
-        const savedOrder = getOrderArray(formData.profile_tabs_order);
-        // ... (30+ lines of logic)
-        return orderedTabs;
-    });
+    const [allTabs, setAllTabs] = useState(() => initializeTabs(defaultTabs, formData));

339-339: Consider responsive width for small screens.

The fixed width of 590px may cause horizontal overflow on smaller screens. Consider using max-width or responsive width utilities to ensure better mobile experience.

🔎 Suggested improvement
-                <div className="wpuf-mt-4 wpuf-p-4 wpuf-bg-white wpuf-border wpuf-border-gray-200 wpuf-rounded-lg" style={{ width: '590px' }}>
+                <div className="wpuf-mt-4 wpuf-p-4 wpuf-bg-white wpuf-border wpuf-border-gray-200 wpuf-rounded-lg wpuf-w-full" style={{ maxWidth: '590px' }}>
assets/js/ud-search-shortcode.js (1)

172-183: Hard-coded grid structure creates maintenance coupling with PHP template.

The innerHTML at line 179 duplicates the exact grid structure from layout-3.php with specific Tailwind classes. If the PHP template's structure changes, this JavaScript will break or create mismatched layouts when restoring the list container after "no results."

Consider storing the original container HTML on initialization or fetching the structure from a data attribute to reduce coupling.

🔎 Alternative approach

Store the original list structure on initialization:

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

     const listingDiv = document.querySelector('.wpuf-user-listing[data-block-id="' + blockId + '"]');
     const input = listingDiv ? listingDiv.querySelector('.wpuf-ud-search-input') : null;
     let userList = listingDiv ? (
         listingDiv.querySelector('.wpuf-ud-tbody') ||
         listingDiv.querySelector('ul[role="list"]')
     ) : null;
     
+    // Store original structure for restoration
+    if (listingDiv && userList) {
+        const listContainer = listingDiv.querySelector('.wpuf-ud-list');
+        if (listContainer) {
+            originalListHTML = listContainer.innerHTML;
+        }
+    }

Then use it when restoring:

-                            listContainer.innerHTML = '<div><ul role="list" class="!wpuf-mx-auto !wpuf-grid !wpuf-max-w-2xl !wpuf-grid-cols-1 !wpuf-gap-x-6 !wpuf-gap-y-6 sm:!wpuf-grid-cols-2 lg:!wpuf-mx-0 lg:!wpuf-max-w-none lg:!wpuf-grid-cols-3"></ul></div>';
+                            if (originalListHTML) {
+                                listContainer.innerHTML = originalListHTML;
+                            }
src/js/user-directory/components/steps/StepAdvanced.js (6)

51-55: Consider providing user feedback when Pro features are clicked.

When a user clicks a Pro-only avatar size, the change is silently ignored. While this is intentional for feature gating, it provides no feedback to the user, who might think the UI is broken.

Since the Pro badge appears on hover, consider adding a visual indication (e.g., a brief tooltip or alert) when a Pro feature is clicked, or ensure the Pro badge remains visible long enough to convey that the feature requires an upgrade.


67-94: Move style objects outside the component to avoid recreation on every render.

The inputStyle object is recreated on every render. Since it doesn't depend on props or state, move it outside the component definition to improve performance.

🔎 Proposed refactor

Move both inputStyle and numberInputStyle (lines 67-113) above the component definition:

+// Common input style for select dropdowns
+const inputStyle = {
+    maxWidth: '793px',
+    width: '100%',
+    // ... rest of styles
+};
+
+// Number input style
+const numberInputStyle = {
+    maxWidth: '793px',
+    // ... rest of styles
+};
+
 const StepAdvanced = ({ formData, setFormData, config }) => {
     const [hoveredOption, setHoveredOption] = useState(null);
-    
-    // Common input style for select dropdowns
-    const inputStyle = {
-        maxWidth: '793px',
-        // ... (remove from here)
-    };

118-118: Ensure asset URL fallback doesn't result in broken image paths.

If both config?.asset_url and window.wpuf_ud_free?.asset_url are undefined, the empty string fallback will produce an invalid image src like '/images/pro-badge.svg', which may fail to load.

Consider providing a more robust fallback or logging a warning when the asset URL is missing.

🔎 Proposed fix
-src={(config?.asset_url || window.wpuf_ud_free?.asset_url || '') + '/images/pro-badge.svg'}
+src={(config?.asset_url || window.wpuf_ud_free?.asset_url || 'data:image/svg+xml,<svg/>') + (config?.asset_url || window.wpuf_ud_free?.asset_url ? '/images/pro-badge.svg' : '')}

Or better, validate at the top of the component:

 const StepAdvanced = ({ formData, setFormData, config }) => {
+    const assetUrl = config?.asset_url || window.wpuf_ud_free?.asset_url || '';
+    if (!assetUrl) {
+        console.warn('StepAdvanced: asset_url is not defined');
+    }
     const [hoveredOption, setHoveredOption] = useState(null);

Then use:

-src={(config?.asset_url || window.wpuf_ud_free?.asset_url || '') + '/images/pro-badge.svg'}
+src={assetUrl + '/images/pro-badge.svg'}

124-164: Consider consistent array structure across all option arrays.

The socialProfiles and profileBases arrays lack the isFree property that sortOptions, avatarSizes, and gallerySizes have. While the code handles this correctly, maintaining a consistent structure would improve maintainability and prevent potential bugs if the code is refactored to iterate over these arrays generically.

Additionally, these arrays are recreated on every render. Consider moving them outside the component definition.


217-229: Ensure consistency between tooltip and placeholder text.

The tooltip on Line 217 states "Enter -1 or leave empty to show all users", while the placeholder on Line 229 only mentions "Enter -1 for all users". This inconsistency might confuse users about whether leaving the field empty is a valid option.

Consider updating the placeholder to match the tooltip:

🔎 Proposed fix
-placeholder={__('Enter -1 for all users', 'wp-user-frontend')}
+placeholder={__('Enter -1 or leave empty for all users', 'wp-user-frontend')}

241-282: Consider extracting the Pro-gated button pattern into a reusable component.

The pattern of hover state management, Pro badge overlay, and disabled styling for Pro features is repeated across multiple sections (sort options, gallery sizes, avatar sizes). This duplication increases maintenance burden.

Consider creating a reusable ProGatedButton or ProGatedOption component that encapsulates this logic.

Example structure:

const ProGatedButton = ({ 
  value, 
  label, 
  isFree, 
  isSelected, 
  onSelect,
  variant = 'default' // 'default' or 'card'
}) => {
  const [isHovered, setIsHovered] = useState(false);
  // ... shared logic
};

This would reduce code duplication and make the component easier to maintain.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3c8911 and 2f23a67.

📒 Files selected for processing (7)
  • assets/js/ud-search-shortcode.js
  • modules/user-directory/Api/Directory.php
  • modules/user-directory/Shortcode.php
  • modules/user-directory/User_Directory.php
  • src/js/user-directory/components/DirectoryWizard.js
  • src/js/user-directory/components/steps/StepAdvanced.js
  • src/js/user-directory/components/steps/StepTabs.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/js/user-directory/components/DirectoryWizard.js
  • modules/user-directory/User_Directory.php
🧰 Additional context used
🧬 Code graph analysis (3)
src/js/user-directory/components/steps/StepTabs.js (2)
src/js/user-directory/App.js (1)
  • config (36-36)
assets/js/wpuf-user-directory-free.js (1)
  • e (2-2)
assets/js/ud-search-shortcode.js (1)
src/js/user-directory/index.js (1)
  • container (17-17)
modules/user-directory/Api/Directory.php (2)
modules/user-directory/User_Directory.php (3)
  • User_Directory (24-317)
  • has_reached_limit (206-214)
  • get_default_settings (286-316)
wpuf-functions.php (1)
  • wpuf_is_pro_active (5045-5047)
🪛 PHPMD (2.15.0)
modules/user-directory/Shortcode.php

498-510: Avoid unused private methods such as 'get_current_page'. (undefined)

(UnusedPrivateMethod)

modules/user-directory/Api/Directory.php

131-131: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)


526-526: Avoid unused local variables such as '$user'. (undefined)

(UnusedLocalVariable)


528-528: Avoid unused local variables such as '$all_data'. (undefined)

(UnusedLocalVariable)


547-547: Avoid unused local variables such as '$pagination'. (undefined)

(UnusedLocalVariable)


563-563: Avoid unused local variables such as '$layout'. (undefined)

(UnusedLocalVariable)

🔇 Additional comments (1)
src/js/user-directory/components/steps/StepAdvanced.js (1)

1-581: Overall assessment: Well-implemented Pro/free gating UI with minor optimization opportunities.

This component successfully implements a comprehensive Advanced Control step with clear Pro/free feature differentiation. The hover-based Pro badge indicators provide good visual feedback, and the feature gating is consistently applied throughout.

Key strengths:

  • Consistent Pro/free gating logic across all features
  • Good use of visual indicators (hover states, Pro badges, disabled styles)
  • Comprehensive form field coverage
  • Proper accessibility considerations (sr-only classes, aria-friendly markup)

Areas for improvement (addressed in specific comments):

  • Performance optimizations (style objects, array definitions)
  • Code reusability (extracted components for repeated patterns)
  • Minor UX enhancements (feedback on Pro feature clicks)
  • Consistency checks (tooltip/placeholder text, case handling)

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: 6

🤖 Fix all issues with AI agents
In `@modules/user-directory/Shortcode.php`:
- Around line 286-333: The per_page computed by $per_page = absint(
$settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) ) can be 0,
causing a division-by-zero in the max_pages calculation and producing a
WP_User_Query with 'number' => 0; add a guard right after computing $per_page to
coerce any 0 value to a sane minimum (e.g. 1) and ensure $args['number'] uses
that sanitized $per_page before running new \WP_User_Query and calculating
$max_pages.
- Around line 413-420: In render_profile, stop reading the non-existent
$settings['default_tabs'] and instead read $settings['profile_tabs'] (keep the
same array/string handling: accept array or CSV string and normalize via
array_map/explode/trim) so the configured profile tabs are used; and in
User_Directory::get_default_settings() change the default tab identifier from
'files' to 'file' (so the default tabs and JS/template checks like
in_array('file', $default_tabs) and pagination config remain consistent).

In `@src/js/user-directory/App.js`:
- Around line 56-84: fetchDirectories currently calls response.json() without
checking response.ok; add a guard that verifies response.ok before parsing to
avoid processing error responses. Inside fetchDirectories, after awaiting
fetch(url,...), check if (!response.ok) then log the response status/statusText,
setDirectories([]) and setTotalPages(1) (and return or skip parsing); only call
response.json() and process data when response.ok is true. Keep references to
fetchDirectories, setDirectories, setTotalPages and the response handling so the
behavior mirrors the delete handler's response.ok check.

In `@src/js/user-directory/components/common/SingleSelect.js`:
- Around line 103-135: The grouped rendering currently outputs option items as
<div> inside a <ul>/<li> list which is invalid HTML; in the SingleSelect
component change the inner option render (inside
Object.entries(groupedOptions).map -> groupOptions.map) to render each option as
an <li> (preserve the key using option.value), keep the same className, onClick
handler (handleSelect(option)), selection logic (isSelected/isFree) and the
ProBadge placement, and ensure any existing structure/content (span for label
and ProBadge) is moved inside that <li> so semantics match the flat rendering
path and nothing else in the component (value, handleSelect, ProBadge) needs to
change.

In `@src/js/user-directory/components/steps/StepAdvanced.js`:
- Around line 146-154: Update the mistaken avatar size value 265 to 256 wherever
it appears: change the element in the avatarSizes array (the object with value
'265' and label '265×265') to use '256' and '256×256', and make the equivalent
update in the avatarSizeHelper module (replace any '265' occurrences used for
available sizes or labels). Ensure both the avatarSizes constant in
StepAdvanced.js and the corresponding size list/logic in avatarSizeHelper.js
consistently use '256' so UI labels and value strings match.

In `@src/js/user-directory/components/steps/StepTabs.js`:
- Around line 80-84: The forEach callback in StepTabs.js is implicitly returning
the result of orderedTabs.push(tab) (because of the arrow shorthand); change the
callback to use a block body or use an explicit loop so it does not return a
value — e.g., replace Object.values(tabsMap).forEach(tab =>
orderedTabs.push(tab)); with a non-returning form (for example:
Object.values(tabsMap).forEach(tab => { orderedTabs.push(tab); }); or a for...of
loop) in the function that builds orderedTabs to satisfy the static analysis
rule.
🧹 Nitpick comments (10)
modules/user-directory/Shortcode.php (2)

449-449: Self-referencing snapshot in $profile_data.

$profile_data['template_data'] = $profile_data copies the full array into itself for "Pro template compatibility." Because PHP arrays are copy-on-write this won't loop, but it roughly doubles the memory footprint of the data bag for every profile render. Consider assigning only the subset the template actually needs, or using a reference if the template truly expects the full bag.


468-489: extract() usage is acceptable here but worth noting.

extract($data) can silently overwrite local variables (including $template_file and $template from the outer scope). Since the template data keys are internally controlled, this is low-risk, but a collision with $template_file or $data would cause subtle bugs.

If you ever expose filter hooks that let third parties inject arbitrary keys into $data, consider using EXTR_SKIP to prevent overwrites:

-        extract( $data );
+        extract( $data, EXTR_SKIP );
src/js/user-directory/components/steps/StepProfile.js (3)

103-123: ProBadge is duplicated across multiple files — consider extracting a shared component.

This file constructs its own inline Pro badge markup (lines 106-118). The same pattern appears in App.js (line 17), DirectoryList.js (line 12), and StepTabs.js (line 5), each with slight variations in props and asset URL resolution logic.

A single shared ProBadge component (e.g., in components/common/ProBadge.js) accepting utm, width, height props would eliminate the duplication and ensure consistent upgrade URL construction across all files.


4-4: Unused config prop.

config is destructured from props but never referenced in the component body. Either remove it from the signature or use it (e.g., for the asset URL resolution on line 114 instead of accessing window globals directly).


56-56: Fixed-width grid items may overflow on narrow viewports.

Each label has width: '255px' (line 69) in a 3-column grid. On viewports below ~800px, this will overflow horizontally. If the admin UI is expected to render in narrower containers (e.g., WordPress admin on tablets), consider using responsive widths or max-width instead.

src/js/user-directory/components/common/SingleSelect.js (1)

66-163: Missing keyboard navigation for accessibility.

The dropdown lacks keyboard handlers — no support for Escape to close, Enter/Space to select, or ArrowUp/ArrowDown to navigate options. Keyboard-only users cannot interact with this control. Consider adding onKeyDown on the dropdown list items and the trigger button, and managing focus with tabIndex and aria-activedescendant.

src/js/user-directory/components/steps/StepTabs.js (1)

398-398: Fixed width 590px on content type preview may overflow narrow containers.

The content type buttons container has a hardcoded width: '590px'. Consider using max-width instead, or a responsive class, to avoid horizontal overflow in narrower admin contexts.

Proposed fix
-                <div className="wpuf-mt-4 wpuf-p-4 wpuf-bg-white wpuf-border wpuf-border-gray-200 wpuf-rounded-lg" style={{ width: '590px' }}>
+                <div className="wpuf-mt-4 wpuf-p-4 wpuf-bg-white wpuf-border wpuf-border-gray-200 wpuf-rounded-lg" style={{ maxWidth: '590px', width: '100%' }}>
src/js/user-directory/App.js (2)

52-54: fetchDirectories is a missing dependency in the useEffect hook.

React's rules of hooks require all referenced functions/values to be listed in the dependency array. fetchDirectories is called inside the effect but not listed. This won't cause a bug here since fetchDirectories receives currentPage as an argument, but it will trigger ESLint's react-hooks/exhaustive-deps warning. Wrapping fetchDirectories in useCallback or inlining the fetch in the effect would silence it.


111-139: Error handling uses alert() — consider a toast notification.

The PR summary mentions toast support in the admin UI. Using alert() on lines 123, 132, and 135 for error feedback blocks the UI and is inconsistent with the rest of the admin experience. Consider using the toast system if available.

src/js/user-directory/components/steps/StepAdvanced.js (1)

23-38: parseInt called without explicit radix.

Line 29 uses parseInt(value) without a radix argument. While modern JS defaults to base 10 for decimal strings, passing the radix explicitly (parseInt(value, 10)) is a best practice to avoid edge cases and satisfies common lint rules.

Proposed fix
-                numericValue = parseInt(value);
+                numericValue = parseInt(value, 10);

Comment on lines +413 to +420
private function render_profile( $user, $settings, $directory_id ) {
// Get default tabs from settings or use Free version defaults
$default_tabs = [ 'about', 'posts', 'comments', 'file' ];
if ( ! empty( $settings['default_tabs'] ) && is_array( $settings['default_tabs'] ) ) {
$default_tabs = $settings['default_tabs'];
} elseif ( ! empty( $settings['default_tabs'] ) && is_string( $settings['default_tabs'] ) ) {
$default_tabs = array_map( 'trim', explode( ',', $settings['default_tabs'] ) );
}
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:

# First, locate the User_Directory class and find get_default_settings
find . -type f -name "*.php" | xargs grep -l "class User_Directory" | head -5

Repository: weDevsOfficial/wp-user-frontend

Length of output: 118


🏁 Script executed:

# Search for get_default_settings method to see what keys are actually defined
rg -n "get_default_settings|default_tabs|profile_tabs" --type=php -A 5 -B 2 | head -100

Repository: weDevsOfficial/wp-user-frontend

Length of output: 9330


🏁 Script executed:

# Check the user-directory module structure
find . -type f -name "*.php" -path "*/user-directory/*" | head -20

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1183


🏁 Script executed:

# Search for where these settings are used in templates or rendering
rg -n "'file'|'files'" --type=php -g "*user-directory*" -B 2 -A 2

Repository: weDevsOfficial/wp-user-frontend

Length of output: 686


🏁 Script executed:

# Search for all tab identifier references in templates
rg -n "in_array.*\$default_tabs|'about'|'posts'|'comments'|'activity'|'message'" modules/user-directory/views/profile/ --type=php -B 1 -A 1

Repository: weDevsOfficial/wp-user-frontend

Length of output: 5218


🏁 Script executed:

# Check if there are other layout files that might use different tab identifiers
fd "layout" modules/user-directory/views/profile/ --type f

Repository: weDevsOfficial/wp-user-frontend

Length of output: 124


🏁 Script executed:

# Check the Shortcode.php more carefully to understand the full context
sed -n '410,425p' modules/user-directory/Shortcode.php

Repository: weDevsOfficial/wp-user-frontend

Length of output: 835


🏁 Script executed:

# Check Helpers.php and other files for any tab identifier references
rg -n "profile_tabs|default_tabs" modules/user-directory/ --type=php -B 2 -A 2 | head -60

Repository: weDevsOfficial/wp-user-frontend

Length of output: 5729


🏁 Script executed:

# Check if there are settings stored/retrieved elsewhere
rg -n "get_option.*directory|update_option.*directory" modules/user-directory/ --type=php

Repository: weDevsOfficial/wp-user-frontend

Length of output: 57


Fix settings key mismatch and correct tab identifier inconsistency.

Two issues here, but the proposed fix will break the Files tab:

  1. Wrong settings key: The code reads $settings['default_tabs'] (lines 416-419), but User_Directory::get_default_settings() defines the key as profile_tabs (line 303). Since default_tabs is never populated by the API, the hardcoded fallback on line 415 is always used.

  2. Critical flaw in proposed fix: The default settings define 'files' (plural) on line 303, but the template checks for 'file' (singular) on line 265, and the JavaScript pagination config uses 'file' on line 334. The proposed diff changes the hardcoded fallback to 'files', which would cause the template's in_array( 'file', $default_tabs ) check to fail, breaking the Files tab entirely.

Correct fix: Change the settings key from 'default_tabs' to 'profile_tabs', AND change the default value in User_Directory::get_default_settings() from 'files' to 'file' to match the template and JavaScript expectations.

Corrected fix
-        $default_tabs = [ 'about', 'posts', 'comments', 'file' ];
-        if ( ! empty( $settings['default_tabs'] ) && is_array( $settings['default_tabs'] ) ) {
-            $default_tabs = $settings['default_tabs'];
-        } elseif ( ! empty( $settings['default_tabs'] ) && is_string( $settings['default_tabs'] ) ) {
-            $default_tabs = array_map( 'trim', explode( ',', $settings['default_tabs'] ) );
+        $default_tabs = [ 'about', 'posts', 'comments', 'file' ];
+        if ( ! empty( $settings['profile_tabs'] ) && is_array( $settings['profile_tabs'] ) ) {
+            $default_tabs = $settings['profile_tabs'];
+        } elseif ( ! empty( $settings['profile_tabs'] ) && is_string( $settings['profile_tabs'] ) ) {
+            $default_tabs = array_map( 'trim', explode( ',', $settings['profile_tabs'] ) );
         }

Additionally, update User_Directory.php line 303:

-            'profile_tabs'       => [ 'about', 'posts', 'files', 'comments' ],
+            'profile_tabs'       => [ 'about', 'posts', 'file', 'comments' ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private function render_profile( $user, $settings, $directory_id ) {
// Get default tabs from settings or use Free version defaults
$default_tabs = [ 'about', 'posts', 'comments', 'file' ];
if ( ! empty( $settings['default_tabs'] ) && is_array( $settings['default_tabs'] ) ) {
$default_tabs = $settings['default_tabs'];
} elseif ( ! empty( $settings['default_tabs'] ) && is_string( $settings['default_tabs'] ) ) {
$default_tabs = array_map( 'trim', explode( ',', $settings['default_tabs'] ) );
}
private function render_profile( $user, $settings, $directory_id ) {
// Get default tabs from settings or use Free version defaults
$default_tabs = [ 'about', 'posts', 'comments', 'file' ];
if ( ! empty( $settings['profile_tabs'] ) && is_array( $settings['profile_tabs'] ) ) {
$default_tabs = $settings['profile_tabs'];
} elseif ( ! empty( $settings['profile_tabs'] ) && is_string( $settings['profile_tabs'] ) ) {
$default_tabs = array_map( 'trim', explode( ',', $settings['profile_tabs'] ) );
}
🤖 Prompt for AI Agents
In `@modules/user-directory/Shortcode.php` around lines 413 - 420, In
render_profile, stop reading the non-existent $settings['default_tabs'] and
instead read $settings['profile_tabs'] (keep the same array/string handling:
accept array or CSV string and normalize via array_map/explode/trim) so the
configured profile tabs are used; and in User_Directory::get_default_settings()
change the default tab identifier from 'files' to 'file' (so the default tabs
and JS/template checks like in_array('file', $default_tabs) and pagination
config remain consistent).

Comment on lines +56 to +84
const fetchDirectories = async (page = 1) => {
setLoading(true);
try {
const restUrl = config.rest_url || '/wp-json/';
const url = `${restUrl}wpuf/v1/user_directory?page=${page}&per_page=${perPage}`;
const response = await fetch(url, {
headers: {
'X-WP-Nonce': config.rest_nonce || '',
},
});
const data = await response.json();
if (data && data.success && Array.isArray(data.result)) {
setDirectories(data.result);
if (data.pagination && data.pagination.total_pages) {
setTotalPages(data.pagination.total_pages);
} else {
setTotalPages(1);
}
} else {
setDirectories([]);
setTotalPages(1);
}
} catch (error) {
console.error('Error fetching directories:', error);
setDirectories([]);
setTotalPages(1);
}
setLoading(false);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing response.ok check before parsing JSON in fetchDirectories.

On line 66, response.json() is called unconditionally. If the server returns a non-2xx status (e.g., 403, 500), this may parse an error response or throw. The delete handler (line 122) correctly checks response.ok first — apply the same pattern here.

Proposed fix
             const response = await fetch(url, {
                 headers: {
                     'X-WP-Nonce': config.rest_nonce || '',
                 },
             });
+            if (!response.ok) {
+                throw new Error(`Server error: ${response.status}`);
+            }
             const data = await response.json();
🤖 Prompt for AI Agents
In `@src/js/user-directory/App.js` around lines 56 - 84, fetchDirectories
currently calls response.json() without checking response.ok; add a guard that
verifies response.ok before parsing to avoid processing error responses. Inside
fetchDirectories, after awaiting fetch(url,...), check if (!response.ok) then
log the response status/statusText, setDirectories([]) and setTotalPages(1) (and
return or skip parsing); only call response.json() and process data when
response.ok is true. Keep references to fetchDirectories, setDirectories,
setTotalPages and the response handling so the behavior mirrors the delete
handler's response.ok check.

Comment on lines +146 to +154
const avatarSizes = [
{ value: '32', label: '32×32', isFree: false },
{ value: '48', label: '48×48', isFree: false },
{ value: '80', label: '80×80', isFree: false },
{ value: '128', label: '128×128', isFree: true },
{ value: '160', label: '160×160', isFree: false },
{ value: '192', label: '192×192', isFree: false },
{ value: '265', label: '265×265', isFree: false },
];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for avatar size 265 references
echo "=== Searching for 265 avatar size references ==="
rg '265' -t js -t php -B 2 -A 2 | head -50

echo ""
echo "=== Checking for Pro/Premium avatar sizes ==="
rg -i 'avatar.*size|size.*avatar' -t js -t php -B 2 -A 2 | grep -E '256|265|192|160' | head -40

echo ""
echo "=== Searching for avatar size definitions ==="
rg 'avatarSizes|avatar.*sizes' -t js -B 1 -A 10 | head -60

Repository: weDevsOfficial/wp-user-frontend

Length of output: 50389


🏁 Script executed:

#!/bin/bash
# Search more specifically for avatar size definitions and Pro version context
echo "=== Searching for 256 (power of 2) vs 265 ==="
rg '256|265' -t js --max-count=20

echo ""
echo "=== Full context of avatarSizeHelper.js ==="
cat src/js/user-directory/utils/avatarSizeHelper.js 2>/dev/null || echo "File not found"

echo ""
echo "=== Full context of avatar sizes in StepAdvanced.js (lines 140-160) ==="
sed -n '140,160p' src/js/user-directory/components/steps/StepAdvanced.js 2>/dev/null || echo "File not found"

echo ""
echo "=== Search for Pro or Premium references with 265 ==="
rg -i 'pro|premium|paid' -B 5 -A 5 '265' -t js | head -40

Repository: weDevsOfficial/wp-user-frontend

Length of output: 50450


🏁 Script executed:

#!/bin/bash
# Read the full avatarSizeHelper.js file
echo "=== Full avatarSizeHelper.js ==="
cat src/js/user-directory/utils/avatarSizeHelper.js 2>/dev/null

echo ""
echo "=== Context around StepAdvanced.js line 146-154 ==="
sed -n '140,160p' src/js/user-directory/components/steps/StepAdvanced.js 2>/dev/null

echo ""
echo "=== Search for any comments or documentation about avatar sizes ==="
rg -i 'avatar|size' -B 2 -A 2 src/js/user-directory/utils/avatarSizeHelper.js src/js/user-directory/components/steps/StepAdvanced.js 2>/dev/null | head -80

Repository: weDevsOfficial/wp-user-frontend

Length of output: 9273


Change avatar size 265 to 256 — this appears to be a typo. The value 265 lacks justification and is not a standard image dimension, while 256 (2⁸) follows the expected power-of-two pattern for sizes like 32, 64, 128, etc. Update both occurrences in src/js/user-directory/components/steps/StepAdvanced.js and src/js/user-directory/utils/avatarSizeHelper.js.

🤖 Prompt for AI Agents
In `@src/js/user-directory/components/steps/StepAdvanced.js` around lines 146 -
154, Update the mistaken avatar size value 265 to 256 wherever it appears:
change the element in the avatarSizes array (the object with value '265' and
label '265×265') to use '256' and '256×256', and make the equivalent update in
the avatarSizeHelper module (replace any '265' occurrences used for available
sizes or labels). Ensure both the avatarSizes constant in StepAdvanced.js and
the corresponding size list/logic in avatarSizeHelper.js consistently use '256'
so UI labels and value strings match.

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.
@Rubaiyat-E-Mohammad
Copy link
Contributor

image
  • Fonts should be bold according to the design feedback. @arifulhoque7 vai
image

@Rubaiyat-E-Mohammad Rubaiyat-E-Mohammad added bug and removed QA Approved This PR is approved by the QA team labels Feb 9, 2026
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.
@arifulhoque7
Copy link
Contributor Author

done @Rubaiyat-E-Mohammad vai

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: 8

🤖 Fix all issues with AI agents
In `@modules/user-directory/tailwind.config.js`:
- Around line 9-13: The Tailwind config's content array currently only scans
'./views/**/*.php' so utility classes in Admin_Menu.php (e.g., wpuf-h-100vh,
wpuf-bg-white, wpuf-ml-[-20px], !wpuf-py-0, wpuf-px-[20px]) are purged; fix by
either adding './Admin_Menu.php' to the content array in
modules/user-directory/tailwind.config.js or, if you prefer to keep content
globs, add those exact class names (and any variants like the !important prefix)
to the safelist in the same file so Admin_Menu.php styles are preserved in
production.
- Around line 1-7: The config mixes ESM import with CommonJS export; replace the
ES import of scopedPreflightStyles and isolateInsideOfContainer with CommonJS
requires so the file uses a single module system: change the top-level "import {
scopedPreflightStyles, isolateInsideOfContainer } from
'tailwindcss-scoped-preflight'" to equivalent const require(...) assignments and
keep the existing module.exports and plugins usage (referencing
scopedPreflightStyles and isolateInsideOfContainer) so all symbols are loaded
via require().

In `@src/js/user-directory/components/DirectoryWizard.js`:
- Around line 131-134: The error handling for failed fetch responses in
DirectoryWizard.js currently calls response.json() directly which can throw on
non-JSON responses; wrap the call to response.json() in a try/catch (inside the
existing if (!response.ok) branch), and if parsing fails use a fallback message
such as response.statusText or a localized generic string before throwing;
update the thrown Error to include either the parsed data.message (when
available) or the fallback text so the code paths in the function that calls
this fetch (the async handler using response) get a meaningful error instead of
a JSON parse exception.
- Around line 86-99: handleNextStep captures currentStepIndex in the timeout
closure which can be stale; change the state update to a functional updater and
set prevStepIndex.current inside that updater so the latest value is used:
inside the setTimeout replace setCurrentStepIndex(currentStepIndex + 1) and
prevStepIndex.current = currentStepIndex with a single setCurrentStepIndex(prev
=> { prevStepIndex.current = prev; return prev + 1; }); apply the same pattern
(functional updater or a ref-read) to handlePrevStep and navigateToStep to avoid
stale-closure bugs.
- Around line 269-273: The footer's inline style reads
document.body.classList.contains('folded') directly in DirectoryWizard's render,
which is non-reactive and causes misalignment when the WP sidebar toggles;
update the component (DirectoryWizard) to track the folded state in React state
via a custom hook or useEffect + MutationObserver (or a window/DOM event
listener) that watches document.body.classList for the 'folded' class and sets a
state like isFolded, then use that state to compute the left style instead of
direct DOM reads; ensure you clean up the observer/listener on unmount and
reference the updated isFolded state where the inline style is currently set.

In `@src/js/user-directory/components/steps/StepTabs.js`:
- Line 400: In the StepTabs component update the div that currently uses
style={{ width: '590px' }} to use responsive sizing instead (e.g., width: '100%'
plus a maxWidth of 590px or a Tailwind utility like "w-full max-w-[590px]") so
it no longer forces a fixed pixel width; locate the div in StepTabs.js (the
container with className "wpuf-mt-4 wpuf-p-4 wpuf-bg-white wpuf-border
wpuf-border-gray-200 wpuf-rounded-lg") and replace the fixed width style with a
responsive approach (maxWidth + width: '100%' or equivalent Tailwind classes).
- Around line 418-423: The overlay div that renders ProBadge when hovered (check
the conditional rendering tied to hoveredContentType in StepTabs.js) uses
style={{ pointerEvents: 'none' }}, which prevents the anchor inside ProBadge
from receiving clicks; remove pointerEvents: 'none' from these overlay divs (or
set pointerEvents: 'auto' on the ProBadge container) so the link inside ProBadge
is clickable — update every instance where the overlay is rendered for the five
content-type buttons adjacent to ProBadge to ensure the badge's internal <a> can
receive pointer events.
- Around line 342-352: In StepTabs (component StepTabs.js) the heading element
rendering "Configure About Tab" is currently an <h1> which breaks the document
outline after an earlier <h2> ("Profile Content"); replace the <h1> used for the
"Configure About Tab" heading with a semantically correct lower-level heading
(e.g., <h3>, or at minimum <h2>) so the heading hierarchy is preserved and
accessibility/SEO are corrected; update the JSX where the string __('Configure
About Tab', 'wp-user-frontend') is rendered to use the chosen heading tag.
🧹 Nitpick comments (7)
src/js/user-directory/components/steps/StepTabs.js (2)

401-521: Extract a reusable component for the five nearly identical content-type buttons.

Each content-type button (Meta Field, Section, Post Type, Comment, Image/File) repeats the same markup, classes, hover logic, and overlay pattern — differing only in icon SVG, label, and UTM string. This is a significant DRY violation that makes future style changes error-prone.

Suggested extraction
const ContentTypeButton = ({ icon, label, typeKey, utm, hoveredContentType, setHoveredContentType }) => (
    <button
        type="button"
        className="wpuf-relative wpuf-flex wpuf-flex-col wpuf-items-center wpuf-justify-center wpuf-rounded-lg wpuf-text-sm wpuf-font-medium wpuf-w-[102px] wpuf-gap-1 wpuf-p-3 wpuf-pb-1 wpuf-bg-gray-50 wpuf-text-gray-400 wpuf-cursor-not-allowed wpuf-opacity-60 hover:wpuf-opacity-80"
        style={{ border: '1px solid `#E5E7EB`' }}
        onMouseEnter={() => setHoveredContentType(typeKey)}
        onMouseLeave={() => setHoveredContentType(null)}
    >
        {icon}
        <span className="wpuf-text-sm wpuf-font-normal wpuf-leading-5 wpuf-text-gray-400">
            {label}
        </span>
        {hoveredContentType === typeKey && (
            <div className="wpuf-absolute wpuf-inset-0 wpuf-flex wpuf-items-center wpuf-justify-center wpuf-bg-white/90 wpuf-rounded-lg">
                <ProBadge utm={utm} />
            </div>
        )}
    </button>
);

Then replace each button with a <ContentTypeButton ... /> call.


5-23: ProBadge is duplicated across at least four files with minor variations.

Identical or near-identical ProBadge components exist in App.js, StepAdvanced.js, DirectoryList.js, and now here. Consider extracting a single shared ProBadge component under components/common/ to avoid drift between copies.

src/js/user-directory/components/DirectoryWizard.js (3)

77-110: Duplicated fade-transition logic across three navigation functions.

navigateToStep, handleNextStep, and handlePrevStep all repeat the same setFade(false) → setTimeout → update prevStepIndex → setCurrentStepIndex → setFade(true) pattern. Extract a shared helper to reduce duplication.

Example helper
+const transitionToStep = (getNextIndex) => {
+    setFade(false);
+    setTimeout(() => {
+        setCurrentStepIndex((prev) => {
+            prevStepIndex.current = prev;
+            return getNextIndex(prev);
+        });
+        setFade(true);
+    }, 200);
+};
+
 const navigateToStep = (stepIndex) => {
-    setFade(false);
-    setTimeout(() => {
-        prevStepIndex.current = currentStepIndex;
-        setCurrentStepIndex(stepIndex);
-        setFade(true);
-    }, 200);
+    transitionToStep(() => stepIndex);
 };

148-150: handleSaveAndExit is a redundant wrapper around handleSubmit.

It adds no logic beyond awaiting handleSubmit. You can pass handleSubmit directly to the button's onClick at line 185.


196-254: Step circles always render a checkmark SVG, even for future (incomplete) steps.

The white checkmark is only hidden by the green background not being applied for future steps, but on a white circle with white stroke it's invisible. This works visually but is semantically misleading for screen readers and fragile if styles change. Consider conditionally rendering the checkmark only for completed steps, or adding aria-current="step" for the active step for better accessibility.

modules/user-directory/tailwind.config.js (2)

14-84: Duplicate safelist entries and potentially ineffective custom class names.

Two issues with the safelist:

  1. Duplicateshover:!wpuf-bg-purple-700 (lines 42 & 62) and hover:!wpuf-text-purple-600 (lines 41 & 65) are listed twice.

  2. wpuf-btn-white / wpuf-btn-primary (lines 76–77) — These are not standard Tailwind utilities, so safelisting them has no effect unless they are defined as custom component classes via @layer components in CSS. If they are already defined in CSS, they don't need to be safelisted. If they're expected to be generated by Tailwind, they won't be.

🧹 Remove duplicates
         'hover:wpuf-text-purple-600',
         'hover:wpuf-bg-purple-700',
         'hover:wpuf-border-purple-600',
         'focus:wpuf-ring-purple-500',
-        'hover:!wpuf-text-purple-600',
-        'hover:!wpuf-bg-purple-700',
         'hover:!wpuf-border-purple-600',
         'focus:!wpuf-ring-purple-500',

And verify whether wpuf-btn-white / wpuf-btn-primary actually need to be in the safelist.


85-92: Consider kebab-case for custom color keys.

primaryHover generates classes like wpuf-bg-primaryHover. Tailwind convention is kebab-case (primary-hoverwpuf-bg-primary-hover), which reads more naturally alongside other Tailwind utilities.

✏️ Suggested rename
         colors: {
             primary: '#059669',
-            primaryHover: '#10b981',
+            'primary-hover': '#10b981',
         },

Comment on lines +1 to +7
/** @type {import('tailwindcss').Config} */
import {
scopedPreflightStyles,
isolateInsideOfContainer,
} from 'tailwindcss-scoped-preflight';

module.exports = {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check package.json for "type": "module" setting
echo "=== Checking package.json type setting ==="
if [ -f package.json ]; then
  jq '.type // "not set"' package.json
else
  echo "package.json not found"
fi

echo ""
echo "=== Looking for other tailwind configs ==="
fd 'tailwind.config' -t f 2>/dev/null || echo "No tailwind.config files found"

echo ""
echo "=== Looking for other postcss/webpack configs ==="
fd -t f '(postcss|webpack|next\.config)' 2>/dev/null | head -20

echo ""
echo "=== Checking the actual file content (first 20 lines) ==="
cat -n modules/user-directory/tailwind.config.js | head -20

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1191


🏁 Script executed:

echo "=== Root tailwind.config.js (first 20 lines) ==="
head -20 tailwind.config.js

echo ""
echo "=== modules/user-directory/postcss.config.js ==="
cat modules/user-directory/postcss.config.js

echo ""
echo "=== modules/user-directory/webpack.config.js (first 30 lines) ==="
head -30 modules/user-directory/webpack.config.js

echo ""
echo "=== Root postcss.config.js ==="
cat postcss.config.js

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1955


🏁 Script executed:

echo "=== Checking for babel config ==="
ls -la | grep -E 'babel|\.babelrc'
find . -maxdepth 2 -name '.babelrc*' -o -name 'babel.config.*' 2>/dev/null

echo ""
echo "=== Checking webpack babel-loader config (from earlier output) ==="
sed -n '20,50p' modules/user-directory/webpack.config.js

echo ""
echo "=== Checking package.json scripts ==="
jq '.scripts' package.json

echo ""
echo "=== Checking how Tailwind is invoked ==="
grep -r 'tailwindcss' package.json 2>/dev/null || echo "Not in package.json"
</parameter>

Repository: weDevsOfficial/wp-user-frontend

Length of output: 2176


🏁 Script executed:

echo "=== Looking for Gruntfile ==="
find . -maxdepth 2 -name 'Gruntfile*' -o -name 'grunt.config.*' 2>/dev/null

echo ""
echo "=== Checking Node version requirements ==="
cat package.json | jq '.engines // "not set"'

echo ""
echo "=== Checking if there's a .npmrc or other config ==="
cat .npmrc 2>/dev/null || echo "No .npmrc"

echo ""
echo "=== Looking for any node config files ==="
ls -la | grep -E 'node|npm'

Repository: weDevsOfficial/wp-user-frontend

Length of output: 280


🏁 Script executed:

# Find the Gruntfile
if [ -f Gruntfile.js ]; then
  echo "=== Gruntfile.js found, checking tailwind task ==="
  grep -A 30 "tailwind" Gruntfile.js | head -50
fi

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1809


🏁 Script executed:

echo "=== Searching for tailwind task definition in Gruntfile ==="
grep -n "grunt.registerTask\|tailwind:" Gruntfile.js | head -20

echo ""
echo "=== Searching for 'tailwind' task configuration ==="
sed -n '/tailwind.*:{/,/^[[:space:]]*}/p' Gruntfile.js | head -100

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1098


🏁 Script executed:

echo "=== Detailed tailwind task implementation (lines 298-365) ==="
sed -n '298,365p' Gruntfile.js

echo ""
echo "=== Looking for shell:tailwind commands ==="
sed -n '/shell.*:/,/^[[:space:]]*}/p' Gruntfile.js | grep -A 5 "tailwind"

Repository: weDevsOfficial/wp-user-frontend

Length of output: 3140


🏁 Script executed:

echo "=== Checking for Node version management files ==="
ls -la | grep -E 'nvmrc|node-version|\.tool-versions'

echo ""
echo "=== Checking if modules/user-directory has its own package.json ==="
if [ -f modules/user-directory/package.json ]; then
  echo "Found! Checking type field:"
  jq '.type // "not set"' modules/user-directory/package.json
  echo ""
  echo "Full package.json:"
  cat modules/user-directory/package.json
fi

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1361


Use consistent module syntax throughout the config file.

Lines 2–5 use ES module import, but line 7 uses module.exports. While the root tailwind.config.js uses the same mixed pattern, consolidating to require() throughout maintains consistency and clarity. Convert the import statement to const ... = require(...) to match the CommonJS pattern already used in module.exports and the plugins array.

Suggested refactoring
 /** `@type` {import('tailwindcss').Config} */
-import {
-    scopedPreflightStyles,
-    isolateInsideOfContainer,
-} from 'tailwindcss-scoped-preflight';
+const {
+    scopedPreflightStyles,
+    isolateInsideOfContainer,
+} = require('tailwindcss-scoped-preflight');
 
 module.exports = {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** @type {import('tailwindcss').Config} */
import {
scopedPreflightStyles,
isolateInsideOfContainer,
} from 'tailwindcss-scoped-preflight';
module.exports = {
/** `@type` {import('tailwindcss').Config} */
const {
scopedPreflightStyles,
isolateInsideOfContainer,
} = require('tailwindcss-scoped-preflight');
module.exports = {
🤖 Prompt for AI Agents
In `@modules/user-directory/tailwind.config.js` around lines 1 - 7, The config
mixes ESM import with CommonJS export; replace the ES import of
scopedPreflightStyles and isolateInsideOfContainer with CommonJS requires so the
file uses a single module system: change the top-level "import {
scopedPreflightStyles, isolateInsideOfContainer } from
'tailwindcss-scoped-preflight'" to equivalent const require(...) assignments and
keep the existing module.exports and plugins usage (referencing
scopedPreflightStyles and isolateInsideOfContainer) so all symbols are loaded
via require().

@Rubaiyat-E-Mohammad Rubaiyat-E-Mohammad added QA Approved This PR is approved by the QA team and removed needs: testing needs: dev review This PR needs review by a developer labels Feb 15, 2026
Copy link
Member

@sapayth sapayth left a comment

Choose a reason for hiding this comment

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

Check the comments/suggestions from coderabbitai. also:

  1. SQL Injection via unsanitized orderby in REST API
    modules/user-directory/Api/Directory.php — The orderby parameter from user input is interpolated directly into SQL:
    $orderby_clause = "ORDER BY {$orderby} {$order}";
    The orderby value must be validated against an allowlist of column names, not just sanitized as text.

  2. Direct $_SERVER superglobal access without sanitization (5 files)
    Multiple templates construct URLs from raw $_SERVER['HTTP_HOST'] and $_SERVER['REQUEST_URI'] — these are user-controllable headers:

  • views/directory/layout-3.php:72
  • views/directory/template-parts/pagination-shortcode.php:46
  • views/profile/template-parts/posts-2.php:96
  • views/profile/template-parts/comments-2.php:88

Fix: Use sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) or better yet, use WordPress functions like home_url( add_query_arg( null, null ) ).

  1. Missing wp_unslash() on $_GET superglobals
  • views/directory/template-parts/search-field.php:17
  • views/directory/template-parts/row-3.php:135,141,147
  1. Missing output escaping — XSS vectors
  • views/profile/template-parts/posts-2.php:56 — wp_trim_words( get_the_title(), 10 ) echoed without esc_html()
  • views/profile/template-parts/posts-2.php:65 — get_permalink() without esc_url()
  • views/profile/template-parts/comments-2.php:56 — get_comment_link() without esc_url()
  • views/profile/template-parts/comments-2.php:66 — wp_trim_words( $comment->comment_content ) without esc_html()
  • views/profile/layout-2.php:90 — Raw SVG icon output without wp_kses()
  • views/directory/template-parts/row-3.php:39 — $avatar without wp_kses_post()
  1. REST API — Missing nonce verification in frontend JS
    assets/js/ud-search-shortcode.js:57-62 — Calls REST API without X-WP-Nonce header.

  2. REST API permission_callback concerns
    Verify that the directory REST endpoints in Api/Directory.php have proper permission_callback functions — public endpoints should explicitly use __return_true, and any write/admin
    endpoints must check capabilities.


HIGH Priority Issues

  1. Hardcoded REST URL in React component
    src/js/user-directory/components/DirectoryList.js:98:
    fetch(/wp-json/wpuf/v1/user_directory/user_count?...)
    This ignores config.rest_url and will break on non-standard WordPress installations (subdirectory installs, custom REST prefixes).

  2. ProBadge component defined inside another component body
    src/js/user-directory/components/steps/StepAdvanced.js:87 — Defining a React component inside another component's render function causes it to unmount/remount on every render.
    ProBadge is also duplicated across 6 files — should be a single shared component.

  3. Modal accessibility (WCAG 2.1 Level A violation)
    src/js/user-directory/components/common/DeleteConfirmModal.js:

  • No focus trap
  • No role="dialog" or aria-modal="true"
  • No aria-labelledby
  • "Cancel" and "Delete" button text not wrapped in __() for translation
  1. Pagination uses with onClick — not keyboard accessible
    src/js/user-directory/components/DirectoryList.js:291-305 — Should be elements with aria-label.

  2. useEffect dependency array violation
    src/js/user-directory/App.js:57-59 — fetchDirectories not in deps and not wrapped in useCallback. Will miss updates to config or perPage.

  3. Member count fetching — race conditions & performance
    src/js/user-directory/components/DirectoryList.js:55-115:

  • Multiple setState calls in forEach loop — should be batched
  • No AbortController cleanup for in-flight requests on unmount
  • State updates on unmounted component warnings

MEDIUM Priority Issues

  1. in_array() missing strict type comparison
    PHPCS config requires true as 3rd parameter. Found ~11 occurrences across:
  • views/directory/template-parts/social-icons.php:22-25
  • views/profile/layout-2.php:202,233,249,265,283,292
  • views/profile/template-parts/file-2.php:101
  1. Missing ABSPATH check in template files
    Several templates lack if ( ! defined( 'ABSPATH' ) ) { exit; }:
  • layout-3.php, row-3.php, pagination-shortcode.php, posts-2.php, comments-2.php
  1. Functions defined inside template files
    wpuf_ud_build_posts_page_url(), wpuf_ud_build_comments_page_url(), wpuf_ud_build_files_page_url() are defined in template files with function_exists() guards. Move these to
    Helpers.php.

  2. Inline <script> blocks in templates
    views/profile/layout-2.php has 3 inline <script> blocks. Should use wp_add_inline_script() for CSP compliance.

  3. posts_per_page => -1 for user attachments
    views/profile/template-parts/file-2.php:28-29 — Loads ALL attachments then filters in PHP. Performance risk for users with many uploads.

  4. Duplicate pagination markup (DRY violation)
    Nearly identical pagination HTML in 3 template files. Should be a shared partial.

  5. Tooltip ID uses Math.random() on every render
    src/js/user-directory/components/common/Tooltip.js:5 — Breaks aria-describedby association. Use useId() or useRef.

  6. navigator.clipboard.writeText — unhandled Promise rejection
    src/js/user-directory/components/DirectoryList.js:32 — No .catch(), shows "Copied!" optimistically.

  7. No form validation before submission
    src/js/user-directory/components/DirectoryWizard.js:445 — Can submit a directory with empty title.

  8. setTimeout without cleanup in DirectoryWizard
    Lines 411-417, 424-429, 438-441 — Will cause state updates on unmounted component.

  9. mailto: link bug
    views/directory/template-parts/row-3.php:69:

esc_url() without the mailto: prefix will prepend http://. Should be esc_url( 'mailto:' . $user_email ).
  1. SVG linearGradient ID collision
    views/directory/template-parts/social-icons.php — Instagram SVG gradient ID paint0_linear_5793_2712 will collide when multiple user rows render on the same page.

  2. Tailwind config mixes ESM import with CommonJS module.exports
    modules/user-directory/tailwind.config.js:5-8 — May fail in strict CommonJS environments.

*
* @package WPUF
* @subpackage Free/User_Directory
* @since 4.3.0
Copy link
Member

Choose a reason for hiding this comment

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

do not hardcode version number here, use WPUF_SINCE

*
* @package WPUF
* @subpackage Modules/User_Directory
* @since 4.3.0
Copy link
Member

Choose a reason for hiding this comment

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

do not hardcode version number here or other places, use WPUF_SINCE

// Add pagination if there are multiple pages
if ( $pagination['total_pages'] > 1 ) {
// Build base URL and query args for pagination
$current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
Copy link
Member

Choose a reason for hiding this comment

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

Direct $_SERVER superglobal access without sanitization. also in:

  • views/directory/layout-3.php:72
  • views/directory/template-parts/pagination-shortcode.php:46
  • views/profile/template-parts/posts-2.php:96
  • views/profile/template-parts/comments-2.php:88

@sapayth sapayth added the needs: author reply This PR needs author feedback or code changes label Mar 3, 2026
arifulhoque7 and others added 2 commits March 3, 2026 15:21
Add an external file-tabs script and register/enqueue it for profile files; remove inline tab JS. Improve security and robustness across templates: add ABSPATH checks, use esc_url/esc_html/wp_kses/wp_kses_post where appropriate, use wp_unslash for $_GET input, strict in_array checks, and replace raw $_SERVER URL construction with home_url( add_query_arg( null, null ) ).

Refactor Helpers: change free layout color palette to emerald, update avatar markup to use Tailwind classes, and add utility helpers (pagination URL builders and wpuf_ud_get_image_size_dimensions). Limit attachment queries to avoid memory issues and tighten file grouping checks. Replace duplicated pagination URL helpers in posts/comments templates with shared helpers.

Frontend JS improvements: include X-WP-Nonce header in ud-search-shortcode fetch, add new wpuf-ud-file-tabs.js, and harden React code (useCallback for fetchDirectories, AbortController for member-count fetches, improved batching/loading state for directory member counts, promise-handling fallback for clipboard API). Small doc/version placeholder updates (WPUF_SINCE). These changes aim to improve security, performance, and maintainability.
@arifulhoque7
Copy link
Contributor Author

fixed @sapayth vai

@arifulhoque7 arifulhoque7 requested a review from sapayth March 3, 2026 10:00
@arifulhoque7 arifulhoque7 added needs: dev review This PR needs review by a developer and removed needs: author reply This PR needs author feedback or code changes labels Mar 3, 2026
Copy link
Member

@sapayth sapayth left a comment

Choose a reason for hiding this comment

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

  • @since 4.3.0 hardcoded across all new files. use WPUF_SINCE placeholder.
  • flush_rewrite_rules() is an expensive function call. try to minimize it as much as possible.
  • Post_Type.php — show_in_rest => true exposes directory config via WP default REST endpoint /wp-json/wp/v2/wpuf_user_listing becomes available. Since this module has its own REST controller, set show_in_rest => false to avoid leaking directory settings to authenticated read-capability users.
  • Helpers.php — $layout parameter silently ignored in wpuf_ud_free_get_layout_colors(). The parameter is accepted but never used. Either use it or remove it — the silent ignore will confuse Pro code that passes different layouts expecting different colors.
  • modules.php — wpuf_free_deactivate_module() always returns true. Returns true even when the module was never in the active list, masking errors.
  • Free_Loader.php — Inline style attributes on new section headings. Move to the module CSS file.

}

// Check URL structure
$current_uri = trim( $_SERVER['REQUEST_URI'], '/' );
Copy link
Member

Choose a reason for hiding this comment

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

use wp_unslash() and sanitization

Comment on lines +288 to +289
// Flush rewrite rules for pretty URLs
flush_rewrite_rules();
Copy link
Member

Choose a reason for hiding this comment

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

@arifulhoque7 bhai. flush_rewrite_rules() is an expensive function call. Minimize it as much as possible

];

foreach ( $profile_params as $param ) {
if ( ! empty( $_GET[$param] ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

use wp_unslash() and sanitization

@sapayth sapayth added needs: author reply This PR needs author feedback or code changes and removed needs: dev review This PR needs review by a developer labels Mar 5, 2026
Apply a set of cleanup and safety fixes for the User Directory free module:

- Update @SInCE tags to use WPUF_SINCE across multiple files.
- Add admin CSS for module section titles, pro badge and preview container (replace inline styles with classes).
- Harden input handling by sanitizing usage of $_GET and $_SERVER in DirectoryStyles.
- Remove an unnecessary flush_rewrite_rules() call from REST directory creation and disable REST exposure for the internal wpuf_user_listing post type.
- Improve wpuf_free_deactivate_module: validate module existence, return WP_Error for invalid/not-active cases and return null on success; use wpuf_free_get_modules() for checks.
- Simplify helpers: free layout color API now always returns the emerald palette and related wrapper functions updated accordingly.
- Misc: update docblocks and minor view/template metadata to reflect the changes.

These changes improve security, clarity, and correctness of the free User Directory implementation and replace inline styling with maintainable CSS classes.
@arifulhoque7 arifulhoque7 requested a review from sapayth March 5, 2026 08:53
@arifulhoque7 arifulhoque7 added needs: dev review This PR needs review by a developer and removed needs: author reply This PR needs author feedback or code changes labels Mar 5, 2026
Multiple fixes and UI refinements for the User Directory module:

- Ensure per_page is at least 1 to avoid zero-limit pagination.
- Use profile_tabs (instead of default_tabs) in Shortcode and update default profile_tabs order to use 'file' key to match settings.
- Fix Tailwind/PostCSS config paths and include Admin_Menu.php in Tailwind content scan.
- DirectoryWizard: track WP sidebar folded state reactively (MutationObserver), switch to functional setState updates to avoid stale values, and improve fetch error handling.
- MultiSelect: import and use cloneElement instead of React.cloneElement for icon cloning.
- SingleSelect: use semantic <li> for option items.
- StepAdvanced: correct avatar size to 256 and remove pointerEvents style from ProBadge overlay.
- StepTabs: small markup and layout adjustments (heading level change, container maxWidth, minor cleanups).
- avatarSizeHelper: correct mapping for layout-6 to 256.

These changes address UX bugs, ensure consistent config keys, and improve robustness of state updates and error handling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs: dev review This PR needs review by a developer QA Approved This PR is approved by the QA team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants