diff --git a/package.json b/package.json index 7775a13..6677ae7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-linkedinbadge", - "version": "5.6.2025", + "version": "5.11.2024", "description": "The LinkedIn Badge Rendering React Component is a powerful tool for displaying LinkedIn badges on websites. With customizable options and support for both profile and entity badges, this component offers improved features over LinkedIn's basic implementation. It enhances code organization by separating badge container rendering from dynamic content rendering and efficiently handles asynchronous loading of content from LinkedIn servers. This component also allows for easy management of multiple badges on a page and provides a callback function for tracking badge rendering completion. Make sure to review the licensing information for proper usage.", "repository": { "type": "git", @@ -8,9 +8,10 @@ }, "icon": "https://linkedinliu.com/favicon.ico", "keywords": [ - "Code organization", - "React component", + "React custom component", "LinkedIn", + "Badges", + "LinkedIn badges", "LinkedIn badge", "Linkedin Entity Badge", "Profile badge", @@ -25,7 +26,7 @@ "CSS", "Asynchronous loading", "React state management", - "Badge display", + "LinkedIn Badge display", "Profile badge", "Company badge", "Social media badges", @@ -38,7 +39,6 @@ "Async content loading", "Script tag management", "Life cycle management", - "Modular architecture", "Web performance optimization", "NPM package", "Node.js package", @@ -47,7 +47,6 @@ "Themes visualization (light, dark)", "Type adaptation (vertical, horizontal)", "Badge size", - "Locale management", "Single page application compatibility", "Multi-badge-rendering technique", "Front-end frameworks", @@ -61,11 +60,8 @@ "Script injection prevention", "Separation of concerns", "Client-side rendering", - "Component reusability", "Profile link embedding", - "Enterprise software", "Software development tools", - "Technology widgets", "Interactive web elements", "Dynamic content components", "User interface components", @@ -75,9 +71,6 @@ "Portfolio website enhancement", "Employee identification", "Online credential verification", - "External script", - "License agreement", - "Contractor collaboration tool", "Badge container", "CDN usage" ], @@ -144,7 +137,7 @@ "bugs": { "url": "https://github.com/ZIPING-LIU-CORPORATION/react-linkedin/issues" }, - "homepage": "https://linkedinliu.com/", + "homepage": "https://linkedinliu.com/badges/", "jsdelivr": "./lib/linkedinbadge.js", "exports": { ".": { diff --git a/src/LIRenderAll/index.tsx b/src/LIRenderAll/index.tsx index cd1f8e7..8f70d2a 100644 --- a/src/LIRenderAll/index.tsx +++ b/src/LIRenderAll/index.tsx @@ -4,16 +4,16 @@ import { LinkedInBadgeProps } from "../index"; export type ChildScripts = { [x: number | string]: unknown }; export type LIRenderAllProps = { - scripts: HTMLScriptElement[]; - responsesReceived: number; - expectedResponses: number; - childScripts: ChildScripts; - BADGE_NAMES: string; - badges: Element[] | null; - CALLBACK_NAME: string; - trackingParam: string; - setBadgeDidRender: (arg0: boolean) => void; - badgeDidRender: boolean; + scripts: HTMLScriptElement[]; + responsesReceived: number; + expectedResponses: number; + childScripts: ChildScripts; + BADGE_NAMES: string; + badges: Element[] | null; + CALLBACK_NAME: string; + trackingParam: string; + setBadgeDidRender: (arg0: boolean) => void; + badgeDidRender: boolean; } & LinkedInBadgeProps; /** @@ -34,401 +34,410 @@ export type LIRenderAllProps = { * @note The LinkedInBadge component, on the other hand, is responsible for rendering the container element with the appropriate props and attributes, as well as passing down the necessary props to the LIRenderAll child component. It also manages its own state related to component mounting and badge rendering. This separation of concerns allows for a more modular and maintainable codebase, where the responsibilities of rendering the badge container and managing the dynamic badge content are clearly separated. */ export const LIRenderAll = (props: Partial) => { - const [scripts, setScripts] = React.useState( - props.scripts || [], - ); - const [componentDidUpdate, setComponentDidUpdate] = React.useState( - props.badgeDidRender || false, - ); - - /** - * State variables are not used because these values are updated in the responseHandler function, - * which is a callback function that is not updated on re-renders. Thus, - * these values will not need to be kept per each re-render, but rather per each badge rendering process that - * is handled by an outside API call by responseHandler. - */ - let responsesReceived = props.responsesReceived || 0; - let expectedResponses = props.expectedResponses || 0; - - const setExpectedResponses = React.useCallback((arg0: number) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - expectedResponses = arg0; - }, []); - const BADGE_NAMES = - props.BADGE_NAMES || ".LI-profile-badge, .LI-entity-badge"; - const CALLBACK_NAME = props.CALLBACK_NAME || "LIBadgeCallback"; - const [badges, setBadges] = React.useState( - props.badges || null, - ); - const TRACKING_PARAM = props.trackingParam || "trk=profile-badge"; - - - const childScripts: LIRenderAllProps["childScripts"] = React.useMemo(() => props.childScripts || {}, [props.childScripts]); - - - // These functions are needed because badge markup is added via innerHtml property which does not run script tags - const shouldReplaceNode = React.useCallback( - ( - node: { src: string | number; getAttribute: (name: string) => string | null } & { - tagName: string; - }, - isCreate = 'false', - ) => { - return ( - isScriptNode(node) && - !childScripts[node.src] && - (!(isCreate === 'true') - || ((isCreate === 'true' - ) && !node.getAttribute("data-isartdeco"))) - ); - }, - [childScripts], - ); - - const logDegug = React.useCallback((message: string, type: string, componentName: string) => { - if (props.debug) { - const currentTime = new Date().toLocaleTimeString(); - const componentNameBoldedOnly = `%c${componentName}`; - const messageNormal = `%c${message}`; - const typeColored = `%c${type}`; - console.log(`[${currentTime}] ${typeColored - } - ${componentNameBoldedOnly}: ${messageNormal}`, "color:skyblue", "font-weight: bold", "color: green"); - } - }, [props.debug]); - - /** - * Handles a response from the server. Finds badge matching badgeUid and inserts badgeHtml there - * @param badgeHtml: String representing contents of the badge - * @param badgeUid: UID of the badge to target - **/ - const responseHandler = React.useCallback( - (badgeHtml: string, badgeUid: number) => { - /** - * Tries to clean added tags - **/ - const tryClean = ( - responsesReceived: number, - expectedResponses: number, - badges: Element[] | null, - scripts: HTMLScriptElement[], - CALLBACK_NAME: string, - setScripts: (scripts: HTMLScriptElement[]) => void, + const [scripts, setScripts] = React.useState( + props.scripts || [], + ); + const [componentDidUpdate, setComponentDidUpdate] = React.useState( + props.badgeDidRender || false, + ); + + /** + * State variables are not used because these values are updated in the responseHandler function, + * which is a callback function that is not updated on re-renders. Thus, + * these values will not need to be kept per each re-render, but rather per each badge rendering process that + * is handled by an outside API call by responseHandler. + */ + let responsesReceived = props.responsesReceived || 0; + let expectedResponses = props.expectedResponses || 0; + + const setExpectedResponses = React.useCallback((arg0: number) => { + // eslint-disable-next-line react-hooks/exhaustive-deps + expectedResponses = arg0; + }, []); + const BADGE_NAMES = + props.BADGE_NAMES || ".LI-profile-badge, .LI-entity-badge"; + const CALLBACK_NAME = props.CALLBACK_NAME || "LIBadgeCallback"; + const [badges, setBadges] = React.useState( + props.badges || null, + ); + const TRACKING_PARAM = props.trackingParam || "trk=profile-badge"; + + const useLinkedInApiUrlPure = props.useLinkedInApiUrlPure || false; + const childScripts: LIRenderAllProps["childScripts"] = React.useMemo(() => props.childScripts || {}, [props.childScripts]); + + + // These functions are needed because badge markup is added via innerHtml property which does not run script tags + const shouldReplaceNode = React.useCallback( + ( + node: { src: string | number; getAttribute: (name: string) => string | null } & { + tagName: string; + }, + isCreate = 'false', ) => { - const isDone = - (responsesReceived >= expectedResponses && expectedResponses > 0) || - responsesReceived >= (badges?.length || 0); - if (isDone) { - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (window as any)[CALLBACK_NAME]; - - // remove all script tags - const scriptsTemp = scripts; - scriptsTemp.map(function (script) { - document.body.removeChild(script); - }); - - setScripts([]); - } + return ( + isScriptNode(node) && + !childScripts[node.src] && + (!(isCreate === 'true') + || ((isCreate === 'true' + ) && !node.getAttribute("data-isartdeco"))) + ); + }, + [childScripts], + ); + + const logDegug = React.useCallback((message: string, type: string, componentName: string) => { + if (props.debug) { + const currentTime = new Date().toLocaleTimeString(); + const componentNameBoldedOnly = `%c${componentName}`; + const messageNormal = `%c${message}`; + const typeColored = `%c${type}`; + console.log(`[${currentTime}] ${typeColored + } - ${componentNameBoldedOnly}: ${messageNormal}`, "color:skyblue", "font-weight: bold", "color: green"); } - const cloneScriptNode = ( - node: - | { - setAttribute: (name: string, value: string) => void; - attributes: { name: string; value: string }[]; - } - | HTMLElement - | HTMLScriptElement - | Element, - ) => { - const script = document.createElement("script"); - for (let i = node.attributes.length - 1; i >= 0; i--) { - script.setAttribute(node.attributes[i].name, node.attributes[i].value); - } - return script; - }; - logDegug(`Response received for badgeUid: ${badgeUid}`, "info", "at responseHandler in LIRenderAll"); + }, [props.debug]); + + /** + * Handles a response from the server. Finds badge matching badgeUid and inserts badgeHtml there + * @param badgeHtml: String representing contents of the badge + * @param badgeUid: UID of the badge to target + **/ + const responseHandler = React.useCallback( + (badgeHtml: string, badgeUid: number) => { + /** + * Tries to clean added tags + **/ + const tryClean = ( + responsesReceived: number, + expectedResponses: number, + badges: Element[] | null, + scripts: HTMLScriptElement[], + CALLBACK_NAME: string, + setScripts: (scripts: HTMLScriptElement[]) => void, + ) => { + const isDone = + (responsesReceived >= expectedResponses && expectedResponses > 0) || + responsesReceived >= (badges?.length || 0); + if (isDone) { + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (window as any)[CALLBACK_NAME]; + + // remove all script tags + const scriptsTemp = scripts; + scriptsTemp.map(function (script) { + document.body.removeChild(script); + }); + + setScripts([]); + } + } + const cloneScriptNode = ( + node: + | { + setAttribute: (name: string, value: string) => void; + attributes: { name: string; value: string }[]; + } + | HTMLElement + | HTMLScriptElement + | Element, + ) => { + const script = document.createElement("script"); + for (let i = node.attributes.length - 1; i >= 0; i--) { + script.setAttribute(node.attributes[i].name, node.attributes[i].value); + } + return script; + }; + logDegug(`Response received for badgeUid: ${badgeUid}`, "info", "at responseHandler in LIRenderAll"); + + + // eslint-disable-next-line react-hooks/exhaustive-deps + responsesReceived += 1; + + + const defaultWidth = 330; // max possible width + const defaultHeight = 300; // max possible height + + for (const badge of badges || []) { + + logDegug(`Checking badge with uid: ${badge.getAttribute("data-uid")}`, "info", "at responseHandler in LIRenderAll"); + // isCreate needed to prevent reloading artdeco script tag + const isCreate = badge.getAttribute("data-iscreate"); + const uid = parseInt(badge.getAttribute("data-uid") || "0", 10); + if (uid === badgeUid) { + const badgeMarkup = `${badgeHtml + }`; + const iframe = document.createElement("iframe"); + + iframe.onload = function () { + const iframeBody = iframe.contentWindow?.document.body; + // 5 px buffer to avoid the badge border being cut off. + iframe.setAttribute( + "height", + `${(iframeBody?.scrollHeight || defaultHeight) + 5}`, + ); + iframe.setAttribute( + "width", + `${iframeBody?.scrollWidth || defaultWidth + 5}`, + ); + }; + + const replaceScriptTags = (badge: Element, isCreate = 'false') => { + logDegug(`Replacing script tags for badge with uid: ${badge.getAttribute("data-uid")}`, "info", "at responseHandler in LIRenderAll"); + const scriptsFoundNow = badge.querySelectorAll("script"); + let i, len, script; + for (i = 0, len = scriptsFoundNow.length; i < len; i++) { + script = scriptsFoundNow[i]; + if (shouldReplaceNode(script, isCreate)) { + const newScript = cloneScriptNode(script); + badge.appendChild(newScript); + badge.removeChild(script); + } + } + }; + + iframe.setAttribute("frameBorder", "0"); + iframe.style.display = "block"; + badge.appendChild(iframe); + if (iframe.contentWindow) { + logDegug(`Writing badge markup for badge with uid: ${badge.getAttribute("data-uid")}`, "info", "at responseHandler in LIRenderAll"); + iframe.contentWindow.document.open(); + iframe.contentWindow.document.write(badgeMarkup); + iframe.contentWindow.document.close(); + } + replaceScriptTags(badge, isCreate || 'false'); + } + } + tryClean( + responsesReceived, + expectedResponses, + badges, + scripts, + CALLBACK_NAME, + setScripts, + ); + }, + [badges, responsesReceived, CALLBACK_NAME, scripts, expectedResponses, logDegug, shouldReplaceNode], + ); + + const jsonp = React.useCallback( + (url: string) => { + + const script = document.createElement("script"); + script.src = url; + const scriptsState = scripts; + scriptsState.push(script); + script.id = `${CALLBACK_NAME}-${scriptsState.length + 1}`; + logDegug(`Adding script tag with id: ${script.id}`, "info", "at jsonp in LIRenderAll"); + setScripts(scriptsState); + document.body.appendChild(script); + }, + [scripts, CALLBACK_NAME, logDegug], + ); + + const renderBadge = React.useCallback( + (badge: Element) => { + const getBadgeKeyQueryParams = (badge: Element): string[] => { + // Convert the badge's attributes to an array + const attributes = Array.from(badge.attributes); + + // Filter the attributes to include only those starting with 'data-key-' + const keyAttributes = attributes.filter((attr: Attr) => + attr.name.startsWith("data-key-"), + ); + // Map the filtered attributes to key-value pairs + const keyValuePairs = keyAttributes.map((attr: Attr) => { + const key = attr.name.replace("data-", "").toLowerCase(); + const value = attr.value; + return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + }); + + logDegug(`Badge key query params: ${keyValuePairs}`, "info", "at renderBadge in LIRenderAll"); + + return keyValuePairs; + }; + const size = props.size || badge.getAttribute("data-size") || "medium"; + const locale = + props.locale || badge.getAttribute("data-locale") || "en_US"; + const type = props.type || badge.getAttribute("data-type") || "VERTICAL"; + const theme = props.theme || badge.getAttribute("data-theme") || "dark"; + const vanity = + props.vanity || badge.getAttribute("data-vanity") || "☯liu"; + const version = + props.version || badge.getAttribute("data-version") || "v1"; + const isEI = badge.hasAttribute("data-ei") || false; + const entity = badge.getAttribute("data-entity") || "PROFILE"; + const isCreatePage = badge.hasAttribute("data-iscreate") || false; + const uid = Math.round(1000000 * Math.random()); + let baseUrl = generateUrl(useLinkedInApiUrlPure, isEI); + console.warn("baseUrl", baseUrl) + let queryParams = [ + "locale=" + encodeURIComponent(locale), + "badgetype=" + encodeURIComponent(type), + "badgetheme=" + encodeURIComponent(theme), + "uid=" + encodeURIComponent(uid), + "version=" + encodeURIComponent(version), + ]; + + if (version === "v2") { + baseUrl += "view"; + queryParams.push("badgesize=" + encodeURIComponent(size || "medium")); + queryParams.push("entity=" + encodeURIComponent(entity || "PROFILE")); + const badgeKeyQueryParams = getBadgeKeyQueryParams(badge); + queryParams = queryParams.concat(badgeKeyQueryParams); + } else { + baseUrl += "profile"; + queryParams.push("maxsize=" + encodeURIComponent(size)); + queryParams.push("trk=" + encodeURIComponent(TRACKING_PARAM)); + queryParams.push("vanityname=" + encodeURIComponent(vanity)); + } + + if (isCreatePage) { + queryParams.push("fromCreate=true"); + } + + const url = baseUrl + "?" + queryParams.join("&"); + console.warn("url", url); + badge.setAttribute("data-uid", `${uid}`); + jsonp(url); //Calls responseHandler when done + }, + [ + props.size, + props.locale, + props.type, + props.theme, + props.vanity, + props.version, + jsonp, + logDegug, + TRACKING_PARAM, + ], + ); - // eslint-disable-next-line react-hooks/exhaustive-deps - responsesReceived += 1; - const defaultWidth = 330; // max possible width - const defaultHeight = 300; // max possible height + const isScriptNode = (node: { tagName: string }) => { + return node.tagName === "SCRIPT"; + }; - for (const badge of badges || []) { - logDegug(`Checking badge with uid: ${badge.getAttribute("data-uid")}`, "info", "at responseHandler in LIRenderAll"); - // isCreate needed to prevent reloading artdeco script tag - const isCreate = badge.getAttribute("data-iscreate"); - const uid = parseInt(badge.getAttribute("data-uid") || "0", 10); - if (uid === badgeUid) { - const badgeMarkup = `${badgeHtml - }`; - const iframe = document.createElement("iframe"); + React.useEffect(() => { + const badgesArray = badges || Array.from(document.querySelectorAll(BADGE_NAMES)); - iframe.onload = function () { - const iframeBody = iframe.contentWindow?.document.body; - // 5 px buffer to avoid the badge border being cut off. - iframe.setAttribute( - "height", - `${(iframeBody?.scrollHeight || defaultHeight) + 5}`, - ); - iframe.setAttribute( - "width", - `${iframeBody?.scrollWidth || defaultWidth + 5}`, - ); - }; - - const replaceScriptTags = (badge: Element, isCreate = 'false') => { - logDegug(`Replacing script tags for badge with uid: ${badge.getAttribute("data-uid")}`, "info", "at responseHandler in LIRenderAll"); - const scriptsFoundNow = badge.querySelectorAll("script"); - let i, len, script; - for (i = 0, len = scriptsFoundNow.length; i < len; i++) { - script = scriptsFoundNow[i]; - if (shouldReplaceNode(script, isCreate)) { - const newScript = cloneScriptNode(script); - badge.appendChild(newScript); - badge.removeChild(script); - } - } - }; - - iframe.setAttribute("frameBorder", "0"); - iframe.style.display = "block"; - badge.appendChild(iframe); - if (iframe.contentWindow) { - logDegug(`Writing badge markup for badge with uid: ${badge.getAttribute("data-uid")}`, "info", "at responseHandler in LIRenderAll"); - iframe.contentWindow.document.open(); - iframe.contentWindow.document.write(badgeMarkup); - iframe.contentWindow.document.close(); - } - replaceScriptTags(badge, isCreate || 'false'); - } - } - tryClean( - responsesReceived, - expectedResponses, - badges, - scripts, - CALLBACK_NAME, - setScripts, - ); - }, - [badges, responsesReceived, CALLBACK_NAME, scripts, expectedResponses, logDegug, shouldReplaceNode], - ); - - const jsonp = React.useCallback( - (url: string) => { - - const script = document.createElement("script"); - script.src = url; - const scriptsState = scripts; - scriptsState.push(script); - script.id = `${CALLBACK_NAME}-${scriptsState.length + 1}`; - logDegug(`Adding script tag with id: ${script.id}`, "info", "at jsonp in LIRenderAll"); - setScripts(scriptsState); - document.body.appendChild(script); - }, - [scripts, CALLBACK_NAME, logDegug], - ); - - const renderBadge = React.useCallback( - (badge: Element) => { - const getBadgeKeyQueryParams = (badge: Element): string[] => { - // Convert the badge's attributes to an array - const attributes = Array.from(badge.attributes); - - // Filter the attributes to include only those starting with 'data-key-' - const keyAttributes = attributes.filter((attr: Attr) => - attr.name.startsWith("data-key-"), - ); - - // Map the filtered attributes to key-value pairs - const keyValuePairs = keyAttributes.map((attr: Attr) => { - const key = attr.name.replace("data-", "").toLowerCase(); - const value = attr.value; - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - }); - - logDegug(`Badge key query params: ${keyValuePairs}`, "info", "at renderBadge in LIRenderAll"); - - return keyValuePairs; - }; - const size = props.size || badge.getAttribute("data-size") || "medium"; - const locale = - props.locale || badge.getAttribute("data-locale") || "en_US"; - const type = props.type || badge.getAttribute("data-type") || "VERTICAL"; - const theme = props.theme || badge.getAttribute("data-theme") || "dark"; - const vanity = - props.vanity || badge.getAttribute("data-vanity") || "☯liu"; - const version = - props.version || badge.getAttribute("data-version") || "v1"; - const isEI = badge.hasAttribute("data-ei") || false; - const entity = badge.getAttribute("data-entity") || "PROFILE"; - const isCreatePage = badge.hasAttribute("data-iscreate") || false; - const uid = Math.round(1000000 * Math.random()); - let baseUrl = generateUrl(isEI); - let queryParams = [ - "locale=" + encodeURIComponent(locale), - "badgetype=" + encodeURIComponent(type), - "badgetheme=" + encodeURIComponent(theme), - "uid=" + encodeURIComponent(uid), - "version=" + encodeURIComponent(version), - ]; - - if (version === "v2") { - baseUrl += "view"; - queryParams.push("badgesize=" + encodeURIComponent(size || "medium")); - queryParams.push("entity=" + encodeURIComponent(entity || "PROFILE")); - const badgeKeyQueryParams = getBadgeKeyQueryParams(badge); - queryParams = queryParams.concat(badgeKeyQueryParams); - } else { - baseUrl += "profile"; - queryParams.push("maxsize=" + encodeURIComponent(size)); - queryParams.push("trk=" + encodeURIComponent(TRACKING_PARAM)); - queryParams.push("vanityname=" + encodeURIComponent(vanity)); - } - if (isCreatePage) { - queryParams.push("fromCreate=true"); - } - const url = baseUrl + "?" + queryParams.join("&"); - badge.setAttribute("data-uid", `${uid}`); - jsonp(url); //Calls responseHandler when done - }, - [ - props.size, - props.locale, - props.type, - props.theme, - props.vanity, - props.version, - jsonp, - logDegug, - TRACKING_PARAM, - ], - ); - const isScriptNode = (node: { tagName: string }) => { - return node.tagName === "SCRIPT"; - }; + const tryClean = () => { + logDegug("Cleaning up", "info", "at useEffect in LIRenderAll" + ", Expected responses: " + expectedResponses + ", Responses received: " + responsesReceived + ", Badges length: " + badgesArray.length); + if ( + responsesReceived >= expectedResponses && expectedResponses > 0 || + (responsesReceived >= badgesArray.length) + ) { + logDegug("Cleaning up", "info", "Deleting callback in window"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (window as any)[CALLBACK_NAME]; + scripts.forEach((script) => document.body.removeChild(script)); + } + }; + return () => { + tryClean(); + }; + }, [ + badges, + BADGE_NAMES, + expectedResponses, + responsesReceived, + scripts, + logDegug, + CALLBACK_NAME + ]); - React.useEffect(() => { - const badgesArray = badges || Array.from(document.querySelectorAll(BADGE_NAMES)); + /** + * Effect hook to render badges when the component mounts, where renderBadge calls + * are made and state status for the updated badges is set + */ + React.useEffect(() => { + let queriedBadges = Array.from(document.querySelectorAll(BADGE_NAMES)); + if (badges === null) { + queriedBadges = Array.from(document.querySelectorAll(BADGE_NAMES)); + logDegug(`Setting badges to: ${Array.prototype.slice.call(document.querySelectorAll(BADGE_NAMES))}`, "info", "at useEffect in LIRenderAll"); + setBadges(queriedBadges); + } + for (const badge of queriedBadges) { + const rendered = badge.getAttribute("data-rendered"); + if (!rendered) { + logDegug(`"Badge not rendered yet" - badgeid: ${badge.id}, "componentDidUpdate", ${componentDidUpdate}`, "info", "at second useEffect in LIRenderAll, setting " + `${CALLBACK_NAME} global as part of linkedin's rendering process to trigger and coupled specific to this current badge, vanity: ${props.vanity || badge.getAttribute("data-vanity")}`); + badge.setAttribute("data-rendered", `${true}`); - const tryClean = () => { - logDegug("Cleaning up", "info", "at useEffect in LIRenderAll" + ", Expected responses: " + expectedResponses + ", Responses received: " + responsesReceived + ", Badges length: " + badgesArray.length); + renderBadge(badge); + setComponentDidUpdate(true); + if (props.setBadgeDidRender) { + props.setBadgeDidRender(true); + } + logDegug(`Badge rendered: ${badge}`, "info", "at useEffect in LIRenderAll, setting " + `${CALLBACK_NAME} global as part of linkedin's rendering process to trigger and coupled specific to this current badge, vanity: ${props.vanity || badge.getAttribute("data-vanity")}`); - if ( - responsesReceived >= expectedResponses && expectedResponses > 0 || - (responsesReceived >= badgesArray.length) - ) { - logDegug("Cleaning up", "info", "Deleting callback in window"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (window as any)[CALLBACK_NAME]; - scripts.forEach((script) => document.body.removeChild(script)); - } - }; - return () => { - tryClean(); - }; - }, [ - badges, - BADGE_NAMES, - expectedResponses, - responsesReceived, - scripts, - logDegug, - CALLBACK_NAME - ]); - - - - /** - * Effect hook to render badges when the component mounts, where renderBadge calls - * are made and state status for the updated badges is set - */ - React.useEffect(() => { - let queriedBadges = Array.from(document.querySelectorAll(BADGE_NAMES)); - if (badges === null) { - queriedBadges = Array.from(document.querySelectorAll(BADGE_NAMES)); - - logDegug(`Setting badges to: ${Array.prototype.slice.call(document.querySelectorAll(BADGE_NAMES))}`, "info", "at useEffect in LIRenderAll"); - setBadges(queriedBadges); - } - - for (const badge of queriedBadges) { - const rendered = badge.getAttribute("data-rendered"); - if (!rendered) { - - logDegug(`"Badge not rendered yet" - badgeid: ${badge.id}, "componentDidUpdate", ${componentDidUpdate}`, "info", "at second useEffect in LIRenderAll, setting " + `${CALLBACK_NAME} global as part of linkedin's rendering process to trigger and coupled specific to this current badge, vanity: ${props.vanity || badge.getAttribute("data-vanity")}`); - badge.setAttribute("data-rendered", `${true}`); - - renderBadge(badge); - setComponentDidUpdate(true); - if (props.setBadgeDidRender) { - props.setBadgeDidRender(true); - } - logDegug(`Badge rendered: ${badge}`, "info", "at useEffect in LIRenderAll, setting " + `${CALLBACK_NAME} global as part of linkedin's rendering process to trigger and coupled specific to this current badge, vanity: ${props.vanity || badge.getAttribute("data-vanity")}`); - - setExpectedResponses(expectedResponses + 1); - } + setExpectedResponses(expectedResponses + 1); + } - logDegug(`Looping through badges, ${componentDidUpdate}, "rendered", ${rendered}, "expectedResponses", ${expectedResponses}, "responsesReceived", ${responsesReceived}`, "info", "at second useEffect in LIRenderAll"); + logDegug(`Looping through badges, ${componentDidUpdate}, "rendered", ${rendered}, "expectedResponses", ${expectedResponses}, "responsesReceived", ${responsesReceived}`, "info", "at second useEffect in LIRenderAll"); - } + } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any)[CALLBACK_NAME] = responseHandler; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any)[CALLBACK_NAME] = responseHandler; - }, [ - badges, - BADGE_NAMES, - componentDidUpdate, - renderBadge, - responseHandler, - expectedResponses, - responsesReceived, - logDegug, - setExpectedResponses, - CALLBACK_NAME, - props - ]); - return <> + }, [ + badges, + BADGE_NAMES, + componentDidUpdate, + renderBadge, + responseHandler, + expectedResponses, + responsesReceived, + logDegug, + setExpectedResponses, + CALLBACK_NAME, + props + ]); + return <> - ; + ; }; function isCNDomain() { - if (typeof window !== "undefined") { - const hostName = (window.location && window.location.hostname) || ""; - return /linkedin(-ei)?.cn$/.test(hostName); - } + if (typeof window !== "undefined") { + const hostName = (window.location && window.location.hostname) || ""; + return /linkedin(-ei)?.cn$/.test(hostName); + } - return false; + return false; } -function generateUrl(isEI: boolean) { - const domainPrefix = isEI - ? "https://badges.linkedin-ei" - : "https://badges.linkedin"; - if (isCNDomain()) { - return domainPrefix + ".cn/"; - } +function generateUrl( + isLinkedIn = false, + isEI?: boolean) { + + if (isLinkedIn) { + const domainPrefix = isEI + ? "https://badges.linkedin-ei" + : "https://badges.linkedin"; + if (isCNDomain()) { + return domainPrefix + ".cn/"; + } - return domainPrefix + ".com/"; + return domainPrefix + ".com/"; + } else { + return `https://ziping.liu.academy/api/v1/linkedin/`; + } } export default LIRenderAll; diff --git a/src/index.tsx b/src/index.tsx index e36e830..ae0e3ba 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,6 +23,7 @@ export type LinkedInBadgeProps = { style: React.CSSProperties; id: string; script_src: string; + useLinkedInApiUrlPure: boolean; name: string; debug: boolean; }; @@ -42,6 +43,7 @@ export type LinkedInBadgeProps = { * @param {string} [props.id] - The ID to assign to the badge container. * @param {string} [props.script_src] - The URL of an external script to include for the badge. * @param {string} [props.name] - The name to display on the badge. + * @param {useLinkedInApiUrlPure} [props.useLinkedInApiUrlPure] - Defaults as false, if set true, when call LinkedIn's badge API, the API request won't be proxied through Ziping Liu's server. The proxied request is used to remove security issues that some browsers may have due to the returned content of LinkedIn's API having certain headers that the browser regards as possibly harmful (although in this case its a red herring given badge content from LinkedIn does not contain malicious content or content that may be inappropriately used to harm the user's computer). * @returns {React.ReactElement} The rendered LinkedIn badge component. * @description This implementation uses two React components - LinkedInBadge and LIRenderAll. * LinkedInBadge is the parent component responsible for @@ -60,6 +62,7 @@ export default function LinkedInBadge(props: Partial) { const type = props.type || "VERTICAL"; const vanity = props.vanity || "☯liu"; const version = props.version || "v1"; + const useLinkedInApiUrlPure = props.useLinkedInApiUrlPure || false; const vanityEncoded = encodeURIComponent(vanity); const name = props.name || ""; const url = `https://www.linkedin.com/in/${vanityEncoded}?trk=profile-badge`; @@ -117,12 +120,13 @@ export default function LinkedInBadge(props: Partial) { badgeDidRender === false && componentDidMount && (