Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: UI Facelift Notifications Popover #658

Merged
merged 7 commits into from
Sep 23, 2024
Merged
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
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
Loading