Skip to content

Commit

Permalink
feat(dashboard,web): opt-in welcome modal (#6920)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChmaraX authored Nov 11, 2024
1 parent 3ec534e commit 9cb79a5
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 23 deletions.
Binary file added apps/dashboard/public/images/opt-in.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions apps/dashboard/src/components/icons/circle-check.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { HTMLAttributes } from 'react';

export const CircleCheck = (props: HTMLAttributes<HTMLOrSVGElement>) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 17 16" fill="none" {...props}>
<path
d="M6.50004 7.99999L7.83337 9.33333L10.5 6.66666M15.1667 7.99999C15.1667 11.6819 12.1819 14.6667 8.50004 14.6667C4.81814 14.6667 1.83337 11.6819 1.83337 7.99999C1.83337 4.3181 4.81814 1.33333 8.50004 1.33333C12.1819 1.33333 15.1667 4.3181 15.1667 7.99999Z"
stroke="#1FC16B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
};
13 changes: 13 additions & 0 deletions apps/dashboard/src/components/icons/opt-in-arrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { HTMLAttributes } from 'react';

export const OptInArrow = (props: HTMLAttributes<HTMLOrSVGElement>) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="84" viewBox="0 0 78 84" fill="none" {...props}>
<path
d="M7 65.7276C11.3062 66.8823 25.7351 69.45 26.0443 60.8367C26.1319 58.3972 23.3702 54.4428 20.443 55.822C16.5397 57.6612 20.5868 62.494 23.2437 63.0655C30.243 64.5709 36.5796 57.4272 38.336 51.4573C39.8111 46.444 38.6183 40.9473 36.8112 36.1965C35.9635 33.9679 34.2644 32.4464 33.326 30.3459C31.8742 27.0965 35.4308 30.6525 37.2158 31.4294C40.8631 33.0167 34.8603 30.6219 33.606 30.0673C31.5365 29.1524 30.6469 34.7193 29.9652 36.7537"
stroke="#1FC16B"
stroke-linecap="round"
/>
</svg>
);
};
15 changes: 15 additions & 0 deletions apps/dashboard/src/components/icons/party-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { HTMLAttributes } from 'react';

export const PartyPopover = (props: HTMLAttributes<HTMLOrSVGElement>) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 21 20" fill="none" {...props}>
<path
d="M5.33329 9.4167L2.16663 18.3334L11.0833 15.175M3.83329 2.49999H3.84163M18.8333 6.66666H18.8416M13 1.66666H13.0083M18.8333 16.6667H18.8416M18.8333 1.66666L16.9666 2.29166C16.4353 2.46865 15.9819 2.82469 15.684 3.29893C15.3861 3.77316 15.2621 4.33615 15.3333 4.89166C15.4166 5.60832 14.8583 6.24999 14.125 6.24999H13.8083C13.0916 6.24999 12.475 6.74999 12.3416 7.44999L12.1666 8.33332M18.8333 10.8333L18.15 10.5583C17.4333 10.2749 16.6333 10.7249 16.5 11.4833C16.4083 12.0666 15.9 12.4999 15.3083 12.4999H14.6666M9.66663 1.66666L9.94163 2.34999C10.225 3.06666 9.77496 3.86666 9.01663 3.99999C8.43329 4.08332 7.99996 4.59999 7.99996 5.19166V5.83332M9.66664 10.8333C11.275 12.4417 12.025 14.3083 11.3333 15C10.6416 15.6917 8.77497 14.9417 7.16664 13.3333C5.55831 11.725 4.80831 9.85834 5.49997 9.16667C6.19164 8.475 8.05831 9.225 9.66664 10.8333Z"
stroke="white"
stroke-width="1.33"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
};
95 changes: 95 additions & 0 deletions apps/dashboard/src/components/opt-in-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { CircleCheck } from '@/components/icons/circle-check';
import { PartyPopover } from '@/components/icons/party-popover';
import { OptInArrow } from '@/components/icons/opt-in-arrow';
import { Button } from '@/components/primitives/button';
import {
Dialog,
DialogClose,
DialogDescription,
DialogContent,
DialogOverlay,
DialogPortal,
DialogTitle,
} from '@/components/primitives/dialog';
import { RiCustomerService2Line } from 'react-icons/ri';
import { useNewDashboardOptIn } from '@/hooks/use-new-dashboard-opt-in';
import { NewDashboardOptInStatusEnum } from '@novu/shared';

