Frontend
Pre-requisites
The following section assumes you have installed all the tools required for web development from our tools section.
Before you move on, it is also mandatory that you read through the key technology list in our web technologies section and you understand the justification for each one, especially if you're not familiar with the specific technology.
It is presumed that you already have a Web Base
prepared and ready for you install modlets, otherwise, please go to the
Web Framework quickstart section to initialize your
Web Base
.
It is also presumed that you are already familiar with running a local development environment, otherwise, please go to
the Local Dev Environment to prepare your machine for local
development. Using dev-containers
with IAN module enabled is recommended.
Installation Guide
1. Installing IAN package
The internal CLI tool
built by AOH will be used to install the ian package. If you are using one of the latest versions of the Web base, the CLI tool is already part of the web base, and you can verify by checking if your package.json has the following:
"@mssfoobar/cli": "^2.2.1"
Do note that your CLI version may differ with the one above, as the one above is the latest version as of the time of writing this documentation.
The CLI tool only works if you did not remove the aoh folder within the src/lib folder in the original web base. This is because it will install the ian package and duplicate them into the src/lib/aoh folder.
Run the following CLI command in the terminal inside your web directory.
npx cli install @mssfoobar/ian-web
You will be prompted to enter the path to your root folder, although the tool will attempt to auto-generate that path for you. Do check if it is accurate. If not, please enter the correct path to your root folder.
Afterwards, simply click enter, and the tool will start to install the ian package.
Once completed, you should see new files being generated in the src/lib/aoh/ian
folder, as well as the routes/(private)/aoh/ian
folder.
├── src
│ └── lib
│ │ └── aoh
│ │ ├── core
│ │ ... └── ian
│ │ ├── api
│ │ ├── assets
│ │ ├── components
│ │ ├── stores
│ │ ├── types
│ │ └── utils
│ │
│
└── routes
├── (private)
│ ├── example
│ │
│ │── aoh
│ │ └── ian
│ │ └── api
│ │ └── messages
...
2. Install devDependencies
Add these dependencies below into the devDependencies
object in your package.json
, and save the file.
"svelte-sonner": "^0.3.28",
"@iconify-json/lucide": "^1.2.26",
3. Install dependencies
Add the dependencies below into the dependencies
object in your package.json
, and save the file. Afterwards, run npm i
or npm install
to install.
"@mssfoobar/sse-client": "^1.1.0",
"iconify-icon": "^2.3.0",
4. Configuration
In the tailwind.config.ts file, add "material-symbols in the addIconSelectors plugin:
plugins: [addIconSelectors(["mdi", "mdi-light", "lucide"]),...],
Usage
1. Update Headerbar
The following steps assume you are putting the Notification Badge
, along with the Dropdown Menu
, within the default web base headerbar. If you are putting it within your own custom header bar, please edit in the correct file respectively.
Go to the Headerbar component you're using and import the component.
import NotificationContainer from "$lib/aoh/ian/components/NotificationContainer/index.svelte";
Update the Headerbar
<header class="...">
... ...
<NotificationContainer />
</header>
The NotificationContainer is a container that containers both the notification button and the dropdown menu.
Using shadcn-svelte's dropdown menu component, the notification button is nested within the DropdownMenu.Trigger element, which allows the dropdown menu to expand when the notification button is clicked.
For more information, visit shadcn-svelte's dropdown menu component documentation.
2. Update +layout.svelte
Go to the +layout.svelte
file found in the (private) folder and import the necessary modules.
import { onMount, onDestroy } from "svelte";
import {
notificationStore,
updateNotificationStore,
} from "$lib/aoh/ian/stores/notificationStore";
import { SSESubscribeClient } from "@mssfoobar/sse-client";
import type { Message } from "$lib/aoh/ian/types/types";
import { env } from "$env/dynamic/public";
import { CustomToast } from "$lib/aoh/ian/components/toast";
import { toast } from "svelte-sonner";
import { Toaster } from "$lib/aoh/ian/components/ui/sonner";
import { updateMessageStatus } from "$lib/aoh/ian/api/notifications";
import toastNotificationSound from "$lib/aoh/ian/assets/sounds/toastNotification.mp3";
Include the following code block below the let tenant: Tenant = $derived(data.user?.active_tenant);
line:
const baseSehUrl = env.PUBLIC_RTUS_SEH_URL;
let unsubscribe: () => void;
// expected structure of data received from RTUS SEH Service
interface IanSSEData {
unread_count: number;
message: Message;
}
let sseClient: SSESubscribeClient<IanSSEData>;
function initiateIanClient() {
if (data.quickAccessData && data.unreadCount !== undefined) {
updateNotificationStore(data.quickAccessData, data.unreadCount, true);
}
if (data.user?.active_tenant?.tenant_id && data.user.sub) {
const domainURL = `${baseSehUrl}`;
const tenantId = data.user.active_tenant.tenant_id;
const mapName = "ian";
const userId = data.user.sub;
/* Configure and connect to the SSE Client
- domainURL, tenantId, mapName, userId, and init are required fields from RTUS SEH
- if init is set to true, RTUS SEH sends initial stored values when the connection is established
- eventHandlers are to allow the application to respond to the different custom events created by the RTUS SEH.
*/
sseClient = new SSESubscribeClient({
domainURL,
tenantId,
mapName,
userId,
init: false,
maxReconnectAttempts: 10,
baseReconnectDelay: 1000,
eventHandlers: {
Added: (data) => {
const { unread_count, message } = data;
updateNotificationStore([message], unread_count, false, "sse");
},
Updated: (data) => {
const { unread_count, message } = data;
updateNotificationStore([message], unread_count, false, "sse");
},
},
});
sseClient.connect();
}
unsubscribe = notificationStore.subscribe((state) => {
if (state.notificationData) {
const { ref_link, ...otherProps } = state.notificationData;
//@ts-expect-error toast expects a string or ComponentType
toast(CustomToast, {
classes: {
toast: "dark:border-stone-700 min-w-[250px] w-auto bg-background",
closeButton: "bg-popover opacity-0 group-hover:opacity-100",
},
componentProps: {
...otherProps,
ref_link,
icon: state.notificationData.icon_id,
onClick: () => {
if (ref_link) {
updateMessageStatus(state.notificationData?.message_id);
window.location.href = ref_link;
}
},
},
duration: 5000,
});
const toastAudio = new Audio(toastNotificationSound);
toastAudio.play();
notificationStore.update((store) => ({
...store,
notificationData: null,
}));
}
});
}
onMount(() => {
tenant = data.user?.active_tenant;
// Initiate SSEClient
initiateIanClient();
});
onDestroy(() => {
if (unsubscribe) unsubscribe();
if (sseClient) {
sseClient.disconnect();
}
});
Include the <toaster>
component within the <AuthProvider>
component.
<AuthProvider claims={...}>
...
<Toaster position="top-right" class="flex justify-end" expand={true} closeButton />
</div>
</div>
</AuthProvider>
3. Update +layout.server.ts
The +layout.server.ts allows your load function to run on the server, for tasks like fetching data from database, or accessing private environment variables.
The data fetching happens before rendering, and will be passed to the Svelte components as props. Therefore, we are fetching data in the +layout.server.ts to populate the unread notification counter on the notification badge
, as well as the dropdown menu
data, on page load.
For more information regarding +layout.server.ts, visit Svelte's official tutorial.
Copy the code below into the +layout.server.ts file within the (private) folder.
import { env } from "$env/dynamic/public";
Copy only the highlighted lines.
export async function load( {locals} ) {
const authResult = locals.authResult;
let user: AuthClaims | undefined;
if ("claims" in authResult && authResult.success) {
user = authResult.claims;
}
.
.
if (authResult.success) {
..
let quickAccessData = { data: [] };
let unreadCountData = { data: { total: 0 } };
try {
const quickAccessResponse = await fetch(
`${baseUrl}/v1/users/${user?.sub}/quick-access?sort=created_at,desc`
);
if (!quickAccessResponse.ok) {
throw new Error("Failed to fetch quick access data");
}
quickAccessData = await quickAccessResponse.json();
} catch (error) {
log.error("Error fetching quick-access data:", error);
}
try {
const unreadCountResponse = await fetch(`${baseUrl}/v1/users/${user?.sub}/messages/unread-count`);
if (!unreadCountResponse.ok) {
throw new Error("Failed to fetch unreadCount data");
}
unreadCountData = await unreadCountResponse.json();
} catch (error) {
log.error("Error fetching unread count data:", error);
}
return {
user: authResult.claims,
quickAccessData: quickAccessData.data,
unreadCount: unreadCountData.data.total,
};
}
API Call for Testing
To ensure that everything works smoothly, we can call the ian backend service API endpoint, with the payload below via Postman. This assumes that your ian backend service is already deployed and running.
{
"title": "Trip to Singapore",
"body": "Day Trip to Singapore",
"sender_id": "SENDER UUID HERE",
"receiver_ids": ["RECEIVER UUID HERE"], // list uuid
"tenant_id": "TENANT UUID HERE", // uuid
"icon_id": "lucide:bell", // string
"ref_link": "link that you want the user to be redirected to when clicked"
}
The icon_id
field should be the name of an icon from the Iconify collections and must follow the format <set-name>:<icon-name>
(e.g., lucide:bell
).