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}}'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}}'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 {