Frontend - liferesearchapp/life-research-members-portal GitHub Wiki

Overview

This document relates to all files within the src directory, excluding those covered in Backend and Authentication. Please see those sections before reading this one, as some of the concepts discussed are used here.

The Ant Design UI library was used for some pre-made components.

Services

This section relates to the files within src/services.

These files contain functions for communicating with the backend. These functions perform HTTP requests, add the correct headers, handle errors, and create notifications. Some functions are wrapped in a React Hook for single fetches, like fetching profile data. Some functions are wrapped in a React Context to make data accessible to all components, like the list of all keywords.

The src/services/notifications/notifications.ts file contains a thin wrapper for Ant Design Messages. These are used to notify the user of loading, success, and errors.

Simple service function

Here's an example of the function to register an account:

src/services/register-account.ts

import type {
  RegisterAccountParams,
  RegisterAccountRes,
} from "../pages/api/register-account";
import ApiRoutes from "../routing/api-routes";
import { en } from "./context/language-ctx";
import getAuthHeader from "./headers/auth-header";
import { contentTypeJsonHeader } from "./headers/content-type-headers";
import Notification from "./notifications/notification";

export default async function registerAccount(
  params: RegisterAccountParams
): Promise<RegisterAccountRes | null> {
  const authHeader = await getAuthHeader();
  if (!authHeader) return null;

  const notification = new Notification();
  try {
    notification.loading(
      en ? "Registering Account..." : "Compte d'enregistrement..."
    );
    const res = await fetch(ApiRoutes.registerAccount, {
      method: "PUT",
      headers: { ...authHeader, ...contentTypeJsonHeader },
      body: JSON.stringify(params),
    });
    if (!res.ok) throw await res.text();
    notification.success();
    return await res.json();
  } catch (e: any) {
    notification.error(e);
    return null;
  }
}

This function does the following:

  • Defines the correct parameter type and return type (imported from the backend file).
  • Gets the authorization header containing the access token
  • Displays a "registering account" notification
  • Makes the HTTP request with the correct method, headers, and stringified body.
  • Checks the return status of the request (res.ok means status is 200)
  • If successful: displays a success notification and returns the result
  • If not successful: displays an error notification and returns null (notification.error also logs the error to the console)

Service Hook

Here's an example of the hook to retrieve an account profile:

src/services/use-account.ts

import { useEffect, useState } from "react";
import ApiRoutes from "../routing/api-routes";
import getAuthHeader from "./headers/auth-header";
import type { AccountInfo } from "./_types";

