diff --git a/js/api/eclipsefdn.interest-groups.js b/js/api/eclipsefdn.interest-groups.js
new file mode 100644
index 0000000000000000000000000000000000000000..c5536c9efe670ae227efdf8808333cbf1f3ca649
--- /dev/null
+++ b/js/api/eclipsefdn.interest-groups.js
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2023 Eclipse Foundation, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * Contributors:
+ *   Olivier Goulet <olivier.goulet@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+import 'isomorphic-fetch';
+import { getOrganizationById } from './eclipsefdn.membership';
+
+const apiBasePath = 'https://projects.eclipse.org/api/interest-groups';
+
+const interestGroupMapper = ig => {
+  return {
+    leads: ig.leads.map(lead => ({
+      fullName: lead.full_name,
+      ...lead,
+    })),
+    participants: ig.participants.map(participant => ({
+      fullName: participant.full_name,
+      ...participant,
+    })),
+    foundationDbProjectId: ig.foundationdb_project_id,
+    gitlab: {
+      projectGroup: ig.gitlab.project_group,
+      ignoredSubGroups: ig.gitlab.ignored_sub_groups,
+    },
+    ...ig,
+  };
+};
+
+export const getInterestGroup = async ({ interestGroupNodeId, interestGroupId }) => {
+  try {
+    let url;
+    if (interestGroupId) {
+      url = `${apiBasePath}?project_id=foundation-internal.ig.${interestGroupId}`;
+    } else if (interestGroupNodeId) {
+      url = `${apiBasePath}/${interestGroupNodeId}`;
+    } else {
+      throw new Error('No interestGroupId or interestGroupNodeId provided to getInterestGroup');
+    }
+
+    const response = await fetch(url);
+    if (!response.ok) throw new Error(
+        interestGroupId 
+          ? `Could not fetch interest group for id "${interestGroupId}". Ensure that you are using the same value as project_short_id from the API`
+          : `Could not fetch interest group for node id "${interestGroupNodeId}"`
+      );
+    
+    // Request without node id will return an array of one element
+    const data = interestGroupNodeId 
+      ? await response.json()
+      : (await response.json())[0];
+    const interestGroup = interestGroupMapper(data);
+
+    return [interestGroup, null];
+  } catch (error) {
+    return [null, error];
+  }
+}
+
+export const getInterestGroups = async () => {
+  try {
+    const response = await fetch(`${apiBasePath}`);
+    if (!response.ok) throw new Error(`Could not fetch interest groups`);
+    
+    const data = await response.json();
+    const interestGroups = data.map(interestGroupMapper);
+
+    return [interestGroups, null];
+  } catch (error) {
+    return [null, error];
+  }
+}
+
+export const getInterestGroupParticipantOrganizations = async (options) => {
+  try {
+    const [interestGroup, error] = await getInterestGroup(options);
+    if (error) throw new Error(error);
+
+    /*
+      Create an array from a set of participants' organizations. The set guarantees that the 
+      organizations have no duplicates.
+    */
+    const participatingOrganizationIds = [
+      ...new Set(interestGroup.participants.map(p => p.organization.id)),
+    ];
+
+    /*
+      Get all participating organizations from organization id. Filter out the ones which had errors 
+      for whatever reason.
+    */
+    const participatingOrganizations = (
+      await Promise.all(
+        participatingOrganizationIds.map(async participantId => {
+          const [participant, participantError] = await getOrganizationById(participantId);
+
+          if (participantError) {
+            console.error(`Could not fetch participant organization from id ${participantId}`);
+          }
+
+          return participant;
+        })
+      )
+    ).filter(participant => participant !== null);
+
+    return [participatingOrganizations, null];
+  } catch (error) {
+    console.error(error);
+    return [null, error];
+  }
+};
diff --git a/js/api/eclipsefdn.members.js b/js/api/eclipsefdn.members.js
new file mode 100644
index 0000000000000000000000000000000000000000..9f668bbfdd71430fd1baa132d0379b1c77ff7b67
--- /dev/null
+++ b/js/api/eclipsefdn.members.js
@@ -0,0 +1,46 @@
+/*!
+ * Copyright (c) 2021 Eclipse Foundation, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * Contributors:
+ *   Christopher Guindon <chris.guindon@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+import parse from 'parse-link-header';
+import 'isomorphic-fetch';
+
+const getMembers = (url = '', members = [], errHandler) => {
+  return new Promise((resolve, reject) =>
+    fetch(url)
+      .then((response) => {
+        if (response.status !== 200) {
+          throw `${response.status}: ${response.statusText}`;
+        }
+        response
+          .json()
+          .then((data) => {
+            members = members.concat(data);
+            const linkHeader = parse(response.headers.get('Link'));
+            if (linkHeader?.next) {
+              const { url } = linkHeader.next;
+              getMembers(url, members, errHandler).then(resolve).catch(reject);
+            } else {
+              members.sort((a, b) => a.name.localeCompare(b.name));
+              resolve(members);
+            }
+          })
+          .catch(reject);
+      })
+      .catch((err) => {
+        errHandler && errHandler(err);
+        reject(err);
+      })
+  );
+};
+
+export default getMembers;
diff --git a/js/api/eclipsefdn.membership.js b/js/api/eclipsefdn.membership.js
new file mode 100644
index 0000000000000000000000000000000000000000..55551fa9365fc1025a00cee2df0553d72f751d3f
--- /dev/null
+++ b/js/api/eclipsefdn.membership.js
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2023 Eclipse Foundation, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * Contributors:
+ *   Olivier Goulet <olivier.goulet@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+import 'isomorphic-fetch';
+import { convertToQueryString } from '../utils/utils';
+
+const apiBasePath = 'https://membership.eclipse.org/api';
+
+export const organizationMapper = organization => {
+  const { organization_id: organizationId, ...otherOrganizationFields } = organization;
+
+  return {
+    organizationId,
+    ...otherOrganizationFields,
+  };
+};
+
+export const getOrganizationById = async organizationId => {
+  try {
+    const response = await fetch(`${apiBasePath}/organizations/${organizationId}`);
+    if (!response.ok) throw new Error(`Could not fetch organization by id ${organizationId}`);
+
+    const data = await response.json();
+    const organization = organizationMapper(data);
+
+    return [organization, null];
+  } catch (error) {
+    console.error(error);
+    return [null, error];
+  }
+};
+
+export const getOrganizations = async (params = {}) => {
+  try {
+    const queryString = convertToQueryString(params); 
+    const response = await fetch(`${apiBasePath}/organizations?${queryString}`);
+    if (!response.ok) {
+      throw new Error(`Could not fetch organizations`);
+    }
+
+    const data = await response.json();
+    const organizations = data
+      .map(organizationMapper)
+    
+    return [organizations, null];
+  } catch (error) {
+    console.error(error);
+    return [null, 'An unexpected error has occurred when fetching organizations'];
+  }
+}
+
+export const getProjectParticipatingOrganizations = async (projectId, params = {}) => {
+  try {
+    if (!projectId) {
+        throw new Error('No project id provided for fetching project participating organizations');
+    }
+
+    const queryString = convertToQueryString(params);
+    const response = await fetch(`${apiBasePath}/projects/${projectId}/organizations?${queryString}`);
+    if (!response.ok) { 
+        throw new Error(`Could not fetch project organizations for project id "${projectId}"`);
+    }
+
+    const data = await response.json();
+    const organizations = data
+        .map(organizationMapper)
+        .sort((a, b) => a.name.localeCompare(b.name));
+
+    return [organizations, null];
+  } catch (error) {
+    console.error(error);
+    return [
+      null,
+      'An unexpected error has occurred when fetching participating organizations for project',
+    ];
+  }
+};
diff --git a/js/eclipsefdn.constants.js b/js/eclipsefdn.constants.js
new file mode 100644
index 0000000000000000000000000000000000000000..01eb3652863f599460d56816a33c3f130e6c0fda
--- /dev/null
+++ b/js/eclipsefdn.constants.js
@@ -0,0 +1,3 @@
+export const SD_IMG = 'https://www.eclipse.org/membership/images/type/strategic-members.png';
+export const AP_IMG = 'https://www.eclipse.org/membership/images/type/contributing-members.png';
+export const AS_IMG = 'https://www.eclipse.org/membership/images/type/associate-members.png';
diff --git a/js/eclipsefdn.interest-group-members-list.js b/js/eclipsefdn.interest-group-members-list.js
new file mode 100644
index 0000000000000000000000000000000000000000..decdcee184f133326398bb7cb6015f7432ce1b9f
--- /dev/null
+++ b/js/eclipsefdn.interest-group-members-list.js
@@ -0,0 +1,249 @@
+/*!
+ * Copyright (c) 2021, 2023 Eclipse Foundation, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * Contributors:
+ *   Christopher Guindon <chris.guindon@eclipse-foundation.org>
+ *   Zhou Fang <zhou.fang@eclipse-foundation.org>
+ *   Olivier Goulet <olivier.goulet@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+import $ from 'jquery';
+import 'jquery-match-height';
+import getMembers from './api/eclipsefdn.members';
+import { displayErrMsg, validateURL } from './utils/utils';
+import template from './templates/explore-members/member.mustache';
+import templateOnlyLogos from './templates/wg-members/wg-member-only-logos.mustache';
+import templateLogoTitleWithLevels from './templates/wg-members/wg-member-logo-title-with-levels.mustache';
+import templateLoading from './templates/loading-icon.mustache';
+import { SD_IMG, AP_IMG, AS_IMG } from './eclipsefdn.constants.js';
+import { getInterestGroupParticipantOrganizations } from './api/eclipsefdn.interest-groups';
+import { organizationMapper } from './api/eclipsefdn.membership';
+
+const SD_NAME = 'Strategic Members';
+const AP_NAME = 'Contributing Members';
+const AS_NAME = 'Associate Members';
+
+const defaultOptions = {
+  mlWg: null,
+  mlLevel: null,
+  mlSort: null,
+  mlLinkMemberWebsite: null,
+  type: 'working-group',
+  id: null
+};
+
+const EclipseFdnMembersList = (async () => {
+  const element = document.querySelector('.interest-group-test');
+  if (!element) return;
+  
+  element.innerHTML = templateLoading();
+
+  let membersListArray = [
+    { level: SD_NAME, members: [] },
+    { level: AP_NAME, members: [] },
+    { level: AS_NAME, members: [] },
+  ];
+  let url = 'https://membership.eclipse.org/api/organizations?pagesize=100';
+
+  const options = { ...defaultOptions, ...element.dataset };
+
+  const isRandom = options.mlSort === 'random';
+  const level = options.mlLevel;
+  const linkMemberWebsite = element.getAttribute('data-ml-link-member-website') === 'true' ? true : false;
+  const industryCollaborationId = options.id || options.mlWg;
+
+  if (level) {
+    level.split(' ').forEach((item) => (url = `${url}&levels=${item}`));
+  }
+
+  let membersData = [];
+  if (options.type === 'working-group') {
+    if (industryCollaborationId) { 
+      url = `${url}&working_group=${industryCollaborationId}`;
+    }
+    // When we show members belong to an industry collaboration, no need to show EF levels
+    membersListArray = [{ level: '', members: [] }];
+    membersData = (await getMembers(url, [], (err) => displayErrMsg(element, err)))
+      .map(organizationMapper);
+  } else if (options.type === 'interest-group') {
+    membersListArray = [{ level: '', members: [] }];
+
+    // We assume the industry collaboration id is a Drupal node ID if it is numeric
+    // This widget needs to support node id and short ids for interest groups
+    const isNodeId = !Number.isNaN(parseInt(industryCollaborationId));
+    [membersData] = await getInterestGroupParticipantOrganizations({ 
+      interestGroupId: isNodeId ? undefined : industryCollaborationId,
+      interestGroupNodeId: isNodeId ? industryCollaborationId : undefined
+    });
+  }
+
+  const addMembersWithoutDuplicates = (levelName, memberData) => {
+    // When we show members belong to a wg, only 1 item exits in membersListArray
+    const currentLevelMembers = industryCollaborationId ? membersListArray[0] : membersListArray.find((item) => item.level === levelName);
+    const isDuplicatedMember = currentLevelMembers.members.find(
+      (item) => item.organizationId === memberData.organizationId
+    );
+    !isDuplicatedMember && currentLevelMembers.members.push(memberData);
+  };
+
+  membersData = membersData.map((member) => {
+    if (!member.name) {
+      // will not add members without a title/name into membersListArray to display
+      return member;
+    }
+    if (member.levels.find((item) => item.level?.toUpperCase() === 'SD')) {
+      if (!member.logos.web) {
+        member.logos.web = SD_IMG;
+      }
+      addMembersWithoutDuplicates(SD_NAME, member);
+      return member;
+    }
+
+    if (member.levels.find((item) => item.level?.toUpperCase() === 'AP' || item.level?.toUpperCase() === 'OHAP')) {
+      if (!member.logos.web) {
+        member.logos.web = AP_IMG;
+      }
+      addMembersWithoutDuplicates(AP_NAME, member);
+      return member;
+    }
+
+    if (member.levels.find((item) => item.level?.toUpperCase() === 'AS')) {
+      if (!member.logos.web) {
+        member.logos.web = AS_IMG;
+      }
+      addMembersWithoutDuplicates(AS_NAME, member);
+      return member;
+    }
+
+    return member;
+  });
+
+  if (isRandom) {
+    // Sort randomly
+    membersListArray.forEach((eachLevel) => {
+      let tempArray = eachLevel.members.map((item) => item);
+      const randomItems = [];
+      eachLevel.members.forEach(() => {
+        const randomIndex = Math.floor(Math.random() * tempArray.length);
+        randomItems.push(tempArray[randomIndex]);
+        tempArray.splice(randomIndex, 1);
+      });
+      eachLevel.members = randomItems;
+    });
+  } else {
+    // Sort alphabetically by default
+    membersListArray.forEach((eachLevel) => {
+      eachLevel.members.sort((a, b) => {
+        const preName = a.name.toUpperCase();
+        const nextName = b.name.toUpperCase();
+        if (preName < nextName) {
+          return -1;
+        }
+        if (preName > nextName) {
+          return 1;
+        }
+        return 0;
+      });
+    });
+  }
+
+  if (level) {
+    membersListArray = membersListArray.filter((list) => list.members.length !== 0);
+  }
+
+  const urlLinkToLogo = function () {
+    if (linkMemberWebsite && validateURL(this.website)) {
+      return this.website;
+    }
+    return `https://www.eclipse.org/membership/showMember.php?member_id=${this.organizationId}`;
+  };
+
+  const displayMembersByLevels = async (theTemplate) => {
+    if (options.type !== 'working-group') {
+      console.error('Only "working-group" type is supported for displaying members by level at this time');
+      return;
+    }
+    
+    const allWGData = await (await fetch('https://membership.eclipse.org/api/working_groups')).json();
+    let currentWGLevels = allWGData.find((item) => item.alias === industryCollaborationId).levels;
+    // defaultLevel is for members without a valid level. So far, only occurs on IoT.
+    const defaultLevel = element.getAttribute('data-ml-default-level');
+    const specifiedLevel = element.getAttribute('data-ml-wg-level');
+    if (specifiedLevel) {
+      currentWGLevels = currentWGLevels.filter((level) =>
+        specifiedLevel.toLowerCase().includes(level.relation.toLowerCase())
+      );
+    }
+    element.innerHTML = '';
+    if (defaultLevel) {
+      currentWGLevels.push({ relation: 'default', description: defaultLevel, members: [] });
+    }
+
+    // categorize members into corresponding levels
+    currentWGLevels.forEach((level) => {
+      level['members'] = [];
+      membersListArray[0].members.forEach((member) => {
+        const memberLevel = member.wgpas.find((wgpa) => wgpa.working_group === industryCollaborationId)?.level;
+        if (memberLevel === level.relation) {
+          level.members.push(member);
+        }
+        if (level.relation === 'default' && memberLevel === null) {
+          level.members.push(member);
+        }
+      });
+
+      if (level.members.length === 0) {
+        return;
+      }
+
+      const showLevelUnderLogo = () => {
+        if (
+          !element.getAttribute('data-ml-level-under-logo') ||
+          element.getAttribute('data-ml-level-under-logo') === 'false'
+        ) {
+          return false;
+        }
+        return level.description.replaceAll(' Member', '');
+      };
+
+      element.innerHTML =
+        element.innerHTML +
+        theTemplate({ item: level.members, levelDescription: level.description, urlLinkToLogo, showLevelUnderLogo });
+    });
+  };
+
+  switch (element.getAttribute('data-ml-template')) {
+    case 'only-logos':
+      element.innerHTML = templateOnlyLogos({
+        item: membersListArray[0].members,
+        urlLinkToLogo,
+      });
+      return;
+
+    case 'logo-title-with-levels':
+      await displayMembersByLevels(templateLogoTitleWithLevels);
+      $.fn.matchHeight._applyDataApi();
+      return;
+    case 'logo-with-levels':
+      displayMembersByLevels(templateOnlyLogos);
+      return;
+    default:
+      break;
+  }
+
+  element.innerHTML = template({
+    sections: membersListArray,
+    hostname: window.location.hostname.includes('staging.eclipse.org')
+      ? 'https://staging.eclipse.org'
+      : 'https://www.eclipse.org',
+  });
+  $.fn.matchHeight._applyDataApi();
+})();
+
+export default EclipseFdnMembersList;
diff --git a/js/main.js b/js/main.js
index 4409b3ee84c5bb35ddccb3f2e8cbfb926cf0fa18..9189ab5559d746bf0129fbd8cc4c999622bed3b0 100644
--- a/js/main.js
+++ b/js/main.js
@@ -1,5 +1,5 @@
 /*!
- * Copyright (c) 2021 Eclipse Foundation, Inc.
+ * Copyright (c) 2021, 2023 Eclipse Foundation, Inc.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -7,8 +7,10 @@
  *
  * Contributors:
  *   Christopher Guindon <chris.guindon@eclipse-foundation.org>
+ *   Olivier Goulet <olivier.goulet@eclipse-foundation.org>
  *
  * SPDX-License-Identifier: EPL-2.0
  */
 
