Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/vertex/app/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,47 @@

---

## Version 1.23.43
**Released:** May 17, 2026
Comment thread
Codebmk marked this conversation as resolved.

### 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.

<details>
<summary><strong>New Features (4)</strong></summary>

- **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.

</details>

<details>
<summary><strong>Layout & UI Bug Fixes (5)</strong></summary>

- **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 `<main>` 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 `<GlobalBannerContainer />` from the root application tree (`providers.tsx`) to the `max-w-7xl` page content wrapper inside `<main>` (`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`).

</details>

<details>
<summary><strong>Files Created/Modified (5)</strong></summary>

- `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]

</details>

---

## Version 1.23.42
**Released:** May 17, 2026

Expand Down
3 changes: 1 addition & 2 deletions src/vertex/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -48,7 +48,6 @@ export default function Providers({ children, session }: { children: React.React
disableTransitionOnChange
>
<BannerProvider>
<GlobalBannerContainer />
{children}
</BannerProvider>
</ThemeProvider>
Expand Down
275 changes: 275 additions & 0 deletions src/vertex/components/features/feedback/page-satisfaction-banner.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>;
isSubmitting: boolean;
type: 'positive' | 'negative';
}

const FeedbackModal: React.FC<FeedbackModalProps> = ({
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 (
<ReusableDialog
isOpen={isOpen}
onClose={() => {
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',
}}
>
<div className="flex flex-col gap-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-200">
Select a reason
</label>
<div className="space-y-2">
{reasons.map((r) => (
<label
key={r}
className="flex items-center gap-2 cursor-pointer"
>
<input
type="radio"
name="reason"
value={r}
checked={reason === r}
onChange={() => setReason(r)}
className="h-4 w-4 text-primary"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">
{r}
</span>
</label>
))}
</div>
</div>

<ReusableInputField
as="textarea"
id="message"
label="Any additional feedback about this page you would like to share with us?"
value={message}
onChange={(event) => setMessage(event.target.value)}
placeholder="Your feedback helps us improve..."
rows={5}
description="Please don't include any sensitive information"
/>
</div>
</ReusableDialog>
);
};

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<boolean> => {
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 (
<>
<div className="w-full border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 py-4 px-4">
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-center sm:text-left">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
Overall, how satisfied are you with {getPageName()}?
</h3>
</div>

<div className="flex items-center gap-3">
<ReusableButton
variant="text"
onClick={() => setModalType('positive')}
className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Satisfied"
>
<ThumbsUp className="h-6 w-6 text-gray-600 dark:text-gray-300" />
</ReusableButton>

<ReusableButton
variant="text"
onClick={() => setModalType('negative')}
className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Not satisfied"
>
<ThumbsDown className="h-6 w-6 text-gray-600 dark:text-gray-300" />
</ReusableButton>

<ReusableButton
variant="text"
onClick={openFeedbackDialog}
className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label="Comment"
>
<MessageSquare className="h-6 w-6 text-gray-600 dark:text-gray-300" />
</ReusableButton>
</div>
</div>
</div>

<FeedbackModal
isOpen={modalType === 'positive'}
onClose={() => setModalType(null)}
onSubmit={handleSubmitFeedback}
isSubmitting={isSubmitting}
type="positive"
/>

<FeedbackModal
isOpen={modalType === 'negative'}
onClose={() => setModalType(null)}
onSubmit={handleSubmitFeedback}
isSubmitting={isSubmitting}
type="negative"
/>
</>
);
};
9 changes: 7 additions & 2 deletions src/vertex/components/layout/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -135,11 +137,13 @@ export default function Layout({ children }: LayoutProps) {
/>
<main
data-vertex-main
className={`flex-1 transition-[margin-left] duration-300 ease-in-out bg-background w-full flex flex-col ${isSecondarySidebarCollapsed ? 'lg:ml-[88px]' : 'lg:ml-[256px]'} overflow-y-auto mt-[calc(50px+var(--vertex-ui-top-offset))] md:mt-[calc(50px+var(--vertex-ui-top-offset))] lg:mt-[calc(48px+var(--vertex-ui-top-offset))] pb-20 md:pb-0`}
className={`flex-1 transition-[margin-left] duration-300 ease-in-out bg-background w-full flex flex-col ${isSecondarySidebarCollapsed ? 'lg:ml-[88px]' : 'lg:ml-[256px]'} overflow-y-auto mt-[calc(50px+var(--vertex-ui-top-offset))] md:mt-[calc(50px+var(--vertex-ui-top-offset))] lg:mt-[calc(48px+var(--vertex-ui-top-offset))]`}
>

<div
className={`flex-1 w-full bg-background max-w-7xl mx-auto flex flex-col gap-4 md:gap-8 px-3 py-3 md:px-2 lg:py-6 lg:px-6`}
className={`flex-1 w-full bg-background max-w-7xl mx-auto flex flex-col gap-4 md:gap-8 px-3 py-3 md:px-2 lg:py-6 lg:px-6 pb-20 md:pb-0`}
Comment thread
Codebmk marked this conversation as resolved.
>
<GlobalBannerContainer />
<ErrorBoundary>
<motion.div
key={pathname}
Expand All @@ -153,6 +157,7 @@ export default function Layout({ children }: LayoutProps) {
</ErrorBoundary>
</div>
<Footer />
{userDetails && <PageSatisfactionBanner />}
</main>
<FeedbackLauncher />
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/vertex/context/banner-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function GlobalBannerContainer() {
if (!state.isVisible || state.isScoped) return null;

return (
<div className="px-4 pt-3">
<div className="pt-3">
<Banner
{...state.bannerProps}
dismissible
Expand Down
Loading