[web] workspace/discussions: add notification icon in the navbar (#1692)
This commit is contained in:
parent
d5374147b3
commit
d99fb9643b
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
Loading…
Reference in New Issue