export default function useAccount(id: number) {
  const [account, setAccount] = useState<AccountInfo | null>(null);
  const [loading, setLoading] = useState(true);

  async function fetchAccount(id: number) {
    try {
      const authHeader = await getAuthHeader();
      if (!authHeader) return;
      setLoading(true);
      const result = await fetch(ApiRoutes.account(id), {
        headers: authHeader,
      });
      if (!result.ok) return console.error(await result.text());
      const account = await result.json();
      setAccount(account);
    } catch (e: any) {
      console.error(e);
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => {
    fetchAccount(id);
  }, [id]);

  function refresh() {
    fetchAccount(id);
  }

  return { account, setAccount, loading, refresh };
}

The hook wraps a simple service function, but provides a state variable for React components to use, which will re-render the UI on change.

The hook also provides a mechanism to let components know if the data is loading.

It also provides a mechanism to overwrite the account in local memory, in the case we have updated the account using another service function, which returned the new value.

And finally it provides a function to re-fetch the account data.

The syntax for using the hook is just:

const { account, setAccount, loading, refresh } = useAccount(id);

You can see this hook being used in src/components/accounts/account-profile.tsx

Service Contexts

React Contexts provide a way to easily cache data and share it between components. This is useful for immediately fetching tables that are not expected to change, such as the list of all faculties, and the list of all member types.

As this application was designed with a small database in mind, the list of all members, the list of all keywords, and the list of all accounts (if the user is an admin) are also fetched and cached on application load, to make the experience faster for the user.

A context is also responsible for managing the current user, as discussed in Authentication.

There is also a context that manages the user's preferred language, and stores the value in local storage to ensure it remains the same across sessions. src/services/context/language-ctx.tsx

Finally there is a context that manages if the user has unsaved changes, and prevents navigation with a save changes prompt. src/services/context/save-changes-ctx.tsx

Here's the context that fetches the list of faculties src/services/context/faculties-ctx.tsx:

First we create the simple service function, no need for loading or success notifications since this will be happening in the background:

async function fetchAllFaculties(): Promise<faculty[]> {
  try {
    const res = await fetch(ApiRoutes.allFaculties);
    if (!res.ok) throw await res.text();
    return await res.json();
  } catch (e: any) {
    new Notification().error(e);
    return [];
  }
}

Then we create a context that defines what data and functions are exposed:

export const FacultiesCtx = createContext<{
  faculties: faculty[];
  refresh: () => void;
}>(null as any);

You can provide a default value instad of null, but there's not much point as the default value should never be accessible.

Then we create a provider, which is a React component that will manage the context and provide it to all of it's children.

export const FacultiesCtxProvider: FC<PropsWithChildren> = ({ children }) => {
  const [faculties, setFaculties] = useState<faculty[]>([]);
  const { en } = useContext(LanguageCtx);

  async function getFaculties() {
    setFaculties((await fetchAllFaculties()).sort(en ? enSorter : frSorter));
  }

  useEffect(() => {
    getFaculties();
  }, []);

  useEffect(() => {
    setFaculties((prev) => [...prev].sort(en ? enSorter : frSorter));
  }, [en]);

  function refresh() {
    getFaculties();
  }

  return <FacultiesCtx.Provider value={{ faculties, refresh }}>{children}</FacultiesCtx.Provider>;
};

To integrate the context into the application, we simply wrap the entire app in this component. But since there are quite a few contexts, we bundle them into a single component in src/services/context/_ctx-bundler.tsx

const AllContextProviders: FC<PropsWithChildren> = [
  ActiveAccountCtxProvider,
  LanguageCtxProvider,
  MemberTypesCtxProvider,
  FacultiesCtxProvider,
  SaveChangesCtxProvider,
  AllKeywordsCtxProvider,
  AllAccountsCtxProvider,
  AllMembersCtxProvider,
].reduceRight((Accumulator, Parent) => {
  const Provider: FC<PropsWithChildren> = ({ children }) => (
    <Parent>
      <Accumulator>{children}</Accumulator>
    </Parent>
  );
  return Provider;
});

export default AllContextProviders;

Now we just need to wrap the entire app with AllContextProviders, which is done in src/pages/_app.tsx, the file that renders Next.js pages.

<AllContextProviders>
  <Navbar />
  <div className="next-page-container">
    <Component {...pageProps} />
  </div>
</AllContextProviders>

Now any component can access a context with the following syntax:

const { faculties } = useContext(FacultiesCtx);

Pages

Next.js Pages are how Next.js handles routing.

Routes are generated based on the file names within src/pages. For example, pages/register.tsx generates a route at <domain root>/register.

The file name index will resolve to the path of the parent directory. For example, pages/members/index.tsx generates a route at <domain root>/members

You can also include parameters in routes with square brackets. For example pages/members/[id]/public will generate a route that accepts any string in place of [id], such as <domain root>/members/123/public.

Each of these files should have a React component as their default export. When a user travels to one of these routes, that component gets rendered.

Its best not to have too much logic in these files, and just use this directory to render a single component from src/components. Routing paths can be subject to change, and you don't want to be moving around a lot of code when that happens.

For example, here's the file for a member's public profile:

src/pages/members/[id]/public.tsx

import { useRouter } from "next/router";
import type { NextPage } from "next/types";
import PublicMemberProfile from "../../../components/members/member-public-profile";

const PublicMemberPage: NextPage = () => {
  const router = useRouter();
  const { id } = router.query;
  if (!(typeof id === "string")) return null;
  return <PublicMemberProfile id={parseInt(id)} />;
};

export default PublicMemberPage;

All we're doing is parsing the route parameter and rendering another component, passing the id through.

To avoid hardcoded strings during navigation, routes for every page are stored in src/routing/page-routes.ts.

Navigation can be performed using the Next.js Router

router.push(PageRoutes.accountProfile(id));

Auth Guards

Although the backend is protected against unauthorized requests, we'd prefer if unauthorized requests didn't happen at all.

If a user tries to navigate to a route that is not meant for them to see, we should not load the page, and instead redirect the user or display a message.

This application handles that case with a PageAuthGuard component.

There are 4 types of authorizations defined:

src/components/auth-guard/authorizations.ts

enum Authorizations {
  admin = "admin", // admins only
  registered = "registered", // any registered account
  matchAccountId = "match account id", // must have matching account id to a route
  matchMemberId = "match member id", // must have matching member id to a route
}

The component just takes what authorizations are required, and decides whether or not to render its children based on the current active user. If the user is not authorized it will display a message.

src/components/auth-guard/page-auth-guard.tsx

type Props = {
  auths: Authorizations[],
  id?: number,
  loadingIcon?: ReactElement,
};

const PageAuthGuard: FC<PropsWithChildren<Props>> = ({
  auths,
  id,
  loadingIcon,
  children,
}) => {
  const { localAccount, loading } = useContext(ActiveAccountCtx);
  const { en } = useContext(LanguageCtx);

  if (loading) return loadingIcon || <CenteredSpinner />;

  const notAuthorized = (
    <h1 style={{ textAlign: "center" }}>
      {en
        ? "You are not authorized to view this page."
        : "Vous n'êtes pas autorisé à afficher cette page."}
    </h1>
  );

  const c = <>{children}</>;

  if (!localAccount) return notAuthorized;
  if (auths.includes(Authorizations.registered)) return c;
  if (auths.includes(Authorizations.admin) && localAccount.is_admin) return c;
  if (auths.includes(Authorizations.matchAccountId) && localAccount.id === id)
    return c;
  if (!localAccount.member) return notAuthorized;
  if (
    auths.includes(Authorizations.matchMemberId) &&
    localAccount.member.id === id
  )
    return c;
  return notAuthorized;
};

Here's an example of using it to stop an unauthorized user from attempting to load the accounts interface:

src/pages/accounts/index.tsx

const AccountsPage: NextPage = () => {
  return (
    <PageAuthGuard
      auths={[Authorizations.admin]}
      loadingIcon={<Table loading></Table>}
    >
      <AllAccounts />
    </PageAuthGuard>
  );
};

Components

React Components are what make up the user interface. All of the components in this application are Function Components, as opposed to Class Components.

These UI components are located in src/components.

Here is a list of each component and it's description:

Name File Description
Welcome welcome.tsx Displays a greeting message depending on the type of user signed in
RegisterAccount register-account.tsx Form for an admin to register a new account
Footer footer.tsx Displays a footer with a copyright notice and a link to the privacy policy
Name File Description
AccountProfile account-profile.tsx Allows admins to view and edit an account
AllAccounts all-accounts.tsx Table of all accounts
DeleteAccountButton delete-account-button Allows deleting an account with a confirmation prompt
DeleteMemberButton delete-member-button.tsx Allows deleting member information attached to an account with a confirmation prompt
GrantAdminButton grant-admin-button.tsx Allows granting admin status to an account with a confirmation prompt
RegisterMemberButton register-member-button.tsx Allows registering an account as a member with a confirmation prompt
RemoveAdminButton remove-admin-button.tsx Allows removing admin status from an account with a confirmation prompt
UpdateEmailButton update-email-button.tsx Allows opening a modal to edit an account's login email
UpdateNameButton update-name-button.tsx Allows opening a modal to edit an account's first and last name
Name File Description
AllEvents all-events.tsx This is a component that displays a table of events, with filters to filter the events based on name, type, start date and end date.
DeleteEventButton delete-event-button.tsx This is a modal button component that, when clicked, opens a modal to confirm the deletion of an event.
PrivateEventProfile event-private-profile.tsx This component displays the profile of an event, with the possibility to view or edit it
PublicEventDescription event-public-description.tsx Displays information about a public event
PublicEventForm event-public-form.tsx This component allows the user to update the public information of an event and manage the related topics, members, partners, products, grants, and events
EventPublicProfile event-public-profile.tsx This is a component that displays the public profile of an event.
RegisterEvent event-register.tsx This component allows users to register a new event by providing the event's name in English and French, date range, event type, and a note.
EventSelector event-selector.tsx This component is a form element that allows a user to search for existing events and select one or more of them.
EventTag event-tag.tsx This component is a presentational component that displays an event in a tag format.

These are Custom Ant Design Form Controls

Name File Description
FacultyFilter faculty-filter.tsx Allows selecting a set of faculty ids via a dropdown select
KeywordFilter keyword-filter.tsx Allows selecting a set of keyword ids via a dropdown select
MemberNameFilter member-name-filter.tsx Allows selecting a set of member ids via a dropdown select
MemberTypeFilter member-type-filter.tsx Allows selecting a set of member type ids via a dropdown select

These are helpers functions that return a list of member names

Name File Description
getInvestigatorMember grant-investigator-member-getter.tsx Returns a list of names of grant investigators
getMemberInvolved grant-member-involved-getter.tsx Returns a list of names of members involved in a grant
getMemberOrg member-partner-getter.tsx Returns a list of names involved in a partnership
getMemberProduct member-product-author-getter.tsx Returns a list of products authored by members
getMemberSupervision member-supervision-getter.tsx Returns a list of names of principal supervisors
getMemberAuthor product-member-author-getter.tsx Returns a list of names of members who are authors of a product
Name File Description
AllGrants all-grants.tsx displays a table of grants, with filters to filter the grants based on name, status and source
DeleteGrantButton delete-grant-button.tsx A button that opens a modal to delete a grant
PrivateGrantProfile grant-private-profile.tsx A detailed view of a private grant's profile
PublicGrantDescription grant-public-description.tsx Displays the public information of a grant in a table format
PublicGrantForm grant-public-form.tsx Displays a form for updating public information of a grant.
PublicGrantProfile grant-public-profile.tsx Displays the public grant profile information for a specific grant id.
RegisterGrant grant-register.tsx Provides a form for registering a grant
GrantSelector grant-selector.tsx Allows a user to search for grants and select one or more of them
GrantTag grant-tag.tsx Presentational component that displays a grant in a tag format
Name File Description
EditKeywordModal edit-keyword-modal.tsx Allows editing an existing keyword
KeywordPreview keyword-preview.tsx Shows a preview of a keyword's tags
KeywordSelector keyword-selector.tsx Allows selecting a single keyword via a dropdown select
KeywordTag keyword-tag.tsx Creates a colorful tag given a keyword
NewKeywordModal new-keyword-modal.tsx Allows creating a new keyword
Name File Description
Layout layout.tsx A simple layout component that contains the main content of the page and a footer
Name File Description
FacultyLink faculty-link.tsx Given a faculty, creates a clickable link to the members table with this faculty as a filter
MemberTypeLink member-type-link.tsx Given a member type, creates a clickable link to the members table with this type as a filter
SafeLink safe-link.tsx A link that conditionally navigates, provided the user does not have unsaved changes. Otherwise opens the save changes prompt
Name File Description
CardSkeleton card-skeleton.tsx Skeleton animation of a card used during loading
CenteredSpinner centered-spinner.tsx An Ant Design Spinner styled to be centered on its parent
Name File Description
AllMembers all-members.tsx The table of all members, includes filter components
MemberInsightDescription member-insight-description.tsx Displays a member's insight information
MemberInsightForm member-insight-form.tsx A form for editing a member's insight information
PrivateMemberDescription member-private-description.tsx Displays a member's private information
PrivateMemberForm member-private-form.tsx A form for editing a member's private information
PrivateMemberProfile member-private-profile.tsx Top level component for viewing a member's profile, includes tabs and editing
PublicMemberDescription member-public-description.tsx Displays a member's public information
PublicMemberForm member-public-form.tsx A form for editing a member's public information
PublicMemberProfile member-public-profile.tsx Top level component for viewing a member's profile, only includes public description
MyProfileRegister my-profile-register.tsx A prompt for when a registered account navigates to My Profile but they are not registered in the members table, allows them to create an entry for themselves
MyProfile my-profile.tsx Same as Private Member Profile, but references the active account
Name File Description
Navbar _navbar.tsx Top level component, containing all other navbar components
AvatarMenu avatar-menu.tsx Displays a signed in user's initials, includes a dropdown for displaying some account info and logging out
HomeLogo home-logo.tsx The application logo and navigation to home
LanguageButton language-button.tsx The button to switch the preferred language between english and french
LoginButton login-button.tsx The button to initial authentication
LogoutButton logout-button.tsx The button to logout
NavMenu nav-menu.tsx The menu containing links to application routes
Name File Description
AllPartners all-partners.tsx Displays a table of partners (organizations), with filters to filter the partners based on name, type and scope
DeletePartnerButton delete-partner-button.tsx Allows an admin user to delete a partner from the system.
PrivatePartnerProfile partner-private-profile.tsx A card which displays private information of a partner
PublicPartnerDescription partner-public-description.tsx Displays the public information of a partner, including organization type, organization scope, and description.
PublicPartnerForm partner-public-form.tsx Allows editing of public information of a Partner, such as name, type, scope, and description.
PublicPartnerProfile partner-public-profile.tsx Displays the public information of an organization/partner
OrganizationSelector partner-selector.tsx A form element that allows a user to search for partners/organizations and select one or more of them.
OrganizationTag partner-tag.tsx A presentational component that displays a partner in a tag format.
RegisterPartner register-partner-member.tsx A form that allows a user to register his partner
RegisterPartner register-partner.tsx A form that allows an admin user to register a partner
Name File Description
AllProducts all-products.tsx This is a component that displays a table of products , with filters to filter the products based on their title, types, authors.
isAuthorMatch author-match.tsx this is a function that checks if the given author name matches the given first name and last name.
DeleteProductButton delete-product-button.tsx This component is a button that opens a modal to delete a product.
ProductAdminDescription product-admin-description.tsx This is a functional component that displays the private information of a product.
ProductAdminForm product-admin-form.tsx This component is a form that allows to update the admin information of a product.
PrivateProductDescription product-private-description.tsx This component displays private information about a product.
PrivateProductForm product-private-form.tsx This component is used to display a form for editing the private information of a product.
PrivateProductProfile product-private-profile.tsx This is a component that displays a private product profile.
PublicProductDescription product-public-description.tsx This is a component that displays the public information of a product.
PublicProductForm product-public-form.tsx This component is a form used to edit the public information of a product.
PublicProductProfile product-public-profile.tsx This component displays the public profile of a product
RegisterProduct product-register.tsx This component is used to register a new product in the system.
ProductSelector product-selector.tsx This component is a form element that allows a user to search for products and select one or more of them.
ProductTag product-tag.tsx This component is a presentational component that displays a product in a tag format.
Name File Description
AllSupervisions all-supervisions.tsx This is a component that displays a table of supervisions , with filters to filter the supervisions based on supervision trainee name, faculty and level
DeleteSupervisionButton delete-supervision-button.tsx This component is a button that opens a modal to delete a supervision.
RegisterSupervision supervision-member-register.tsx This component is used to register a supervision trainee.
PrivateSupervisionProfile supervision-private-profile.tsx A component that displays a private supervision profile.
PublicSupervisionDescription supervision-public-description.tsx This component displays the public information of a supervision in a description list format
PublicSupervisionForm supervision-public-form.tsx This is a form component that allows the user to edit public information of a supervision.
PublicSupervisionProfile supervision-public-profile.tsx This component displays the public profile of a supervision.
RegisterSupervision supervision-register.tsx This component is a form component to register a new supervision

Exploring Events: A Step-by-Step Guide

Users can navigate to the Events tab to initiate the exploration process. Here, an array of events awaits, as depicted in the illustrative figure below: image

Upon spotting an event of interest, users can click on it to gain a summary view of the event. In the example below, Event 1 has been selected, leading to a dedicated page that presents a concise summary of the chosen event: image

Journey of Event Page

Under the summarized table mentioned above, a button placed and labeled "Journey of Event". Clicking this button provides a hierarchical view specific to the chosen event. The visualization below illustrates this feature: image

Expandable Options

In our development, we have categorized an event into two parts:

  1. Event Summary Expansion( image ) ;
  2. Next Event Expansion( image )

Event Summary Expansion

image

In the event summary (image), it displays the title of the event, type of event, start date and number of next event/s. Once a user expands, the user can see a detailed view of the member involved, grant, partner involved, and any product has resulted such as book, paper etc.

Next Event Expansion

image

Once the user clicks on the ( image ), it will display the event's child event, if any.

Pages

To achiche the abouve pages, a journey folder was created at /src/pages/joe, followed by previous development. Following is the code in this folder:

import { useRouter } from "next/router";
import type { NextPage } from "next/types";
import { useContext, useEffect } from "react";
import { ActiveAccountCtx } from "../../../services/context/active-account-ctx";
import CardSkeleton from "../../../components/loading/card-skeleton";
import PageRoutes from "../../../routing/page-routes";

const PrivatejoePage: NextPage = () => {
  const router = useRouter();
  const { localAccount, loading } = useContext(ActiveAccountCtx);
  const { id: idString } = router.query;

  useEffect(() => {
    if (!(typeof idString === "string")) {
      return;
    }
    if (loading) return;

    const id = parseInt(idString);

    if (localAccount?.is_admin) {
      router.replace(PageRoutes.privateEventProfile(id));
      return;
    } else {
      router.replace(PageRoutes.publicEventProfile(id));
    }
  }, [localAccount, loading, idString, router]);

  return <CardSkeleton />;
};

export default PrivatejoePage;

Routing

To route these page above, it has been added to our routing page at src/routing/page-routes.ts as joe: (id: number) => "/joe/" + id + "/private"

Styles

This application using SCSS which is a superset of CSS, so all vanilla CSS is still valid.

All SCSS files are located in src/styles.

Only src/styles/_globals.scss is imported into the application (in src/pages/_app.tsx. So when creating a new stylesheet just import it into _globals.scss.

If just doing a small amount of styling somewhere specific, you can always use React inline styling instead.

⚠️ **GitHub.com Fallback** ⚠️