Skip to content

Commit

Permalink
latest working component <NotificationTray />
Browse files Browse the repository at this point in the history
  • Loading branch information
Temuulen Bayanmunkh authored and Temuulen Bayanmunkh committed Oct 20, 2023
1 parent 4f83cc5 commit 3af4b90
Show file tree
Hide file tree
Showing 11 changed files with 52,628 additions and 2 deletions.
Binary file added .DS_Store
Binary file not shown.
14 changes: 12 additions & 2 deletions addon/components/layout/header.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<LinkToExternal @route="console" class="logo navbar-logo mr-4">
<LogoIcon @brand={{@brand}} @size="8" />
</LinkToExternal>
{{#unless (media 'isMobile')}}
{{#unless (media "isMobile")}}
<div role="menu" class="next-catalog-menu-items flex mr-4">
{{#each @menuItems as |menuItem|}}
<LinkToExternal @route={{menuItem.route}} class="next-view-header-item {{menuItem.class}}" role="menuitem">
Expand Down Expand Up @@ -33,6 +33,9 @@
<Layout::Header::LoadingIndicator />
<div id="view-header-actions"></div>
<div class="flex items-center justify-between">
<div class="flex-1 flex items-center pr-1">
<NotificationTray />
</div>
<div class="flex-1 flex items-center pr-1">
<Layout::Header::Dropdown @items={{this.organizationNavigationItems}} @onAction={{@onAction}} class="flex-shrink-0" @triggerClass="flex-shrink-0" as |dd|>
<div class="next-org-button-trigger flex-shrink-0 {{if dd.isOpen 'is-open'}}">
Expand All @@ -46,7 +49,14 @@
<div class="flex-1 flex items-center justify-end">
<Layout::Header::Dropdown @items={{this.userNavigationItems}} @onAction={{@onAction}} class="flex-shrink-0" @triggerClass="flex-shrink-0" as |dd|>
<div class="next-user-button-trigger flex-shrink-0 {{if dd.isOpen 'is-open'}}">
<Image class="rounded-full h-5 w-5 shadow-sm flex-shrink-0" height="20" width="20" src={{@user.avatar_url}} @fallbackSrc="https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png" alt={{@user.name}} />
<Image
class="rounded-full h-5 w-5 shadow-sm flex-shrink-0"
height="20"
width="20"
src={{@user.avatar_url}}
@fallbackSrc="https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png"
alt={{@user.name}}
/>
</div>
</Layout::Header::Dropdown>
</div>
Expand Down
32 changes: 32 additions & 0 deletions addon/components/notification-tray.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<BasicDropdown @renderInPlace={{true}} @registerAPI={{this.registerAPI}} class="notification-tray" ...attributes as |dd|>
<dd.Trigger class="btn btn-default btn-xs">
<FaIcon @icon="inbox" />
{{#if this.notifications.length}}
<div class="notification-tray-unread-notifications-badge">{{this.notifications.length}}</div>
{{/if}}
</dd.Trigger>
<dd.Content class="notification-tray-panel-container">
<div class="notification-tray-panel">
<div class="px-4">
<h1 class="mb-2 text-black dark:text-gray-100 text-base font-semibold pt-2 px-1">Unread Notifications</h1>
<div class="h-48 overflow-y-scroll px-1">
{{#each this.notifications as |notification|}}
<a href="javascript:;" class="notification-item" {{on "click" (fn this.onClickNotification notification)}}>
<h3 class="font-semibold text-small">{{notification.data.subject}}</h3>
<p class="text-xs mb-1.5">{{notification.data.message}}</p>
<span class="text-xs">Received: {{notification.createdAgo}}</span>
</a>
{{else}}
<div class="flex flex-1 items-center justify-center w-full h-full">
<span class="text-base text-gray-800 dark:text-gray-300 italic">No unread notifications</span>
</div>
{{/each}}
</div>
</div>
<div class="px-2 py-1.5 border-t border-gray-200 dark:border-gray-700 flex flex-row space-x-4">
<a href="javascript:;" class="notification-tray-view-all-link" {{on "click" this.onPressViewAllNotifications}}>View all notification</a>
</div>
</div>

</dd.Content>
</BasicDropdown>
223 changes: 223 additions & 0 deletions addon/components/notification-tray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { isArray } from '@ember/array';
import { action } from '@ember/object';

/**
* NotificationTrayComponent is a Glimmer component for handling notifications.
*
* @class NotificationTrayComponent
* @extends Component
*/
export default class NotificationTrayComponent extends Component {
/**
* Inject the `socket` service.
*
* @memberof NotificationTrayComponent
* @type {SocketService}
*/
@service socket;

/**
* Inject the `store` service.
*
* @memberof NotificationTrayComponent
* @type {StoreService}
*/
@service store;

/**
* Inject the `fetch` service.
*
* @memberof NotificationTrayComponent
* @type {FetchService}
*/
@service fetch;

/**
* Inject the `currentUser` service.
*
* @memberof NotificationTrayComponent
* @type {CurrentUserService}
*/
@service currentUser;

/**
* An array to store notifications.
*
* @memberof NotificationTrayComponent
* @type {Array}
*/
@tracked notifications = [];

/**
* A boolean to track whether the notification tray is open or closed.
*
* @memberof NotificationTrayComponent
* @type {boolean}
*/
@tracked isOpen = false;

/**
* A reference to the notification sound.
*
* @memberof NotificationTrayComponent
* @type {Audio}
*/
notificationSound = new Audio('/sounds/notification-sound.mp3');

/**
* Creates an instance of the NotificationTrayComponent
*/
constructor() {
super(...arguments);
this.listenForNotificationFrom(`user.${this.currentUser.id}`);
this.listenForNotificationFrom(`company.${this.currentUser.companyId}`);
this.fetchNotificationsFromStore();

if (typeof this.args.onInitialize === 'function') {
this.args.onInitialize(this.context);
}
}

/**
* Listens for notifications from a specific channel.
*
* @param {string} channelId - The channel to listen to.
* @memberof NotificationTrayComponent
*/
async listenForNotificationFrom(channelId) {
// setup socket
const socket = this.socket.instance();

// listen on company channel
const channel = socket.subscribe(channelId);

// listen to channel for events
await channel.listener('subscribe').once();

// get incoming data and console out
(async () => {
for await (let incomingNotification of channel) {
this.onReceivedNotification(incomingNotification);
}
})();
}

/**
* Handles a received notification by fetching the notification record and processing it.
*
* @param {Object} notificationData - The received notification data.
* @param {string} notificationData.id - The unique identifier of the notification.
* @returns {Promise} A promise that resolves after processing the notification.
* @memberof NotificationTrayComponent
*/
onReceivedNotification({ id }) {
return this.getNotificationRecordUsingId(id).then((notification) => {
// add to notifications array
this.insertNotifications(notification);

// trigger notification sound
this.ping();

// handle callback
if (typeof this.args.onReceivedNotification === 'function') {
this.args.onReceivedNotification(notification);
}
});
}

/**
* Inserts one or more notifications into the notifications array, ensuring uniqueness.
*
* @param {Array|Object} notifications - The notification(s) to insert into the array.
* @memberof NotificationTrayComponent
*/
insertNotifications(notifications) {
let _notifications = [...this.notifications];

if (isArray(notifications)) {
_notifications.pushObjects(notifications);
} else {
_notifications.pushObject(notifications);
}

this.notifications = _notifications.filter(({ read_at }) => !read_at).uniqBy('id');
}

/**
* Fetches a notification record using its unique identifier.
*
* @param {string} id - The unique identifier of the notification.
* @returns {Promise} A promise that resolves with the notification record.
* @memberof NotificationTrayComponent
*/
getNotificationRecordUsingId(id) {
return this.store.findRecord('notification', id);
}

/**
* Fetches notifications from the store.
*
* @memberof NotificationTrayComponent
*/
fetchNotificationsFromStore() {
this.store.query('notification', { sort: '-created_at', limit: 20, unread: true }).then((notifications) => {
this.insertNotifications(notifications);

if (typeof this.args.onNotificationsLoaded === 'function') {
this.args.onNotificationsLoaded(notifications);
}
});
}

/**
* Handles the click event on a notification.
*
* @param {NotificationModel} notification - The clicked notification.
* @returns {Promise} A promise that resolves after marking the notification as read.
* @memberof NotificationTrayComponent
*/
@action onClickNotification(notification) {
notification.set('read_at', new Date());
return notification.save().then(() => {
this.notifications.removeObject(notification);
});
}

/**
* Registers the dropdown API.
*
* @param {DropdownApi} dropdownApi - The dropdown API instance.
* @memberof NotificationTrayComponent
*/
@action registerAPI(dropdownApi) {
this.dropdownApi = dropdownApi;

if (typeof this.args.registerAPI === 'function') {
this.args.registerAPI(...arguments);
}
}

/**
* Handler for when "View all notifications" link is pressed in footer
*
* @returns {void}
* @memberof NotificationTrayComponent
*/
@action onPressViewAllNotifications() {
if (typeof this.args.onPressViewAllNotifications === 'function') {
this.args.onPressViewAllNotifications();
}
}

/**
* Plays the notification sound.
*
* @memberof NotificationTrayComponent
*/
ping() {
this.notificationSound.play();
}
}
1 change: 1 addition & 0 deletions addon/styles/addon.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
@import 'components/date-picker.css';
@import 'components/dashboard.css';
@import 'components/kanban.css';
@import 'components/notification-tray.css';

/** Third party */
@import 'air-datepicker/air-datepicker.css';
57 changes: 57 additions & 0 deletions addon/styles/components/notification-tray.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* Default styling for the notification tray container when it's closed */
.notification-tray {
position: relative;
display: flex;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* Styling for the notification tray container when it's open in the light theme */
[data-theme='light'] .notification-tray.is-open {
background-color: #ffffff;
color: #000000;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* Styling for the notification tray container when it's open in the dark theme */
[data-theme='dark'] .notification-tray.is-open {
background-color: #333333;
color: #ffffff;
box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1); /* Customize for the dark theme */
}

/* Styling for the notification tray badge */
.notification-tray-unread-notifications-badge {
position: absolute;
top: -6px;
right: -6px;
width: 16px;
height: 16px;
background: red;
color: white;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
}

/* Adding a hover effect for the badge */
.notification-tray-unread-notifications-badge:hover {
background-color: darkred;
cursor: pointer;
}

.notification-tray-panel {
@apply mt-5 bg-white border border-gray-200 shadow-md rounded-lg dark:bg-gray-900 dark:border-gray-700 space-y-2 truncate;
width: 300px;
}

.notification-tray-panel .notification-item {
@apply flex flex-col rounded-md px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 truncate;
}

.notification-tray-view-all-link {
@apply rounded px-2 py-1 text-sm text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-moregray-750;
}
1 change: 1 addition & 0 deletions app/components/notification-tray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/ember-ui/components/notification-tray';
Binary file added assets/.DS_Store
Binary file not shown.
Binary file added assets/sounds/notification-sound.mp3
Binary file not shown.
Loading

0 comments on commit 3af4b90

Please sign in to comment.