Skip to content

Commit

Permalink
Redesigning notification slider
Browse files Browse the repository at this point in the history
  • Loading branch information
Civolilah committed Feb 10, 2025
1 parent 17f71b0 commit ce396d8
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 42 deletions.
30 changes: 30 additions & 0 deletions src/common/hooks/useReplaceTranslationVariables.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/

import reactStringReplace from 'react-string-replace';
import { ReactNode } from 'react';

interface Replacements {
[key: string]: ReactNode;
}

export function useReplaceVariables() {
return (text: string, replacements: Replacements): ReactNode => {
let result = text;

for (const [variable, value] of Object.entries(replacements)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
result = reactStringReplace(result, `:${variable}`, () => value);
}

return result;
};
}
266 changes: 231 additions & 35 deletions src/components/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@
* @license https://www.elastic.co/licensing/elastic-license
*/

import { Bell, Trash } from 'react-feather';
import { Bell } from 'react-feather';
import { Slider } from './cards/Slider';
import { useEffect, useState } from 'react';
import { ReactNode, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { atomWithStorage } from 'jotai/utils';
import { useAtom } from 'jotai';
import { GenericMessage, useSocketEvent } from '$app/common/queries/sockets';
import { Invoice } from '$app/common/interfaces/invoice';
import { route } from '$app/common/helpers/route';
import { ClickableElement } from './cards';
import { date, isHosted, isSelfHosted, trans } from '$app/common/helpers';
import { useCurrentCompanyDateFormats } from '$app/common/hooks/useCurrentCompanyDateFormats';
import { NonClickableElement } from './cards/NonClickableElement';
Expand All @@ -30,12 +29,17 @@ import { useSockets } from '$app/common/hooks/useSockets';
import { useReactSettings } from '$app/common/hooks/useReactSettings';
import { Icon } from './icons/Icon';
import { useColorScheme } from '$app/common/colors';

import { Button, Link } from './forms';
import { useReplaceVariables } from '$app/common/hooks/useReplaceTranslationVariables';
import { CardCheck } from './icons/CardCheck';
import { GoDotFill } from 'react-icons/go';
export interface Notification {
label: string;
displayLabel: ReactNode;
date: string;
link: string | null;
readAt: string | null;
icon?: ReactNode;
}

export const notificationsAtom = atomWithStorage<Notification[]>(
Expand All @@ -48,6 +52,8 @@ export function Notifications() {

const { timeFormat } = useCompanyTimeFormat();

const replaceVariables = useReplaceVariables();

const [isVisible, setIsVisible] = useState(false);
const [notifications, setNotifications] = useAtom(notificationsAtom);

Expand All @@ -68,6 +74,22 @@ export function Notifications() {

const notification = {
label: `${$invoice.number}: ${t('invoice_paid')}`,
displayLabel: replaceVariables(
t('notification_invoice_paid_subject') as string,
{
invoice: (
<Link to={route('/invoices/:id/edit', { id: $invoice.id })}>
{`#${$invoice.number}`}
</Link>
),

client: (
<Link to={route('/clients/:id', { id: $invoice.client_id })}>
{$invoice.client?.display_name}
</Link>
),
}
),
date: new Date().toString(),
link: route('/invoices/:id/edit', { id: $invoice.id }),
readAt: null,
Expand Down Expand Up @@ -98,6 +120,22 @@ export function Notifications() {
invoice: $invoice.number,
client: $invoice.client?.display_name,
}),
displayLabel: replaceVariables(
t('notification_invoice_viewed_subject') as string,
{
invoice: (
<Link to={route('/invoices/:id/edit', { id: $invoice.id })}>
{`#${$invoice.number}`}
</Link>
),

client: (
<Link to={route('/clients/:id', { id: $invoice.client_id })}>
{$invoice.client?.display_name}
</Link>
),
}
),
date: new Date().toString(),
link: route('/invoices/:id/edit', { id: $invoice.id }),
readAt: null,
Expand All @@ -117,7 +155,26 @@ export function Notifications() {
const $credit = data as Credit;

const notification = {
label: `${t('credit_created')}: ${$credit.number}`,
label: trans('notification_credit_created_subject', {
invoice: $credit.number,
client: $credit.client?.display_name,
}),
displayLabel: replaceVariables(
t('notification_invoice_viewed_subject') as string,
{
invoice: (
<Link to={route('/credits/:id/edit', { id: $credit.id })}>
{`#${$credit.number}`}
</Link>
),

client: (
<Link to={route('/clients/:id', { id: $credit.client_id })}>
{$credit.client?.display_name}
</Link>
),
}
),
date: new Date().toString(),
link: route('/credits/:id/edit', { id: $credit.id }),
readAt: null,
Expand All @@ -138,6 +195,15 @@ export function Notifications() {

const notification = {
label: `${t('credit_updated')}: ${$credit.number}`,
displayLabel: (
<div className="flex items-center space-x-1">
<span>{t('credit_updated')}:</span>

<Link to={route('/credits/:id/edit', { id: $credit.id })}>
{`#${$credit.number}`}
</Link>
</div>
),
date: new Date().toString(),
link: route('/credits/:id/edit', { id: $credit.id }),
readAt: null,
Expand All @@ -158,6 +224,15 @@ export function Notifications() {

const notification = {
label: `${t('payment_updated')}: ${payment.number}`,
displayLabel: (
<div className="flex items-center space-x-1">
<span>{t('payment_updated')}:</span>

<Link to={route('/payments/:id/edit', { id: payment.id })}>
{`#${payment.number}`}
</Link>
</div>
),
date: new Date().toString(),
link: route('/payments/:id/edit', { id: payment.id }),
readAt: null,
Expand All @@ -179,6 +254,116 @@ export function Notifications() {
const dateFormat = useCurrentCompanyDateFormats();
const reactSettings = useReactSettings();

const randomNotifications: Notification[] = [
{
label: 'Invoice #1001 paid by Acme Corp',
displayLabel: replaceVariables(
t('notification_invoice_paid_subject') as string,
{
invoice: (
<Link to={route('/invoices/:id/edit', { id: '1' })}>#INV-1001</Link>
),
client: (
<Link to={route('/clients/:id', { id: '1' })}>Acme Corp</Link>
),
}
),
date: new Date(Date.now() - 1800000).toString(),
link: route('/invoices/:id/edit', { id: '1' }),
readAt: null,
icon: (
<div
className="p-2 rounded-full"
style={{ backgroundColor: colors.$5 }}
>
<CardCheck size="1.3rem" color={colors.$8} />
</div>
),
},
{
label: 'Invoice #1002 viewed by TechStart Ltd',
displayLabel: replaceVariables(
t('notification_invoice_viewed_subject') as string,
{
invoice: (
<Link to={route('/invoices/:id/edit', { id: '2' })}>#INV-1002</Link>
),
client: (
<Link to={route('/clients/:id', { id: '2' })}>TechStart Ltd</Link>
),
}
),
date: new Date(Date.now() - 3600000).toString(),
link: route('/invoices/:id/edit', { id: '2' }),
readAt: new Date(Date.now() - 1800000).toString(),
icon: (
<div
className="p-2 rounded-full"
style={{ backgroundColor: colors.$5 }}
>
<CardCheck size="1.3rem" color={colors.$8} />
</div>
),
},
{
label: 'Credit note #CRD-001 created for Global Services Inc',
displayLabel: replaceVariables(
t('notification_credit_created_subject') as string,
{
invoice: (
<Link to={route('/credits/:id/edit', { id: '1' })}>#CRD-001</Link>
),
client: (
<Link to={route('/clients/:id', { id: '3' })}>
Global Services Inc
</Link>
),
}
),
date: new Date(Date.now() - 7200000).toString(),
link: route('/credits/:id/edit', { id: '1' }),
readAt: new Date(Date.now() - 3600000).toString(),
},
{
label: 'Credit updated: #CRD-002',
displayLabel: (
<div className="flex items-center space-x-1">
<span>{t('credit_updated')}:</span>
<Link to={route('/credits/:id/edit', { id: '2' })}>#CRD-002</Link>
</div>
),
date: new Date(Date.now() - 86400000).toString(),
link: route('/credits/:id/edit', { id: '2' }),
readAt: new Date(Date.now() - 43200000).toString(),
},
{
label: 'Payment updated: #PAY-001',
displayLabel: (
<div className="flex items-center space-x-1">
<span>{t('payment_updated')}:</span>
<Link to={route('/payments/:id/edit', { id: '1' })}>#PAY-001</Link>
</div>
),
date: new Date(Date.now() - 172800000).toString(),
link: route('/payments/:id/edit', { id: '1' }),
readAt: null,
},
{
label: 'Payment updated: #PAY-002',
displayLabel: (
<div className="flex items-center space-x-1">
<span>{t('payment_updated')}:</span>
<Link to={route('/payments/:id/edit', { id: '2' })}>#PAY-002</Link>
</div>
),
date: new Date(Date.now() - 259200000).toString(),
link: route('/payments/:id/edit', { id: '2' }),
readAt: new Date(Date.now() - 172800000).toString(),
},
];

console.log(randomNotifications);

useEffect(() => {
if (
isSelfHosted() &&
Expand All @@ -196,6 +381,7 @@ export function Notifications() {
(message: GenericMessage) => {
const notification = {
label: message.message,
displayLabel: message.message,
date: new Date().toString(),
link: message.link,
readAt: null,
Expand Down Expand Up @@ -241,42 +427,52 @@ export function Notifications() {
size="regular"
title={t('notifications')!}
topRight={
<button type="button" onClick={() => setNotifications([])}>
<Trash size={18} />
</button>
<Button
type="minimal"
behavior="button"
className="rounded-md"
onClick={() => setNotifications([])}
>
{t('clear')}
</Button>
}
withoutDivider
>
{notifications.map((notification, i) =>
notification.link ? (
<ClickableElement key={i} to={notification.link}>
<div>
<p>{notification.label}</p>
<p className="text-xs">
{date(
notification.date,
`${dateFormat.dateFormat} ${timeFormat}`
)}
</p>
</div>
</ClickableElement>
) : (
<NonClickableElement key={i}>
<p>{notification.label}</p>
<p className="text-xs">
{date(
notification.date,
`${dateFormat.dateFormat} ${timeFormat}`
{randomNotifications.length > 0 ? (
<div className="flex flex-col space-y-2 pt-2">
{randomNotifications.map((notification, i) => (
<div
key={i}
className="flex items-center justify-between px-6 py-2 space-x-2"
>
<div className="flex items-center space-x-2">
{notification.icon}

<div className="flex flex-col space-y-1">
<div className="text-sm">{notification.displayLabel}</div>

<p className="text-xs text-gray-500">
{date(
notification.date,
`${dateFormat.dateFormat} ${timeFormat}`
)}
</p>
</div>
</div>

{!notification.readAt && (
<div>
<Icon element={GoDotFill} size={14} color="#2176FF" />
</div>
)}
</p>
</NonClickableElement>
)
)}

{notifications.length === 0 ? (
</div>
))}
</div>
) : (
<NonClickableElement>
{t('no_unread_notifications')}
</NonClickableElement>
) : null}
)}
</Slider>
</>
);
Expand Down
Loading

0 comments on commit ce396d8

Please sign in to comment.