export const OptInModal = () => {
const { status, optIn } = useNewDashboardOptIn();

const isOptedIn = status === NewDashboardOptInStatusEnum.OPTED_IN;

if (isOptedIn) {
return null;
}

return (
<Dialog modal open={!isOptedIn} onOpenChange={optIn}>
<DialogPortal>
<DialogOverlay />
<DialogContent className="p-0">
<div className="flex items-start gap-4 self-stretch">
<Image />
<div className="flex w-[383px] flex-col items-start">
<Header />
<CheckBulletPoint
content={
<span>
We're still building and welcome your feedback — share your thoughts using{' '}
<RiCustomerService2Line className="inline" /> in the top right.
</span>
}
/>
<CheckBulletPoint content="You can switch back to the legacy UI anytime from the user profile menu in the top right." />
<CheckBulletPoint content="Create workflows with in-app steps on the new dashboard; support for more steps is coming soon." />
<Footer />
</div>
</div>
</DialogContent>
</DialogPortal>
</Dialog>
);
};

const Image = () => (
<div className="relative">
<img src="/public/images/opt-in.png" alt="New Dashboard Preview" />
<div
className="absolute inset-0 rounded-lg"
style={{ background: 'linear-gradient(163deg, rgba(255, 255, 255, 0.00) 7.65%, #FFF 92.93%)' }}
/>
<div className="absolute bottom-[13px] left-[13px] flex w-[158.5px] flex-col items-start">
<span className="text-success text-[10px] font-normal italic">We're doing light mode for now!</span>
<OptInArrow className="absolute bottom-[-11.505px] left-[152.5px]" />
</div>
</div>
);

const Header = () => (
<div className="flex items-start justify-between gap-1 self-stretch p-3">
<div className="flex flex-1 flex-col items-start gap-1">
<DialogTitle className="text-lg font-medium">Thanks for opting-in! 🎉</DialogTitle>
<DialogDescription className="text-foreground-400 text-xs font-normal">
Get an early look at our enhanced dashboard.
</DialogDescription>
</div>
</div>
);

const CheckBulletPoint = ({ content }: { content: React.ReactNode }) => (
<div className="flex items-center gap-1 px-3 py-2">
<CircleCheck />
<div className="text-foreground-500 px-2 py-1 text-xs font-medium">{content}</div>
</div>
);

const Footer = () => (
<div className="flex w-full justify-end p-3">
<DialogClose asChild aria-label="Close">
<Button type="button" size="sm" variant="primary" className="gap-2 p-2">
I'm in <PartyPopover />
</Button>
</DialogClose>
</div>
);
5 changes: 3 additions & 2 deletions apps/dashboard/src/components/primitives/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/10',
className
)}
{...props}
Expand All @@ -37,7 +37,8 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-5 border p-5 shadow duration-200 sm:rounded-lg',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-auto min-w-[320px] translate-x-[-50%] translate-y-[-50%] gap-5 border p-5 shadow duration-200 sm:rounded-lg',

className
)}
{...props}
Expand Down
50 changes: 50 additions & 0 deletions apps/dashboard/src/hooks/use-new-dashboard-opt-in.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useUser } from '@clerk/clerk-react';
import { NewDashboardOptInStatusEnum } from '@novu/shared';

