[web] workspace/discussions: add notification icon in the navbar (#1692)

This commit is contained in:
Nitesh Seram 2025-09-15 08:01:24 +05:30 committed by GitHub
parent d5374147b3
commit d99fb9643b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 210 additions and 21 deletions

View File

@ -8,39 +8,43 @@ type Props = Omit<React.ComponentProps<typeof NavbarEnd>, 'className'> &
Readonly<{
hideAdvertiseWithUsBadge?: boolean;
isPremium: boolean;
startAddOnItems?: React.ReactNode;
}>;
export default function NavbarEndWithAdvertiseWithUsBadge({
hideAdvertiseWithUsBadge,
isLoading,
isPremium,
startAddOnItems,
...props
}: Props) {
const user = useUser();
const isLoggedIn = user != null;
return (
<div
className={clsx('flex grow items-center justify-end lg:grow-0', 'gap-8')}>
{!hideAdvertiseWithUsBadge &&
(isLoggedIn && isPremium ? (
<div className="hidden sm:flex">
<SponsorsAdvertiseWithUsBadge />
</div>
) : (
<div
className={clsx(
'hidden min-[1200px]:flex',
isLoading ? 'opacity-0' : 'opacity-100',
)}>
<SponsorsAdvertiseWithUsBadge />
</div>
))}
<NavbarEnd
isLoading={isLoading}
{...props}
className={clsx('flex items-center gap-x-8')}
/>
<div className={clsx('flex grow items-center justify-end lg:grow-0')}>
{startAddOnItems}
<div className="flex items-center gap-x-8">
{!hideAdvertiseWithUsBadge &&
(isLoggedIn && isPremium ? (
<div className="hidden sm:flex">
<SponsorsAdvertiseWithUsBadge />
</div>
) : (
<div
className={clsx(
'hidden min-[1200px]:flex',
isLoading ? 'opacity-0' : 'opacity-100',
)}>
<SponsorsAdvertiseWithUsBadge />
</div>
))}
<NavbarEnd
isLoading={isLoading}
{...props}
className={clsx('flex items-center gap-x-8')}
/>
</div>
</div>
);
}

View File

@ -12,6 +12,7 @@ import { useAnchorClickHandler } from '~/hooks/useAnchorClickHandler';
import useIsSticky from '~/hooks/useIsSticky';
import useUserProfile from '~/hooks/user/useUserProfile';
import { INTERVIEWS_DISCUSSIONS_IS_LIVE } from '~/data/FeatureFlags';
import { SocialLinks } from '~/data/SocialLinks';
import NavbarAuthLink from '~/components/common/navigation/NavbarAuthLink';
@ -42,6 +43,7 @@ import {
themeBorderColor,
} from '~/components/ui/theme';
import InterviewsNotification from '../notifications/InterviewsNotification';
import InterviewsNavbarEndAddOnItems from './InterviewsNavbarEndAddOnItems';
import useInterviewsLoggedInLinks from './useInterviewsLoggedInLinks';
import useInterviewsNavItems from './useInterviewsNavItems';
@ -143,6 +145,18 @@ export default function InterviewsNavbar({
isLoading={isUserProfileLoading}
isPremium={isPremium}
links={rightLinks}
startAddOnItems={
INTERVIEWS_DISCUSSIONS_IS_LIVE ? (
<div
className={clsx(
'mr-3',
'hidden sm:block',
isUserProfileLoading ? 'opacity-0' : 'opacity-100',
)}>
<InterviewsNotification />
</div>
) : undefined
}
/>
<div className="-ml-3 lg:hidden">
<SlideOut

View File

@ -0,0 +1,60 @@
'use client';
import { useUser } from '@supabase/auth-helpers-react';
import clsx from 'clsx';
import { useState } from 'react';
import { RiNotification3Line } from 'react-icons/ri';
import { useIntl } from '~/components/intl';
import Button from '~/components/ui/Button';
import Popover from '~/components/ui/Popover';
import useInterviewsNotificationUnreadCount from './hooks/useInterviewsNotificationUnreadCount';
import InterviewsNotificationPopoverContent from './InterviewsNotificationPopoverContent';
import InterviewsNotificationUnreadIndicator from './InterviewsNotificationUnreadIndicator';
export default function InterviewsNotification() {
const user = useUser();
const intl = useIntl();
const unreadCount = useInterviewsNotificationUnreadCount();
const [showNotification, setShowNotification] = useState(false);
if (user == null) {
return null;
}
return (
<Popover
align="start"
className={clsx('overflow-y-auto', 'mr-6 w-[464px]')}
open={showNotification}
trigger={
<div className="relative">
<Button
icon={RiNotification3Line}
isLabelHidden={true}
label={intl.formatMessage({
defaultMessage: 'Notifications',
description: 'Notifications label',
id: 'sY9s8P',
})}
size="xs"
variant="tertiary"
onClick={() => setShowNotification(true)}
/>
{unreadCount > 0 && (
<InterviewsNotificationUnreadIndicator className="absolute right-0.5 top-0.5" />
)}
</div>
}
onOpenChange={(open) => {
if (!open) {
setShowNotification(false);
}
}}>
<InterviewsNotificationPopoverContent
closeNotification={() => setShowNotification(false)}
/>
</Popover>
);
}

View File

@ -0,0 +1,57 @@
import clsx from 'clsx';
import { RiNotification3Line } from 'react-icons/ri';
import { FormattedMessage } from '~/components/intl';
import Spinner from '~/components/ui/Spinner';
import Text from '~/components/ui/Text';
import {
themeDivideEmphasizeColor,
themeTextSubtitleColor,
} from '~/components/ui/theme';
type Props = Readonly<{ closeNotification: () => void }>;
export default function InterviewsNotificationPopoverContent(_: Props) {
const isLoading = false;
const notifications = [];
return (
<div>
{isLoading ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner size="sm" />
</div>
) : notifications?.length === 0 ? (
<div
className={clsx(
'h-full w-full',
'flex flex-col items-center justify-center gap-4',
)}>
<RiNotification3Line
className={clsx('size-10 shrink-0', themeTextSubtitleColor)}
/>
<div className="flex flex-col gap-1 text-center">
<Text size="body1" weight="medium">
<FormattedMessage
defaultMessage="No notification yet!"
description="Label for no notification"
id="hz5dJR"
/>
</Text>
<Text color="subtle" size="body2">
<FormattedMessage
defaultMessage="It looks like you don't have any notifications at the moment. Check back here for updates on your activities."
description="Description for no notification"
id="Nn5jH+"
/>
</Text>
</div>
</div>
) : (
<div className={clsx('divide-y', themeDivideEmphasizeColor)}>
Notifications
</div>
)}
</div>
);
}

View File

@ -0,0 +1,21 @@
import clsx from 'clsx';
import { themeBackgroundBrandColor } from '~/components/ui/theme';
type Props = Readonly<{
className?: string;
}>;
export default function InterviewsNotificationUnreadIndicator({
className,
}: Props) {
return (
<div
className={clsx(
'size-1.5 shrink-0 rounded-full',
themeBackgroundBrandColor,
className,
)}
/>
);
}

View File

@ -0,0 +1,17 @@
import { useUser } from '@supabase/auth-helpers-react';
import { trpc } from '~/hooks/trpc';
export default function useInterviewsNotificationUnreadCount() {
const user = useUser();
const { data: unreadCount } = trpc.notifications.getUnreadCount.useQuery(
undefined,
{
enabled: user != null,
refetchOnWindowFocus: true,
},
);
return unreadCount ?? 0;
}

View File

@ -5,6 +5,7 @@ import { emailsRouter } from './emails';
import { feedbackRouter } from './feedback';
import { guideProgressRouter } from './guide-progress';
import { marketingRouter } from './marketing';
import { notificationsRouter } from './notifications';
import { profileRouter } from './profile';
import { projectsRouter } from './projects';
import { promotionsRouter } from './promotions';
@ -28,6 +29,7 @@ export const appRouter = router({
feedback: feedbackRouter,
guideProgress: guideProgressRouter,
marketing: marketingRouter,
notifications: notificationsRouter,
profile: profileRouter,
projects: projectsRouter,
promotions: promotionsRouter,

View File

@ -0,0 +1,14 @@
import prisma from '~/server/prisma';
import { router, userProcedure } from '../trpc';
export const notificationsRouter = router({
getUnreadCount: userProcedure.query(async ({ ctx: { viewer } }) => {
return await prisma.interviewsActivity.count({
where: {
read: false,
recipientId: viewer.id,
},
});
}),
});