-import 'eclipsefdn-solstice-assets'
\ No newline at end of file
+import 'eclipsefdn-solstice-assets'
+import './eclipsefdn.interest-group-members-list'
\ No newline at end of file
diff --git a/js/templates/explore-members/member-detail.mustache b/js/templates/explore-members/member-detail.mustache
new file mode 100644
index 0000000000000000000000000000000000000000..4ade78cc4c40eb6872df95b6b0afbb3eddf4a2d1
--- /dev/null
+++ b/js/templates/explore-members/member-detail.mustache
@@ -0,0 +1,80 @@
+{{#section}}
+<div class="col-md-14 col-lg-17">
+    {{#validateURL}}
+    <a href="{{website}}" title="{{name}}" target="_blank">
+    {{/validateURL}}
+        {{#logos.web}}
+            <img src="{{logos.web}}" alt="{{name}} logo" title="{{name}}" class="img-responsive padding-bottom-25" />
+        {{/logos.web}}
+        {{^logos.web}}
+            <h1>{{name}}</h1>
+        {{/logos.web}}
+    {{#validateURL}}
+    </a>
+    {{/validateURL}}
+
+    <p>{{{trimDescription}}}</p>
+
+    {{#listings}}
+    <h2>{{name}}&apos;s Marketplace Listings</h2>
+    <ul>
+    </ul>
+    {{/listings}}
+</div>
+<div class="col-md-10 col-lg-7">
+
+    <div style="border:1px solid #eee; padding:10px" class="margin-bottom-20">
+        <img class="img-responsive" src={{getMemberLevelImg}} />
+    </div>
+
+    {{#projects.length}}
+    <div class="text-highlight margin-bottom-20">
+        <i class="fa pull-left fa-trophy orange fa-4x margin-top-10 margin-bottom-25"></i>
+        <h3 class="h5 fw-700">{{name}}</h3>
+        <p>{{name}} contributes to one or more <a href="#projects">Eclipse Projects!</a></p>
+    </div>
+    {{/projects.length}}
+
+    {{#shouldShowLinksSideBar}}
+    <div class="sideitem">
+        <h6>Links</h6>
+        <ul class="fa-ul">
+            {{#products.length}}
+            <li>
+                <i class="fa-li fa fa-chevron-circle-right orange"></i>
+                {{name}}&apos;s Other Products and Services:
+                <ul>
+                {{#products}}
+                <li><a href={{product_url}} target="_blank">{{name}}</a></li>
+                {{/products}}
+                </ul>
+            </li>
+            {{/products.length}}
+            {{#projects.length}}
+            <li>
+                <i class="fa-li fa fa-chevron-circle-right orange"></i>
+                {{name}} is an Active Contributor to the following Project(s):
+                <ul>
+                {{#projects}}
+                    {{#active}}
+                    <li><a href="https://projects.eclipse.org/projects/{{project_id}}" target="_blank">{{name}}</a></li>
+                    {{/active}}
+                {{/projects}}
+                </ul>
+            </li>
+            {{/projects.length}}
+        </ul>
+    </div>
+    {{/shouldShowLinksSideBar}}
+
+    <div class="sideitem">
+        <h6>Interact</h6>
+        <ul class="fa-ul">
+            <li>
+                <i class="fa-li fa fa-chevron-circle-right orange"></i>
+                <a href="https://membership.eclipse.org/portal/org-profile">Edit This Page</a>
+            </li>
+        </ul>
+    </div>
+</div>
+{{/section}}
\ No newline at end of file
diff --git a/js/templates/explore-members/member.mustache b/js/templates/explore-members/member.mustache
new file mode 100644
index 0000000000000000000000000000000000000000..e6d62568e53856e6651c1440a143965a292039ee
--- /dev/null
+++ b/js/templates/explore-members/member.mustache
@@ -0,0 +1,24 @@
+{{#sections}}
+
+<div class="row">
+<h2>{{level}}</h2>
+{{#.}}
+{{#members}}
+<div class="col-xs-24 col-sm-12 col-md-8 margin-bottom-20 m-card">
+  <div class="bordered-box text-center">
+    <div class="box-header background-light-grey vertical-align" data-mh="m-header">
+      <h3 class="h4 margin-0"><a href="{{hostname}}/membership/showMember.php?member_id={{organization_id}}" title="{{name}}">{{name}}</a></h3>
+    </div>
+    <div class="box-body vertical-align" style="height: 160px">
+      <div class="image-container">
+        <a href="{{hostname}}/membership/showMember.php?member_id={{organization_id}}" title="{{name}}">
+        <img src="{{logos.web}}" class="img-responsive margin-auto logos" alt="{{name}} logo">
+        </a>
+      </div>
+    </div>
+  </div>
+</div>
+{{/members}}
+{{/.}}
+</div>
+{{/sections}}
diff --git a/js/templates/loading-icon.mustache b/js/templates/loading-icon.mustache
new file mode 100644
index 0000000000000000000000000000000000000000..3c5a1434f68079d28d1b1cc65d9f672f8ea179f3
--- /dev/null
+++ b/js/templates/loading-icon.mustache
@@ -0,0 +1,4 @@
+<div class="text-center">
+  <i class="fa fa-spinner fa-pulse fa-2x fa-fw margin-20"></i>
+  <span class="sr-only">Loading...</span>
+</div>
diff --git a/js/templates/member.mustache b/js/templates/member.mustache
new file mode 100644
index 0000000000000000000000000000000000000000..e6d62568e53856e6651c1440a143965a292039ee
--- /dev/null
+++ b/js/templates/member.mustache
@@ -0,0 +1,24 @@
+{{#sections}}
+
+<div class="row">
+<h2>{{level}}</h2>
+{{#.}}
+{{#members}}
+<div class="col-xs-24 col-sm-12 col-md-8 margin-bottom-20 m-card">
+  <div class="bordered-box text-center">
+    <div class="box-header background-light-grey vertical-align" data-mh="m-header">
+      <h3 class="h4 margin-0"><a href="{{hostname}}/membership/showMember.php?member_id={{organization_id}}" title="{{name}}">{{name}}</a></h3>
+    </div>
+    <div class="box-body vertical-align" style="height: 160px">
+      <div class="image-container">
+        <a href="{{hostname}}/membership/showMember.php?member_id={{organization_id}}" title="{{name}}">
+        <img src="{{logos.web}}" class="img-responsive margin-auto logos" alt="{{name}} logo">
+        </a>
+      </div>
+    </div>
+  </div>
+</div>
+{{/members}}
+{{/.}}
+</div>
+{{/sections}}
diff --git a/js/templates/wg-members/wg-member-logo-title-with-levels.mustache b/js/templates/wg-members/wg-member-logo-title-with-levels.mustache
new file mode 100644
index 0000000000000000000000000000000000000000..985e0b58277cba6f9a5ae77a0ddbbe3ec1e89a78
--- /dev/null
+++ b/js/templates/wg-members/wg-member-logo-title-with-levels.mustache
@@ -0,0 +1,21 @@
+<h2>{{levelDescription}}</h2>
+<div class="row">
+  {{#item}}
+    <div class="col-xs-24 col-sm-12 col-md-8 margin-bottom-20 m-card">
+      <div class="bordered-box text-center">
+        <div class="box-header background-light-grey vertical-align" data-mh="m-header"">
+          <h3 class="h4 margin-0">
+            <a href="{{urlLinkToLogo}}" title="{{name}}">{{name}}</a>
+          </h3>
+        </div>
+        <div class="box-body vertical-align" style="height: 160px">
+          <div class="image-container">
+            <a href="{{urlLinkToLogo}}" title="{{name}}">
+              <img src="{{logos.web}}" class="img-responsive margin-auto logos" alt="{{name}} logo">
+            </a>
+          </div>
+        </div>
+      </div>
+    </div>
+  {{/item}}
+</div>
diff --git a/js/templates/wg-members/wg-member-only-logos.mustache b/js/templates/wg-members/wg-member-only-logos.mustache
new file mode 100644
index 0000000000000000000000000000000000000000..7493f5f996733e5f0e02fe6fbfb673567e51c28b
--- /dev/null
+++ b/js/templates/wg-members/wg-member-only-logos.mustache
@@ -0,0 +1,12 @@
+{{#item}}
+<li class="members-item flex-center flex-column">
+  <a target="_blank" href="{{urlLinkToLogo}}" class="flex-center">
+    <img alt="{{name}}" class="img-responsive" src="{{logos.web}}">
+  </a>
+  {{#showLevelUnderLogo}}
+    <span>
+      {{showLevelUnderLogo}}
+    </span>
+  {{/showLevelUnderLogo}}
+</li>
+{{/item}}
diff --git a/js/utils/utils.js b/js/utils/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..e0f234979a3d42c5e4c62b06339408ba77be7787
--- /dev/null
+++ b/js/utils/utils.js
@@ -0,0 +1,55 @@
+/*!
+ * Copyright (c) 2021, 2022, 2023 Eclipse Foundation, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * Contributors:
+ *   Zhou Fang <zhou.fang@eclipse-foundation.org>
+ *   Eric Poirier <eric.poirier@eclipse-foundation.org>
+ *   Olivier Goulet <olivier.goulet@eclipse-foundation.org>
+ * 
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+export const displayErrMsg = (element, err = '', errText = 'Sorry, something went wrong, please try again later.') =>
+  (element.innerHTML = `<div class="alert alert-danger" role="alert"><p><strong>Error ${err}</strong></p> <p>${errText}</p></div>`);
+
+const absUrlPattern = /^[a-zA-Z]+:\/\//;
+
+export const validateURL = (url) => {
+  if (!url) return false;
+  
+  const isAbsolute = url.match(absUrlPattern);
+  const base = isAbsolute ? undefined : window.location.href;
+  
+  let isValidate;
+  try {
+    // This will fail if the url is not valid
+    new URL(url, base);
+    isValidate = true;
+  } catch (error) {
+    isValidate = false;
+  }
+  return isValidate;
+};
+
+import querystring from 'querystring';
+
+export const convertToQueryString = params => {
+  // Filters out undefined params
+  const filteredParams = Object.fromEntries(
+    Object
+      .entries(params)
+      .filter(([_, v]) => v !== undefined)
+  );
+
+  return querystring.stringify(filteredParams);
+}
+
+export const scrollToAnchor = () => {
+  const elementId = location.hash.replace('#', '');
+  const element = document.getElementById(elementId);
+  element.scrollIntoView();
+}
\ No newline at end of file
diff --git a/layouts/shortcodes/homepage/members.html b/layouts/shortcodes/homepage/members.html
index d3c06a83bc79784a715a9a093418000b6ec282f2..3900d377cc030d561f4fda015c72443db5eb7a71 100644
--- a/layouts/shortcodes/homepage/members.html
+++ b/layouts/shortcodes/homepage/members.html
@@ -2,7 +2,7 @@
 <div class="margin-top-20">
   <h2 class="heading-line text-center" style="color:rgb(245,147,49); font-weight: bold;">Our Members</h2>
   <div class="container-fluid text-center">
-    <ul class="eclipsefdn-members-list list-inline margin-30 flex-center gap-50" data-ml-wg="openmobility"
-      data-ml-template="only-logos"></ul>
+    <ul class="interest-group-test list-inline margin-30 flex-center gap-50" data-type="interest-group"
+      data-id="openmobility" data-ml-template="only-logos"></ul>
   </div>
 </div>
diff --git a/layouts/shortcodes/members-page.html b/layouts/shortcodes/members-page.html
index ed95985a85da4d6bc792752bce3eb4f412698a9b..430193ac3c24eb814ba4f63b1848b7194ee5bd21 100644
--- a/layouts/shortcodes/members-page.html
+++ b/layouts/shortcodes/members-page.html
@@ -1 +1 @@
-<div id="members" class="eclipsefdn-members-list" data-ml-wg="openmobility" data-ml-template="logo-title-with-levels"></div>
+<ul id="members" class="interest-group-test flex-center list-inline gap-40" data-id="openmobility" data-type="interest-group" data-ml-template="only-logos"></ul>
diff --git a/less/styles.less b/less/styles.less
index d08acd7a6081e27fb6ef47b144c6184e1d0782b0..4eace99b983b6d565412baaee223dccefd9f7d17 100644
--- a/less/styles.less
+++ b/less/styles.less
@@ -23,7 +23,7 @@ h1#join-us {
   color: #4c4d4e;
 }
 
-.eclipsefdn-members-list {
+.eclipsefdn-members-list, .interest-group-test {
   a {
     max-width: 135px;
     img.logos.img-responsive {