export function useNewDashboardOptIn() {
const { user } = useUser();

const updateUserOptInStatus = (status: NewDashboardOptInStatusEnum) => {
if (!user) return;

user.update({
unsafeMetadata: {
...user.unsafeMetadata,
newDashboardOptInStatus: status,
},
});
};

const getCurrentOptInStatus = () => {
if (!user) return null;

return user.unsafeMetadata?.newDashboardOptInStatus || null;
};

const redirectToNewDashboard = () => {
const newDashboardUrl = process.env.NEW_DASHBOARD_URL;
if (!newDashboardUrl) return;

window.location.href = newDashboardUrl;
};

const optIn = () => {
updateUserOptInStatus(NewDashboardOptInStatusEnum.OPTED_IN);
};

const optOut = () => {
updateUserOptInStatus(NewDashboardOptInStatusEnum.OPTED_OUT);
};

const dismiss = () => {
updateUserOptInStatus(NewDashboardOptInStatusEnum.DISMISSED);
};

return {
optIn,
optOut,
dismiss,
status: getCurrentOptInStatus(),
redirectToNewDashboard,
};
}
2 changes: 2 additions & 0 deletions apps/dashboard/src/pages/workflows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { Input } from '@/components/primitives/input';
import { Button } from '@/components/primitives/button';
import { RiSearch2Line } from 'react-icons/ri';
import { CreateWorkflowButton } from '@/components/create-workflow-button';
import { OptInModal } from '@/components/opt-in-modal';

export const WorkflowsPage = () => {
return (
<DashboardLayout headerStartItems={<h1 className="text-foreground-950">Workflows</h1>}>
<OptInModal />
<div className="mt-3 flex justify-between px-6 py-2.5">
<div className="flex w-[20ch] items-center gap-2 rounded-lg bg-neutral-50 p-2">
<RiSearch2Line className="text-foreground-400 size-5" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useFeatureFlag } from '../../../../hooks';
import { useNewDashboardOptIn } from '../../../../hooks/useNewDashboardOptIn';

export function NewDashboardOptInWidget() {
const { dismiss, optIn, status } = useNewDashboardOptIn();
const { dismiss, redirectToNewDashboard, status } = useNewDashboardOptIn();

const isNewDashboardEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_NEW_DASHBOARD_ENABLED);

Expand All @@ -32,7 +32,7 @@ export function NewDashboardOptInWidget() {
</Text>
</div>
<div className={styles.buttonContainer}>
<Button size="sm" variant="transparent" onClick={optIn}>
<Button size="sm" variant="transparent" onClick={redirectToNewDashboard}>
Take me there
</Button>
</div>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/ee/clerk/components/UserProfileButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import { useNewDashboardOptIn } from '../../../hooks/useNewDashboardOptIn';
import { useFeatureFlag } from '../../../hooks';

export function UserProfileButton() {
const { optIn } = useNewDashboardOptIn();
const { redirectToNewDashboard } = useNewDashboardOptIn();
const isNewDashboardEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_NEW_DASHBOARD_ENABLED);

return (
<UserButton afterSignOutUrl={ROUTES.AUTH_LOGIN} userProfileUrl={ROUTES.MANAGE_ACCOUNT_USER_PROFILE}>
{isNewDashboardEnabled && (
<UserButton.MenuItems>
<UserButton.Action
label="Try our new Dashboard (beta)"
label="Try out the new dashboard (beta)"
labelIcon={<IconBolt size="16" color="var(--nv-colors-typography-text-main)" />}
onClick={optIn}
onClick={redirectToNewDashboard}
/>
</UserButton.MenuItems>
)}
Expand Down
15 changes: 12 additions & 3 deletions apps/web/src/hooks/useNewDashboardOptIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ export function useNewDashboardOptIn() {
return user.unsafeMetadata?.newDashboardOptInStatus || null;
};

const optIn = () => {
const redirectToNewDashboard = () => {
const newDashboardUrl = process.env.NEW_DASHBOARD_URL;
if (!newDashboardUrl) return;

updateUserOptInStatus(NewDashboardOptInStatusEnum.OPTED_IN);
window.location.href = newDashboardUrl;
};

const optIn = () => {
updateUserOptInStatus(NewDashboardOptInStatusEnum.OPTED_IN);
};

const optOut = () => {
updateUserOptInStatus(NewDashboardOptInStatusEnum.OPTED_OUT);
};
Expand All @@ -37,5 +40,11 @@ export function useNewDashboardOptIn() {
updateUserOptInStatus(NewDashboardOptInStatusEnum.DISMISSED);
};

return { optIn, optOut, dismiss, status: getCurrentOptInStatus() };
return {
optIn,
optOut,
dismiss,
status: getCurrentOptInStatus(),
redirectToNewDashboard,
};
}
26 changes: 13 additions & 13 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9cb79a5

Please sign in to comment.