diff --git a/src/vertex/app/changelog.md b/src/vertex/app/changelog.md index 9b990b726c..d87503fbea 100644 --- a/src/vertex/app/changelog.md +++ b/src/vertex/app/changelog.md @@ -4,6 +4,47 @@ --- +## Version 1.23.43 +**Released:** May 17, 2026 + +### Page Satisfaction Feedback Banner & Global Banner Positioning + +Implements a full-featured, page-satisfaction feedback banner for the Vertex platform (Issue #3480), and resolves several core layout and banner rendering issues to ensure perfect visual presentation on both desktop and mobile viewports. + +
+New Features (4) + +- **Page Satisfaction Feedback Banner**: Designed and deployed a new `PageSatisfactionBanner` component situated statically directly below the page footer for all authenticated sessions, encouraging quick, one-tap platform feedback. +- **Structured Feedback Modal Flow**: Integrated interactive positive/negative feedback modals (`FeedbackModal`) that prompt users for structured reasons and optional comments when voting. +- **Session & API Integration**: Automatically pre-fills the user's registered identity from the active user session context and forwards feedback payloads to the backend `feedbackService.submitFeedback` with rich client telemetry. +- **Dynamic Context Detection**: Automatically parses the active URL path to display the localized screen name (e.g. "Home", "Cohorts") in the satisfaction prompt. + +
+ +
+Layout & UI Bug Fixes (5) + +- **Mobile Viewport Bottom Spacing**: Fixed a 80px white space below the satisfaction banner on mobile and tablet screens. Relocated `pb-20 md:pb-0` padding classes from the outer scrolling `
` container to the inner `max-w-7xl` page content wrapper, ensuring the banner rests perfectly flush at the absolute bottom of the viewport. +- **Dialog Notification Hardening**: Hardened dialog error feedback by setting `scoped: true` on submission and authentication error banners. Error messages are now rendered beautifully inline at the top of the modal (`BannerSlot` inside `ReusableDialog`) instead of being hidden behind the backdrop. +- **Deferred Success Notification**: Deferred the global success banner schedule inside `FeedbackModal`'s `handleSubmit` via a 150ms timeout. This allows `ReusableDialog`'s unmount cleanup `hideBanner()` to complete before the persistent success banner is successfully scheduled on the main screen. +- **Overlapping Global Banners Resolved**: Moved `` from the root application tree (`providers.tsx`) to the `max-w-7xl` page content wrapper inside `
` (`layout.tsx`). The global banner now respects all layout boundaries and sidebars, avoiding any overlapping or clipping underneath the fixed `Topbar` and `Sidebar` layouts. +- **Global Banner Margins Refinement**: Removed the trailing `px-4` margin from the `GlobalBannerContainer` wrapper inside `context/banner-context.tsx` to fully leverage the parent responsive grid padding (`px-3 py-3 md:px-2 lg:py-6 lg:px-6`). + +
+ +
+Files Created/Modified (5) + +- `src/vertex/app/providers.tsx` [MODIFIED] +- `src/vertex/app/(authenticated)/layout.tsx` [MODIFIED] +- `src/vertex/components/features/feedback/page-satisfaction-banner.tsx` [NEW] +- `src/vertex/components/layout/layout.tsx` [MODIFIED] +- `src/vertex/context/banner-context.tsx` [MODIFIED] + +
+ +--- + ## Version 1.23.42 **Released:** May 17, 2026 diff --git a/src/vertex/app/providers.tsx b/src/vertex/app/providers.tsx index 3e6949f0d9..383d25a7a3 100644 --- a/src/vertex/app/providers.tsx +++ b/src/vertex/app/providers.tsx @@ -10,7 +10,7 @@ import { ThemeProvider } from "@/components/theme-provider"; import SessionLoadingState from "@/components/layout/loading/session-loading"; import { QueryProvider } from "@/core/providers/query-provider"; import { runClientCacheMaintenance } from "@/core/utils/clientCache"; -import { BannerProvider, GlobalBannerContainer } from "@/context/banner-context"; +import { BannerProvider } from "@/context/banner-context"; const NetworkStatusBanner = dynamic( () => import('@/components/features/network-status-banner'), @@ -48,7 +48,6 @@ export default function Providers({ children, session }: { children: React.React disableTransitionOnChange > - {children} diff --git a/src/vertex/components/features/feedback/page-satisfaction-banner.tsx b/src/vertex/components/features/feedback/page-satisfaction-banner.tsx new file mode 100644 index 0000000000..4b3723e585 --- /dev/null +++ b/src/vertex/components/features/feedback/page-satisfaction-banner.tsx @@ -0,0 +1,275 @@ +'use client'; + +import React, { useState } from 'react'; +import { ThumbsUp, ThumbsDown, MessageSquare } from 'lucide-react'; +import { openFeedbackDialog } from './feedback-dialog'; +import { feedbackService } from '@/core/apis/feedback'; +import { useUserContext } from '@/core/hooks/useUserContext'; +import { useBanner } from '@/context/banner-context'; +import ReusableDialog from '@/components/shared/dialog/ReusableDialog'; +import ReusableInputField from '@/components/shared/inputfield/ReusableInputField'; +import ReusableButton from '@/components/shared/button/ReusableButton'; +import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'; +import { usePathname } from 'next/navigation'; + +const NEGATIVE_REASONS = [ + 'Confusing', + 'Not helpful', + 'Missing features', + 'Missing or limited data', + 'I encountered technical issues', + 'Other', +]; + +const POSITIVE_REASONS = [ + 'Informative', + 'Actionable', + 'Makes my job easier', + 'Ease of use', + 'Other', +]; + +interface FeedbackModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (rating: number, reason: string, message: string) => Promise; + isSubmitting: boolean; + type: 'positive' | 'negative'; +} + +const FeedbackModal: React.FC = ({ + isOpen, + onClose, + onSubmit, + isSubmitting, + type, +}) => { + const [reason, setReason] = useState(''); + const [message, setMessage] = useState(''); + const { showBanner } = useBanner(); + + const reasons = type === 'positive' ? POSITIVE_REASONS : NEGATIVE_REASONS; + + const handleSubmit = async () => { + if (!reason) { + return; + } + const submitted = await onSubmit( + type === 'positive' ? 5 : 1, + reason, + message + ); + if (!submitted) return; + setReason(''); + setMessage(''); + onClose(); + setTimeout(() => { + showBanner({ + severity: 'success', + title: 'Thank you for your feedback!', + message: 'Your input helps us improve the platform.', + }); + }, 300); + }; + + return ( + { + setReason(''); + setMessage(''); + onClose(); + }} + title="Why did you choose this rating?" + size="md" + showFooter + primaryAction={{ + label: isSubmitting ? 'Sending...' : 'Submit', + onClick: handleSubmit, + disabled: isSubmitting || !reason, + }} + secondaryAction={{ + label: 'Cancel', + onClick: () => { + setReason(''); + setMessage(''); + onClose(); + }, + disabled: isSubmitting, + variant: 'outline', + }} + > +
+
+ +
+ {reasons.map((r) => ( + + ))} +
+
+ + setMessage(event.target.value)} + placeholder="Your feedback helps us improve..." + rows={5} + description="Please don't include any sensitive information" + /> +
+
+ ); +}; + +export const PageSatisfactionBanner: React.FC = () => { + const [modalType, setModalType] = useState<'positive' | 'negative' | null>(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const pathname = usePathname(); + const { userDetails } = useUserContext(); + const { showBanner } = useBanner(); + + const getPageName = () => { + if (!pathname) return 'this page'; + const parts = pathname.split('/').filter(Boolean); + const lastPart = parts[parts.length - 1] || 'home'; + return lastPart + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + }; + + const handleSubmitFeedback = async ( + rating: number, + reason: string, + message: string + ): Promise => { + const userEmail = userDetails?.email || userDetails?.userName || ''; + + if (!userEmail) { + showBanner({ + severity: 'error', + title: 'Unable to identify your account', + message: 'Please try again in a moment.', + scoped: true, + }); + return false; + } + + setIsSubmitting(true); + + try { + const metadata = { + page: pathname || window.location.pathname, + reason, + browser: + typeof window !== 'undefined' + ? navigator.userAgent.slice(0, 80) + : 'Unknown', + appVersion: process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0', + }; + + await feedbackService.submitFeedback({ + email: userEmail, + subject: `Page Satisfaction: ${getPageName()}`, + message: message || `Rating: ${rating}/5, Reason: ${reason}`, + rating, + category: 'page_satisfaction', + platform: 'web', + app: 'vertex', + metadata, + }); + + + return true; + } catch (error) { + showBanner({ + severity: 'error', + title: 'Failed to submit feedback', + message: error instanceof Error ? getApiErrorMessage(error) : 'An error occurred while submitting feedback', + scoped: true, + }); + return false; + } finally { + setIsSubmitting(false); + } + }; + + return ( + <> +
+
+
+

+ Overall, how satisfied are you with {getPageName()}? +

+
+ +
+ setModalType('positive')} + className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" + aria-label="Satisfied" + > + + + + setModalType('negative')} + className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" + aria-label="Not satisfied" + > + + + + + + +
+
+
+ + setModalType(null)} + onSubmit={handleSubmitFeedback} + isSubmitting={isSubmitting} + type="positive" + /> + + setModalType(null)} + onSubmit={handleSubmitFeedback} + isSubmitting={isSubmitting} + type="negative" + /> + + ); +}; diff --git a/src/vertex/components/layout/layout.tsx b/src/vertex/components/layout/layout.tsx index 440a1d02fb..7ec14c4e3e 100644 --- a/src/vertex/components/layout/layout.tsx +++ b/src/vertex/components/layout/layout.tsx @@ -12,6 +12,8 @@ import ErrorBoundary from '../shared/ErrorBoundary'; import Footer from './Footer'; import { OrganizationSetupBanner } from './organization-setup-banner'; import { FeedbackLauncher } from '../features/feedback/feedback-launcher'; +import { PageSatisfactionBanner } from '../features/feedback/page-satisfaction-banner'; +import { GlobalBannerContainer } from '@/context/banner-context'; import { setLastActiveModule } from '@/core/utils/userPreferences'; @@ -135,11 +137,13 @@ export default function Layout({ children }: LayoutProps) { />
+
+
+ {userDetails && }
diff --git a/src/vertex/context/banner-context.tsx b/src/vertex/context/banner-context.tsx index a791bd56f5..8fb984e875 100644 --- a/src/vertex/context/banner-context.tsx +++ b/src/vertex/context/banner-context.tsx @@ -105,7 +105,7 @@ export function GlobalBannerContainer() { if (!state.isVisible || state.isScoped) return null; return ( -
+