| <NotificationsIcon /> | <NotificationsIcon /> | ||||
| </HeaderIconContainer> | </HeaderIconContainer> | ||||
| } | } | ||||
| contentContainerStyles={{ borderRadius: "8px" }} | |||||
| contentContainerStyles={{ | |||||
| "& .MuiPopover-paper": { | |||||
| borderRadius: "6px", | |||||
| backgroundColor: "transparent", | |||||
| }, | |||||
| }} | |||||
| content={<HeaderNotificationsContent />} | content={<HeaderNotificationsContent />} | ||||
| popoverProps={{ | popoverProps={{ | ||||
| anchorOrigin: { | anchorOrigin: { |
| import React from "react"; | |||||
| import React, { useMemo } from "react"; | |||||
| import PropTypes from "prop-types"; | import PropTypes from "prop-types"; | ||||
| import { | import { | ||||
| HeaderLatestNotificationsContainer, | HeaderLatestNotificationsContainer, | ||||
| HeaderNotificationsContentHeaderTitle, | HeaderNotificationsContentHeaderTitle, | ||||
| } from "./HeaderNotificationsContent.styled"; | } from "./HeaderNotificationsContent.styled"; | ||||
| import SingleNotification from "./SingleNotification/SingleNotification"; | import SingleNotification from "./SingleNotification/SingleNotification"; | ||||
| import { useNotificationsQuery } from "features/user/usersApiSlice"; | |||||
| import { AccountCircle } from "@mui/icons-material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| const HeaderNotificationsContent = (props) => { | |||||
| console.log(props); | |||||
| const dummyNotification = { | |||||
| userPicture: <AccountCircle />, | |||||
| notificationText: ( | |||||
| <p> | |||||
| <i>Olivia Saturday</i> commented on your post. | |||||
| </p> | |||||
| ), | |||||
| date: new Date(2023, 6, 10), | |||||
| }; | |||||
| const HeaderNotificationsContent = () => { | |||||
| const { data } = useNotificationsQuery(); //eslint-disable-line | |||||
| const { t } = useTranslation(); | |||||
| const latestNotifications = useMemo(() => { | |||||
| // return data?.notifications?.slice(0, 3); | |||||
| return Array(3).fill(dummyNotification, 0); | |||||
| }); | |||||
| return ( | return ( | ||||
| <HeaderNotificationsContentContainer> | <HeaderNotificationsContentContainer> | ||||
| <HeaderNotificationsContentHeader> | <HeaderNotificationsContentHeader> | ||||
| <HeaderNotificationsContentHeaderTitle> | <HeaderNotificationsContentHeaderTitle> | ||||
| Notifications | |||||
| {t("notifications.title")} | |||||
| </HeaderNotificationsContentHeaderTitle> | </HeaderNotificationsContentHeaderTitle> | ||||
| <HeaderNotificationsContentHeaderSeeMore> | <HeaderNotificationsContentHeaderSeeMore> | ||||
| See more | |||||
| {t("common.seeMore")} | |||||
| </HeaderNotificationsContentHeaderSeeMore> | </HeaderNotificationsContentHeaderSeeMore> | ||||
| </HeaderNotificationsContentHeader> | </HeaderNotificationsContentHeader> | ||||
| <HeaderLatestNotificationsContainer> | <HeaderLatestNotificationsContainer> | ||||
| <SingleNotification /> | |||||
| <SingleNotification /> | |||||
| <SingleNotification /> | |||||
| {latestNotifications?.map?.((singleNotification, index) => ( | |||||
| <SingleNotification | |||||
| key={index} | |||||
| notificationObject={singleNotification} | |||||
| /> | |||||
| ))} | |||||
| </HeaderLatestNotificationsContainer> | </HeaderLatestNotificationsContainer> | ||||
| </HeaderNotificationsContentContainer> | </HeaderNotificationsContentContainer> | ||||
| ); | ); |
| import React from "react"; | |||||
| import React, { useEffect, useMemo } from "react"; | |||||
| import PropTypes from "prop-types"; | import PropTypes from "prop-types"; | ||||
| import { | import { | ||||
| SingleNotificationContainer, | SingleNotificationContainer, | ||||
| SingleNotificationProfile, | SingleNotificationProfile, | ||||
| SingleNotificationUnseen, | SingleNotificationUnseen, | ||||
| } from "./SingleNotification.styled"; | } from "./SingleNotification.styled"; | ||||
| import AccountCircleIcon from "@mui/icons-material/AccountCircle"; | |||||
| import { formatTimeSpan } from "util/dateHelpers"; | |||||
| import { useSeeNotificationMutation } from "features/user/usersApiSlice"; | |||||
| import { makeErrorToastMessage } from "util/toastMessage"; | |||||
| const SingleNotification = (props) => { | |||||
| const [seeNotification, result] = useSeeNotificationMutation(); | |||||
| const timespan = useMemo( | |||||
| () => formatTimeSpan(props?.notificationObject?.date), | |||||
| [props?.notificationObject?.date] | |||||
| ); | |||||
| const handleSeeNotification = () => { | |||||
| seeNotification(2); | |||||
| console.log(result) | |||||
| } | |||||
| useEffect(() => { | |||||
| if (result.isError) { | |||||
| makeErrorToastMessage("Server error") | |||||
| } | |||||
| }, [result.isError]) | |||||
| const SingleNotification = () => { | |||||
| return ( | return ( | ||||
| <SingleNotificationContainer> | |||||
| <SingleNotificationContainer onClick={handleSeeNotification}> | |||||
| <SingleNotificationProfile> | <SingleNotificationProfile> | ||||
| <AccountCircleIcon /> | |||||
| {props?.notificationObject?.userPicture} | |||||
| </SingleNotificationProfile> | </SingleNotificationProfile> | ||||
| <SingleNotificationDetails> | <SingleNotificationDetails> | ||||
| <SingleNotificationContent> | <SingleNotificationContent> | ||||
| <p> | |||||
| <i>Olivia Saturday</i> commented on your | |||||
| post. | |||||
| </p> | |||||
| {props?.notificationObject?.notificationText} | |||||
| </SingleNotificationContent> | </SingleNotificationContent> | ||||
| <SingleNotificationDate> | |||||
| 12 hours ago | |||||
| </SingleNotificationDate> | |||||
| <SingleNotificationDate>{timespan}</SingleNotificationDate> | |||||
| </SingleNotificationDetails> | </SingleNotificationDetails> | ||||
| <SingleNotificationUnseen /> | <SingleNotificationUnseen /> | ||||
| </SingleNotificationContainer> | </SingleNotificationContainer> | ||||
| SingleNotification.propTypes = { | SingleNotification.propTypes = { | ||||
| children: PropTypes.node, | children: PropTypes.node, | ||||
| notificationObject: PropTypes.shape({ | |||||
| userPicture: PropTypes.oneOfType([PropTypes.element, PropTypes.node]), | |||||
| notificationText: PropTypes.node, | |||||
| date: PropTypes.object, | |||||
| }), | |||||
| }; | }; | ||||
| export default SingleNotification; | export default SingleNotification; |
| import { HeaderProfileContainer } from "./HeaderProfile.styled"; | import { HeaderProfileContainer } from "./HeaderProfile.styled"; | ||||
| import PersonIcon from "@mui/icons-material/Person"; | import PersonIcon from "@mui/icons-material/Person"; | ||||
| import AccountCircleIcon from "@mui/icons-material/AccountCircle"; | import AccountCircleIcon from "@mui/icons-material/AccountCircle"; | ||||
| import PopoverComponent from "components/Popover/Popover"; | |||||
| import { HeaderIconContainer } from "components/Header/Header.styled"; | |||||
| import HeaderProfilePopoverContent from "./HeaderProfilePopoverContent/HeaderProfilePopoverContent"; | |||||
| const HeaderProfile = () => { | const HeaderProfile = () => { | ||||
| return <HeaderProfileContainer>profile</HeaderProfileContainer>; | |||||
| return ( | |||||
| <HeaderProfileContainer> | |||||
| <PopoverComponent | |||||
| contentContainerStyles={{ | |||||
| "& .MuiPopover-paper": { | |||||
| borderRadius: "8px", | |||||
| overflow: "hidden", | |||||
| backgroundColor: "transparent", | |||||
| }, | |||||
| }} | |||||
| trigger={ | |||||
| <HeaderIconContainer> | |||||
| <AccountCircleIcon /> | |||||
| </HeaderIconContainer> | |||||
| } | |||||
| content={<HeaderProfilePopoverContent />} | |||||
| /> | |||||
| </HeaderProfileContainer> | |||||
| ); | |||||
| }; | }; | ||||
| HeaderProfile.propTypes = { | HeaderProfile.propTypes = { |
| import { Box } from "@mui/material"; | import { Box } from "@mui/material"; | ||||
| import styled from "styled-components"; | import styled from "styled-components"; | ||||
| export const HeaderProfileContainer = styled(Box)`` | |||||
| export const HeaderProfileContainer = styled(Box)` | |||||
| & .MuiPopover-paper { | |||||
| border-radius: 8px; | |||||
| } | |||||
| ` |
| import React, { useMemo } from "react"; | |||||
| import PropTypes from "prop-types"; | |||||
| import { | |||||
| HeaderProfileMenu, | |||||
| HeaderProfileMenuItem, | |||||
| HeaderProfilePopoverContentContainer, | |||||
| HeaderProfilePopoverContentHeaderContainer, | |||||
| ProfileDetails, | |||||
| ProfileInitials, | |||||
| ProfileMail, | |||||
| ProfileName, | |||||
| } from "./HeaderProfilePopoverContent.styled"; | |||||
| import { PAGES } from "constants/pages"; | |||||
| import { useMyUserQuery } from "features/user/usersApiSlice"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| const HeaderProfilePopoverContent = () => { | |||||
| const { data } = useMyUserQuery(); //eslint-disable-line | |||||
| const { t } = useTranslation(); | |||||
| const myInitials = useMemo(() => { | |||||
| // return `${data?.firstName?.[0]}${data?.lastName?.[0]}` | |||||
| return "AM"; | |||||
| }, [data]); | |||||
| const myName = useMemo(() => { | |||||
| // return `${data?.firstName} ${data?.lastName}` | |||||
| return "Andrew Marks"; | |||||
| }, [data]); | |||||
| const myMail = useMemo(() => { | |||||
| // return `${data?.mail}` | |||||
| return "dummymail@dilig.net"; | |||||
| }, [data]); | |||||
| return ( | |||||
| <HeaderProfilePopoverContentContainer> | |||||
| <HeaderProfilePopoverContentHeaderContainer> | |||||
| <ProfileInitials to={PAGES.PROFILE.route}>{myInitials}</ProfileInitials> | |||||
| <ProfileDetails> | |||||
| <ProfileName to={PAGES.PROFILE.route}>{myName}</ProfileName> | |||||
| <ProfileMail>{myMail}</ProfileMail> | |||||
| </ProfileDetails> | |||||
| </HeaderProfilePopoverContentHeaderContainer> | |||||
| <HeaderProfileMenu> | |||||
| <HeaderProfileMenuItem>{t("pages.settings")}</HeaderProfileMenuItem> | |||||
| <HeaderProfileMenuItem>{t("common.logout")}</HeaderProfileMenuItem> | |||||
| </HeaderProfileMenu> | |||||
| </HeaderProfilePopoverContentContainer> | |||||
| ); | |||||
| }; | |||||
| HeaderProfilePopoverContent.propTypes = { | |||||
| children: PropTypes.node, | |||||
| }; | |||||
| export default HeaderProfilePopoverContent; |
| import { Box, Typography } from "@mui/material"; | |||||
| import { NavLink } from "react-router-dom"; | |||||
| import styled from "styled-components"; | |||||
| export const HeaderProfilePopoverContentContainer = styled(Box)` | |||||
| width: 250px; | |||||
| padding: 0 8px; | |||||
| padding-bottom: 8px; | |||||
| cursor: default; | |||||
| background-color: ${(props) => props?.theme?.colors?.primaryDark}; | |||||
| border-radius: 8px; | |||||
| `; | |||||
| export const HeaderProfilePopoverContentHeaderContainer = styled(Box)` | |||||
| display: flex; | |||||
| align-items: center; | |||||
| gap: 12px; | |||||
| height: 60px; | |||||
| border-bottom: 1px solid ${(props) => props?.theme?.colors?.secondaryDark}; | |||||
| `; | |||||
| export const ProfileInitials = styled(NavLink)` | |||||
| width: 44px; | |||||
| height: 44px; | |||||
| min-width: 44px; | |||||
| min-height: 44px; | |||||
| border-radius: 100%; | |||||
| text-align: center; | |||||
| vertical-align: middle; | |||||
| line-height: 44px; | |||||
| text-decoration: none; | |||||
| background-color: ${(props) => props?.theme?.colors?.primaryLighter}; | |||||
| `; | |||||
| export const ProfileDetails = styled(Box)` | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| justify-content: space-evenly; | |||||
| height: 100%; | |||||
| padding: 8px 0; | |||||
| `; | |||||
| export const ProfileName = styled(NavLink)` | |||||
| font-family: Inter; | |||||
| text-decoration: none; | |||||
| font-size: 18px; | |||||
| font-weight: 700; | |||||
| color: ${(props) => props?.theme?.colors?.textColor}; | |||||
| `; | |||||
| export const ProfileMail = styled(Typography)` | |||||
| font-family: Inter; | |||||
| font-size: 14px; | |||||
| font-weight: 400; | |||||
| color: ${(props) => props?.theme?.colors?.textColor}; | |||||
| `; | |||||
| export const HeaderProfileMenu = styled(Box)` | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| `; | |||||
| export const HeaderProfileMenuItem = styled(NavLink)` | |||||
| padding: 4px 8px; | |||||
| border-radius: 8px; | |||||
| text-decoration: none; | |||||
| cursor: pointer; | |||||
| color: ${(props) => props?.theme?.colors?.textColor}; | |||||
| &:hover { | |||||
| background-color: ${(props) => props?.theme?.colors?.textColor}; | |||||
| color: ${(props) => props?.theme?.colors?.primaryDark}; | |||||
| } | |||||
| `; |
| {props?.trigger} | {props?.trigger} | ||||
| </PopoverTrigger> | </PopoverTrigger> | ||||
| <Popover | <Popover | ||||
| style={props?.contentContainerStyles} | |||||
| sx={props?.contentContainerStyles} | |||||
| open={isOpened} | open={isOpened} | ||||
| anchorEl={anchorEl} | anchorEl={anchorEl} | ||||
| onClose={togglePopover} | onClose={togglePopover} |
| import { Box } from "@mui/material"; | import { Box } from "@mui/material"; | ||||
| import styled from "styled-components"; | import styled from "styled-components"; | ||||
| export const PopoverContainer = styled(Box)`` | |||||
| export const PopoverTrigger = styled(Box)`` | |||||
| export const PopoverContainer = styled(Box)``; | |||||
| export const PopoverTrigger = styled(Box)``; |
| import { apiSlice } from "features/api/apiSlice"; | |||||
| const notificationTag = "notifications"; | |||||
| export const usersApiSlice = apiSlice.injectEndpoints({ | |||||
| tagTypes: [notificationTag], | |||||
| endpoints: (builder) => ({ | |||||
| myUser: builder.query({ | |||||
| query: () => ({ | |||||
| url: "api/user/me", | |||||
| }), | |||||
| }), | |||||
| notifications: builder.query({ | |||||
| query: () => ({ | |||||
| url: "api/user/me/notifications", | |||||
| }), | |||||
| providesTags: [notificationTag], | |||||
| }), | |||||
| seeNotification: builder.mutation({ | |||||
| query: (notificationId) => ({ | |||||
| url: `api/user/me/notifications/${notificationId}`, | |||||
| method: "PATCH", | |||||
| }), | |||||
| invalidatesTags: [notificationTag], | |||||
| }), | |||||
| }), | |||||
| }); | |||||
| export const { | |||||
| useMyUserQuery, | |||||
| useNotificationsQuery, | |||||
| useSeeNotificationMutation, | |||||
| } = usersApiSlice; |
| select: "Select...", | select: "Select...", | ||||
| none: "None", | none: "None", | ||||
| date: { | date: { | ||||
| range: "{{start}} to {{end}}", | |||||
| range: "{{start}} do {{end}}", | |||||
| }, | }, | ||||
| logout: "Izloguj se", | |||||
| seeMore: "Vidi još", | |||||
| }, | }, | ||||
| notifications: { | |||||
| title: "Obaveštenja", | |||||
| }, | |||||
| pages: { | pages: { | ||||
| home: "Početna", | home: "Početna", | ||||
| login: "Login", | login: "Login", | ||||
| WrongPasswordAccountIsLocked: "Wrong credentials, account is locked", | WrongPasswordAccountIsLocked: "Wrong credentials, account is locked", | ||||
| AccountIsLocked: "Account is locked", | AccountIsLocked: "Account is locked", | ||||
| }, | }, | ||||
| date: { | |||||
| timespan: { | |||||
| yearsAgo: "Pre {{years}} godine", | |||||
| monthsAgo: "Pre {{months}} meseca", | |||||
| daysAgo: "Pre {{days}} dana", | |||||
| hoursAgo: "Pre {{hours}} sata", | |||||
| minutesAgo: "Pre {{minutes}} minuta", | |||||
| secondsAgo: "Pre {{seconds}} sekunde", | |||||
| now: "Upravo sada", | |||||
| }, | |||||
| }, | |||||
| }; | }; |
| import { format } from "date-fns"; | import { format } from "date-fns"; | ||||
| import { enUS } from "date-fns/locale"; | import { enUS } from "date-fns/locale"; | ||||
| import i18next from "i18next"; | |||||
| import i18n from "../i18nt/index"; | |||||
| export function formatDate(date, fmt = "MM/dd/y", locale = enUS) { | export function formatDate(date, fmt = "MM/dd/y", locale = enUS) { | ||||
| const dt = new Date(date); | const dt = new Date(date); | ||||
| export function formatDateRange(dates) { | export function formatDateRange(dates) { | ||||
| const start = formatDate(dates.start); | const start = formatDate(dates.start); | ||||
| const end = formatDate(dates.end); | const end = formatDate(dates.end); | ||||
| return i18next.t("common.date.range", { start, end }); | |||||
| return i18n.t("common.date.range", { start, end }); | |||||
| } | } | ||||
| export const calculateTimeSpan = (date) => { | |||||
| let currentDate = new Date(); | |||||
| let timeElapsed = (currentDate.getTime() - date?.getTime()) / 1000; | |||||
| if (timeElapsed <= 0) { | |||||
| return { | |||||
| yearsPassed: 0, | |||||
| monthsPassed: 0, | |||||
| daysPassed: 0, | |||||
| hoursPassed: 0, | |||||
| minutesPassed: 0, | |||||
| secondsPassed: 0, | |||||
| }; | |||||
| } | |||||
| const yearsPassed = Math.floor(timeElapsed / (60 * 60 * 24 * 365)); | |||||
| timeElapsed -= yearsPassed * (60 * 60 * 24 * 365); | |||||
| const monthsPassed = Math.floor(timeElapsed / (60 * 60 * 24 * 31)); | |||||
| timeElapsed -= monthsPassed * (60 * 60 * 24 * 31); | |||||
| const daysPassed = Math.floor(timeElapsed / (60 * 60 * 24)); | |||||
| timeElapsed -= daysPassed * (60 * 60 * 24); | |||||
| const hoursPassed = Math.floor(timeElapsed / (60 * 60)); | |||||
| timeElapsed -= hoursPassed * (60 * 60); | |||||
| const minutesPassed = Math.floor(timeElapsed / 60); | |||||
| timeElapsed -= minutesPassed * 60; | |||||
| const secondsPassed = Math.floor(timeElapsed); | |||||
| timeElapsed -= secondsPassed; | |||||
| return { | |||||
| yearsPassed, | |||||
| monthsPassed, | |||||
| daysPassed, | |||||
| hoursPassed, | |||||
| minutesPassed, | |||||
| secondsPassed, | |||||
| }; | |||||
| }; | |||||
| export const formatTimeSpan = (date) => { | |||||
| const timespanObject = calculateTimeSpan(date); | |||||
| if (timespanObject.yearsPassed > 0) | |||||
| return i18n.t("date.timespan.yearsAgo", { | |||||
| years: timespanObject.yearsPassed, | |||||
| }); | |||||
| if (timespanObject.monthsPassed > 0) | |||||
| return i18n.t("date.timespan.monthsAgo", { | |||||
| months: timespanObject.monthsPassed, | |||||
| }); | |||||
| if (timespanObject.daysPassed > 0) | |||||
| return i18n.t("date.timespan.daysAgo", { | |||||
| days: timespanObject.daysPassed, | |||||
| }); | |||||
| if (timespanObject.hoursPassed > 0) | |||||
| return i18n.t("date.timespan.hoursAgo", { | |||||
| hours: timespanObject.hoursPassed, | |||||
| }); | |||||
| if (timespanObject.minutesPassed > 0) | |||||
| return i18n.t("date.timespan.minutesAgo", { | |||||
| minutes: timespanObject.minutesPassed, | |||||
| }); | |||||
| if (timespanObject.secondsPassed > 0) | |||||
| return i18n.t("date.timespan.secondsAgo", { | |||||
| seconds: timespanObject.secondsPassed, | |||||
| }); | |||||
| return i18n.t("date.timespan.now") | |||||
| }; |