| @@ -13,7 +13,12 @@ const HeaderNotifications = () => { | |||
| <NotificationsIcon /> | |||
| </HeaderIconContainer> | |||
| } | |||
| contentContainerStyles={{ borderRadius: "8px" }} | |||
| contentContainerStyles={{ | |||
| "& .MuiPopover-paper": { | |||
| borderRadius: "6px", | |||
| backgroundColor: "transparent", | |||
| }, | |||
| }} | |||
| content={<HeaderNotificationsContent />} | |||
| popoverProps={{ | |||
| anchorOrigin: { | |||
| @@ -1,4 +1,4 @@ | |||
| import React from "react"; | |||
| import React, { useMemo } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| HeaderLatestNotificationsContainer, | |||
| @@ -8,23 +8,45 @@ import { | |||
| HeaderNotificationsContentHeaderTitle, | |||
| } from "./HeaderNotificationsContent.styled"; | |||
| 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 ( | |||
| <HeaderNotificationsContentContainer> | |||
| <HeaderNotificationsContentHeader> | |||
| <HeaderNotificationsContentHeaderTitle> | |||
| Notifications | |||
| {t("notifications.title")} | |||
| </HeaderNotificationsContentHeaderTitle> | |||
| <HeaderNotificationsContentHeaderSeeMore> | |||
| See more | |||
| {t("common.seeMore")} | |||
| </HeaderNotificationsContentHeaderSeeMore> | |||
| </HeaderNotificationsContentHeader> | |||
| <HeaderLatestNotificationsContainer> | |||
| <SingleNotification /> | |||
| <SingleNotification /> | |||
| <SingleNotification /> | |||
| {latestNotifications?.map?.((singleNotification, index) => ( | |||
| <SingleNotification | |||
| key={index} | |||
| notificationObject={singleNotification} | |||
| /> | |||
| ))} | |||
| </HeaderLatestNotificationsContainer> | |||
| </HeaderNotificationsContentContainer> | |||
| ); | |||
| @@ -1,4 +1,4 @@ | |||
| import React from "react"; | |||
| import React, { useEffect, useMemo } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| SingleNotificationContainer, | |||
| @@ -8,24 +8,39 @@ import { | |||
| SingleNotificationProfile, | |||
| SingleNotificationUnseen, | |||
| } 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 ( | |||
| <SingleNotificationContainer> | |||
| <SingleNotificationContainer onClick={handleSeeNotification}> | |||
| <SingleNotificationProfile> | |||
| <AccountCircleIcon /> | |||
| {props?.notificationObject?.userPicture} | |||
| </SingleNotificationProfile> | |||
| <SingleNotificationDetails> | |||
| <SingleNotificationContent> | |||
| <p> | |||
| <i>Olivia Saturday</i> commented on your | |||
| post. | |||
| </p> | |||
| {props?.notificationObject?.notificationText} | |||
| </SingleNotificationContent> | |||
| <SingleNotificationDate> | |||
| 12 hours ago | |||
| </SingleNotificationDate> | |||
| <SingleNotificationDate>{timespan}</SingleNotificationDate> | |||
| </SingleNotificationDetails> | |||
| <SingleNotificationUnseen /> | |||
| </SingleNotificationContainer> | |||
| @@ -34,6 +49,11 @@ const SingleNotification = () => { | |||
| SingleNotification.propTypes = { | |||
| children: PropTypes.node, | |||
| notificationObject: PropTypes.shape({ | |||
| userPicture: PropTypes.oneOfType([PropTypes.element, PropTypes.node]), | |||
| notificationText: PropTypes.node, | |||
| date: PropTypes.object, | |||
| }), | |||
| }; | |||
| export default SingleNotification; | |||
| @@ -4,8 +4,29 @@ import PropTypes from "prop-types"; | |||
| import { HeaderProfileContainer } from "./HeaderProfile.styled"; | |||
| import PersonIcon from "@mui/icons-material/Person"; | |||
| 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 = () => { | |||
| 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 = { | |||
| @@ -1,4 +1,8 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| export const HeaderProfileContainer = styled(Box)`` | |||
| export const HeaderProfileContainer = styled(Box)` | |||
| & .MuiPopover-paper { | |||
| border-radius: 8px; | |||
| } | |||
| ` | |||
| @@ -0,0 +1,56 @@ | |||
| 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; | |||
| @@ -0,0 +1,66 @@ | |||
| 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}; | |||
| } | |||
| `; | |||
| @@ -20,7 +20,7 @@ const PopoverComponent = (props) => { | |||
| {props?.trigger} | |||
| </PopoverTrigger> | |||
| <Popover | |||
| style={props?.contentContainerStyles} | |||
| sx={props?.contentContainerStyles} | |||
| open={isOpened} | |||
| anchorEl={anchorEl} | |||
| onClose={togglePopover} | |||
| @@ -1,5 +1,5 @@ | |||
| import { Box } from "@mui/material"; | |||
| 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)``; | |||
| @@ -0,0 +1,33 @@ | |||
| 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; | |||
| @@ -40,10 +40,15 @@ export default { | |||
| select: "Select...", | |||
| none: "None", | |||
| date: { | |||
| range: "{{start}} to {{end}}", | |||
| range: "{{start}} do {{end}}", | |||
| }, | |||
| logout: "Izloguj se", | |||
| seeMore: "Vidi još", | |||
| }, | |||
| notifications: { | |||
| title: "Obaveštenja", | |||
| }, | |||
| pages: { | |||
| home: "Početna", | |||
| login: "Login", | |||
| @@ -116,4 +121,15 @@ export default { | |||
| WrongPasswordAccountIsLocked: "Wrong credentials, 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", | |||
| }, | |||
| }, | |||
| }; | |||
| @@ -1,6 +1,6 @@ | |||
| import { format } from "date-fns"; | |||
| 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) { | |||
| const dt = new Date(date); | |||
| @@ -36,5 +36,76 @@ export function formatDateTimeLocale(date) { | |||
| export function formatDateRange(dates) { | |||
| const start = formatDate(dates.start); | |||
| 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") | |||
| }; | |||