Skip to content

Commit

Permalink
Feat: UI Facelift Notifications Popover (#658)
Browse files Browse the repository at this point in the history
## What was done?

- Updated all (or almost all) notifications-related components with our
own, removing native-base from those.

<img width="1440" alt="Bildschirmfoto 2024-09-15 um 16 57 47"
src="https://github.com/user-attachments/assets/ab82c187-907b-40f7-a230-e9df41f04232">
  • Loading branch information
JeangelLF authored Sep 23, 2024
1 parent dee390c commit a6ccaa6
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 272 deletions.
3 changes: 0 additions & 3 deletions src/assets/icons/lernfair/ico-settings.svg

This file was deleted.

6 changes: 5 additions & 1 deletion src/components/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ const PopoverTrigger = PopoverPrimitive.Trigger;

const PopoverAnchor = PopoverPrimitive.Anchor;

const PopoverArrow = PopoverPrimitive.Arrow;

const PopoverClose = PopoverPrimitive.Close;

const PopoverContent = React.forwardRef<React.ElementRef<typeof PopoverPrimitive.Content>, React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>>(
({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
Expand All @@ -25,4 +29,4 @@ const PopoverContent = React.forwardRef<React.ElementRef<typeof PopoverPrimitive
)
);

export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverArrow, PopoverClose };
132 changes: 63 additions & 69 deletions src/components/notifications/MessageBox.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,32 @@
import { Box, HStack, Modal, Pressable, Spacer, Text, Tooltip, VStack, useBreakpointValue } from 'native-base';
import { getIconForMessageType, isMessageValid } from '../../helper/notification-helper';
import TimeIndicator from './TimeIndicator';
import { useNavigate } from 'react-router-dom';
import { FC, useState } from 'react';
import { InterfaceBoxProps } from 'native-base/lib/typescript/components/primitives/Box';
import { useState } from 'react';
import LeavePageModal from '../../modals/LeavePageModal';
import { Concrete_Notification } from '../../gql/graphql';
import AppointmentCancelledModal from './NotificationModal';
import NotificationModal from './NotificationModal';
import AchievementMessageModal from '../../modals/AchievementMessageModal';
import { Typography } from '../Typography';
import { cn } from '@/lib/Tailwind';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../Tooltip';

type Props = {
interface MessageBoxProps {
userNotification: Concrete_Notification;
isStandalone?: boolean;
isRead?: boolean;
updateLastTimeChecked?: () => void;
};
}

const MessageBox: FC<Props> = ({ userNotification, isStandalone, isRead, updateLastTimeChecked }) => {
const MessageBox = ({ userNotification, isStandalone, isRead, updateLastTimeChecked }: MessageBoxProps) => {
const [leavePageModalOpen, setLeavePageModalOpen] = useState<boolean>(false);
const [achievementModalForId, setAchievementModalForId] = useState<number | null>(null);
const [notificationModalOpen, setNotificationModalOpen] = useState<boolean>(false);
const navigate = useNavigate();
const isMobile = useBreakpointValue({
base: true,
lg: false,
});

if (!userNotification || !userNotification.message || !isMessageValid(userNotification.message)) return null;

const { sentAt } = userNotification || { sentAt: '' };
const { headline, body, type, navigateTo, modalText } = userNotification.message;
const boxProps = {
mb: 2,
height: '100%',
fullWidth: 320,
width: 270,
borderRadius: 10,
maxHeight: 500,
};

const vStackProps = {
mt: 2,
maxW: 200,
};

const navigateToLink = () => {
if (modalText) {
Expand Down Expand Up @@ -70,18 +54,21 @@ const MessageBox: FC<Props> = ({ userNotification, isStandalone, isRead, updateL
const navigateExternal = () => (navigateTo ? window.open(navigateTo, '_blank') : null);

const Icon = getIconForMessageType(type);

const LinkedBox: FC<InterfaceBoxProps> = ({ children, ...boxProps }) => {
const Component = () => <Box {...boxProps}>{children}</Box>;
const LinkedBox = ({ children, ...rest }: React.HTMLAttributes<HTMLDivElement>) => {
const Component = () => <div {...rest}>{children}</div>;
if (typeof navigateTo === 'string') {
return (
<>
<Pressable onPress={navigateToLink}>
<div onClick={navigateToLink}>
<Component />
</Pressable>
<Modal isOpen={leavePageModalOpen}>
<LeavePageModal url={navigateTo} messageType={type} onClose={() => setLeavePageModalOpen(false)} navigateTo={navigateExternal} />
</Modal>
</div>
<LeavePageModal
isOpen={leavePageModalOpen}
url={navigateTo}
messageType={type}
onOpenChange={setLeavePageModalOpen}
navigateTo={navigateExternal}
/>
{achievementModalForId !== null && (
<AchievementMessageModal achievementId={achievementModalForId} isOpenModal={true} onClose={() => setAchievementModalForId(null)} />
)}
Expand All @@ -90,17 +77,16 @@ const MessageBox: FC<Props> = ({ userNotification, isStandalone, isRead, updateL
} else if (modalText) {
return (
<>
<Pressable onPress={navigateToLink}>
<div onClick={navigateToLink}>
<Component />
</Pressable>
<Modal isOpen={notificationModalOpen}>
<AppointmentCancelledModal
messageType={type}
onClose={() => setNotificationModalOpen(false)}
modalText={modalText}
headline={headline}
/>
</Modal>
</div>
<NotificationModal
messageType={type}
isOpen={notificationModalOpen}
onOpenChange={setNotificationModalOpen}
modalText={modalText}
headline={headline}
/>
</>
);
}
Expand All @@ -109,38 +95,46 @@ const MessageBox: FC<Props> = ({ userNotification, isStandalone, isRead, updateL

return (
<LinkedBox
borderRadius={boxProps.borderRadius}
bgColor={isRead ? 'ghost' : 'primary.100'}
mb={boxProps.mb}
h={boxProps.height}
w={!isStandalone ? boxProps.fullWidth : boxProps.width}
maxH={boxProps.maxHeight}
className={cn(
'cursor-pointer rounded-md mb-2 py-2 h-full max-h-[500px] hover:bg-primary-lighter',
isRead ? 'bg-white' : 'bg-primary-lighter',
!isStandalone ? 'w-full' : 'w-[270px]'
)}
>
<HStack alignItems="center" space={1}>
<VStack>
<Box px="1.5">
<Icon />
</Box>
</VStack>
<VStack mt={vStackProps.mt} maxW={vStackProps.maxW}>
<Text bold fontSize="md" ellipsizeMode="tail" numberOfLines={1}>
{headline}
</Text>
<Tooltip maxW={300} label={body} _text={{ textAlign: 'center' }}>
<Pressable onPress={navigateToLink}>
<Text fontSize="sm" ellipsizeMode="tail" numberOfLines={isMobile ? 5 : 2}>
<div className="flex items-center gap-x-1">
<div className="flex flex-col px-1.5">
<Icon />
</div>
<div className="flex flex-col max-w-[200px]">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Typography className="font-semibold line-clamp-2 leading-3">{headline}</Typography>
</TooltipTrigger>
<TooltipContent side="bottom" className="bg-primary text-primary-foreground border-transparent max-w-80">
{headline}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Typography className="line-clamp-5 lg:line-clamp-2" variant="sm">
{body}
</Typography>
</TooltipTrigger>
<TooltipContent side="bottom" className="bg-primary text-primary-foreground border-transparent max-w-80">
{body}
</Text>
</Pressable>
</Tooltip>
</VStack>
<Spacer />
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{!isStandalone && (
<VStack>
<div className="ml-auto">
<TimeIndicator sentAt={sentAt} />
</VStack>
</div>
)}
</HStack>
</div>
</LinkedBox>
);
};
Expand Down
70 changes: 37 additions & 33 deletions src/components/notifications/NotificationAlert.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Popover } from 'native-base';
import { MutableRefObject, useContext, useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { useLastTimeCheckedNotifications } from '../../hooks/useLastTimeCheckedNotifications';
import { useConcreteNotifications } from '../../hooks/useConcreteNotifications';
import NotificationPanel from './NotificationPanel';
import { NotificationsContext } from '../../context/NotificationsProvider';
import { getNewNotifications } from '../../helper/notification-helper';
import { Button } from '../Button';
import { IconBell } from '@tabler/icons-react';
import { IconBell, IconX } from '@tabler/icons-react';
import { Badge } from '../Badge';
import { Popover, PopoverArrow, PopoverClose, PopoverContent, PopoverTrigger } from '../Popover';

const NotificationAlert: React.FC = () => {
const [count, setCount] = useState<number>(0);
Expand All @@ -33,40 +33,44 @@ const NotificationAlert: React.FC = () => {
setCount(unreadNotifications.length);
}, [lastTimeCheckedNotifications, userNotifications]);

const handleTrigger = ({ onPress, ref }: { onPress: () => void; ref: MutableRefObject<any> }): React.ReactElement => {
return (
<div className="flex flex-col relative">
{!!count && (
<Badge className="absolute self-start size-4 top-[4px] right-[5px]" variant="destructive" shape="rounded">
{count}
</Badge>
)}
<Button onClick={onPress} ref={ref} variant="none" size="icon">
<IconBell size={24} />
</Button>
</div>
);
};

const onOpen = () => {
refetch();
setIsOpen(true);
};

const onClose = () => {
updateLastTimeChecked();
setIsOpen(false);
const handleOnOpenChange = (value: boolean) => {
setIsOpen(value);
if (value) {
refetch();
} else {
updateLastTimeChecked();
}
};

return (
<>
<Popover placement="bottom" trigger={(triggerprops) => handleTrigger(triggerprops)} onClose={onClose} onOpen={onOpen}>
<NotificationPanel
loading={loading}
userNotifications={userNotifications || []}
lastTimeCheckedNotifications={lastTimeCheckedNotifications}
updateLastTimeChecked={() => updateLastTimeChecked()}
/>
<Popover onOpenChange={handleOnOpenChange}>
<PopoverTrigger asChild>
<div className="flex flex-col relative">
{!!count && (
<Badge className="absolute self-start size-4 top-[4px] right-[5px]" variant="destructive" shape="rounded">
{count}
</Badge>
)}
<Button variant="none" size="icon">
<IconBell size={24} />
</Button>
</div>
</PopoverTrigger>
<PopoverContent align="end" side="bottom" className="min-w-[350px] max-h-[500px] px-0 border-t-transparent">
<div className="px-4">
<PopoverArrow className="fill-white" />
<PopoverClose className="ml-auto block mb-2">
<IconX size={18} />
</PopoverClose>
</div>
<NotificationPanel
loading={loading}
userNotifications={userNotifications || []}
lastTimeCheckedNotifications={lastTimeCheckedNotifications}
updateLastTimeChecked={() => updateLastTimeChecked()}
/>
</PopoverContent>
</Popover>
</>
);
Expand Down
59 changes: 21 additions & 38 deletions src/components/notifications/NotificationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,35 @@
import { Box, CloseIcon, Heading, Modal, Pressable, useTheme, Row, Button, Text } from 'native-base';
import { useTranslation } from 'react-i18next';
import { getIconForNotificationPreferenceModal } from '../../helper/notification-helper';
import { Button } from '../Button';
import { BaseModalProps, Modal, ModalFooter, ModalHeader, ModalTitle } from '../Modal';
import { Typography } from '../Typography';

type Props = {
interface NotificationModalProps extends BaseModalProps {
messageType: string;
headline?: string;
modalText: string;
onClose: () => any;
};
const AppointmentCancelledModal: React.FC<Props> = ({ messageType, onClose, headline, modalText }) => {
}
const NotificationModal = ({ isOpen, onOpenChange, messageType, headline, modalText }: NotificationModalProps) => {
const { t } = useTranslation();
const { space } = useTheme();

const Icon = getIconForNotificationPreferenceModal(messageType);

return (
<>
<Modal.Content width="350" marginX="auto" backgroundColor="transparent">
<Box position="absolute" zIndex="1" right="20px" top="14px">
<Pressable onPress={onClose}>
<CloseIcon color="white" />
</Pressable>
</Box>
<Modal.Body background="primary.900" padding={space['1']}>
<Box alignItems="center" marginY={space['1']}>
<Icon />
</Box>
<Box paddingY={space['2']} maxW={'100%'}>
{headline && (
<Heading maxWidth="330px" marginX="auto" fontSize="lg" textAlign={'center'} color="lightText" marginBottom={space['0.5']}>
{headline}
</Heading>
)}
<Text my={2} textAlign={'center'} fontSize="sm" color="lightText">
{modalText}
</Text>
</Box>
<Box>
<Row marginBottom={space['0.5']}>
<Button variant={'outlinelight'} onPress={onClose} width="100%">
{t('notification.controlPanel.closeButton')}
</Button>
</Row>
</Box>
</Modal.Body>
</Modal.Content>
</>
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalHeader>
<ModalTitle>{headline}</ModalTitle>
</ModalHeader>
<div className="flex flex-col items-center">
<Icon className="scale-[0.5]" />
<Typography>{modalText}</Typography>
</div>
<ModalFooter>
<Button className="w-full lg:w-fit" variant="outline" onClick={() => onOpenChange(false)}>
{t('notification.controlPanel.closeButton')}
</Button>
</ModalFooter>
</Modal>
);
};

export default AppointmentCancelledModal;
export default NotificationModal;
Loading

0 comments on commit a6ccaa6

Please sign in to comment.