How We Built It: Atlas Custom Header
With the latest Atlas redesign, we reimagined our Community header to build a more intuitive navigation and search experience. Learn more about our redesign in Atlas Redesign Project Story. Our custom header experience differs depending on: whether the user is anonymous or registered whether the user is on a desktop or a mobile device whether the user is on the Community Page or any other page in Atlas the user's role(s) in the community This article includes a zip file with our commented source code for the components, functions, macros, and CSS we used to build our customization. Let's look at the different ways the header renders depending on user, device, and page. Anonymous view (Desktop) Anonymous users have limited access to Atlas. You'll notice that some areas, such as the Dev Network, are not accessible to anonymous users at all. Registered user view (Desktop) Registered users see all menu items that they have permission to view based on their role(s) in Atlas. Notice that the registered user in the gif below has many more options than our anonymous friend above. Mobile/Tablet view Roles determine what menu items appear in mobile or tablet navigation, just as they do in a desktop viewport. The mobile/Tablet header uses the out-of-the-box Community Navigation (Slide-out) component, which uses a hamburger menu control. Community Page view (Desktop) The Atlas landing page (the Community Page quilt) includes a hero component with a search box. When the search box in the hero is in view, we don't include a search bar in the header. Once the user scrolls past the hero component, the search icon appears. This behavior uses JavaScript injected to our custom header component via the liaAddScript FreeMarker directive. Learn more about liaAddScript. Non-Community Page view (Desktop) On all other pages, the header includes the search icon and expanding/collapsing search box. Customization source files Our source code is in a zip file attached to this article. To keep our component code easier to understand, we separate the business logic code for creating the different sections of the navigation menu into separate functions and macros outside of our custom header component (custom-new-header.ftl). This makes it easier to read and understand the display that builds the header within the component code. You'll find these files in the attachment: custom-new-header.ftl - the component that renders the Atlas header theme-lib-common-functions.ftl - a macro that includes common functions used throughout Atlas customizations function executeLiQLQuery - logic to run LiQL queries and define the response format new-header-util-macro.ftl - a set of macros specific to the Atlas header customization function getUserInfo - gets a user's role(s) in the Atlas community and sets the role data in the user cache macro renderMainMenu - contains the logic to retrieve and render the main menu items macro dropdownButton - renders the dropdown that contains the submenu items for each main menu item macro renderSubMenu - contains the logic to retrieve and list the submenu items for each main menu item macro renderCasePortalMenu - contains the logic to render the Support menu item in the main menu _new-header.scss - The SASS used for the custom header styling avIncludes.pngCustom header configuration We placed our custom header component within the Page Header section of the Community Wrapper in Studio. We call the component with the following HTML. The cacheTimeMs parameter is used to set the component cache time-to-live (TTL). <div class="custom-header"><@component id="custom.new-header" cacheTimeMs="60000" /></div> Caching considerations Component-level caching We use component caching to cache our custom header component. This caches the entire menu for each user. Because the content of the menu can vary based on the user's roles and permissions, we set the variation parameter to current_user and set the anonymousOnly parameter to false. The cache TTL is 5 minutes. You'll find our component caching code near the top of custom-new-header.ftl in the source file attachment. Learn about component caching in Component caching with liaMarkupCache. User-level caching The menu items displayed in the header's main navigation menu depends on the current user's roles. We store role data in the user cache so that we can check the cache rather than running a series of if/else statements each time we need to decide whether or not to display a menu item. The user cache logic is contained in a function called getUserInfo. You'll find the code for getUserInfo in the new-header-util-macro.ftl file in the source file attachment. Learn more about the user cache in User and application caching and usercache.3.5KViews
Sign in to react to this post9Comments
How We Built It: Author Stamps
As part of our ongoing series featuring customizations and solutions we’ve created for Khoros Atlas, this guide is going to give you a step-by-step breakdown of how you can add a colorful author stamp to comments and replies made by the original author of a blog post or message. In the above image, you’ll see a magenta stamp with the word AUTHOR in it. This stamp only appears for the original author of a blog post or message within their replies or comments. Creating this stamp is pretty simple. It involves the creation of a component, the addition of some custom styling, and the component’s placement on two item types. Step 1: Create the component The author-badge component needs to be created so we can place it in the appropriate places using the Quilt. Here’s the process: Navigate to Studio > Components within the Community Admin. Select New Component. Assign the new component the name author-badge . Drop in the following code snippet. <#if (env.context.message.author.id)?? && (env.context.message.parent)?? && (page.context.thread.topicMessage.author.id)??> <#assign userId = env.context.message.author.id /> <#assign topicMessageId = page.context.thread.topicMessage.author.id/> <#if (userId)?? && (topicMessageId)?? > <#if (topicMessageId)?? && userId == topicMessageId> <div class="author.badge"> <span class="badge1" data-badge="AUTHOR"></span> </div> </#if> </#if> </#if> Select Save to create the new component. The snippet performs several important tasks. First, it captures the ID of the author and message. If the author ID matches the ID of the author of the message, the component displays a stamp with the word AUTHOR . That badge’s styling is provided by the author.badge and badge1 classes. Step 2: Add custom styling to the skin The next step involves locating the skin your site is currently using, and editing the _styles.scss file to include new styling for the stamp. Here is how you can accomplish this: Navigate to Admin > Display > Skins within Community Admin. Take note of the skin currently being used. This skin has a filled-in radial field next to it on the list. Navigate to Studio > Community Style. Locate the current, active skin in the sidebar and select it. Select the CSS tab. Under sass in the file tree, choose the _style.scss file. Add the below snippet to the stylesheet. .badge1 { position:relative; padding-left:2vh; } .badge1[data-badge]:after { content:attr(data-badge); top:2px; right:3px; font-size:.8em; background:#E21A9A; color:white; width:40%; height:15px; text-align:center; line-height:18px; box-shadow:0 0 1px #333; font-weight:bold; } Select Save. Now we have the component created, and the styling to go along with it. All we need to do is assign the component to the page(s) and put it to use. Step 3: Add the component to the appropriate message types The final step actually applies the new component to the areas in the site in which you want to have them appear. There are two-page types we are focusing on in this tutorial: Blog Reply Message Forum Message Here are the steps: Navigate to Studio > Page in Community Admin. Click the Change link next to the page name. Select the Blogs > Blog Reply Message page. In the sidebar, locate the author-badge component. It is found in Custom Components. Select the author-badge component to add it to the quilt. Make sure the author-badge component is placed in the header-left area. We set ours at the end, so it appears last. Select Save to lock in your changes. Once this is done, locate a blog post with replies from the author to validate the change. You should see a new AUTHOR stamp in any reply left by the original author of the blog post. Now, do the same thing again, but instead of Blogs > Blog Reply Message, select Forums > Forum Message. That’s it! You now have a shiny new AUTHOR stamp on replies and comments left by the original author of each blog post and message.3.4KViews
Sign in to react to this post14Comments
How We Built It: Access Signposting
Atlas users have asked us how we built our customization that displays a fixed indicator (we call it a signpost) highlighting the access level of content on the current page being viewed. You can easily incorporate this customization into your community using the examples and source code in this article. Thanks to Claudius and AndyK for writing and sharing this customization! About the customization Make this your own Customization source code We built a custom component with FreeMarker that determines the right signpost to use based on the node in context. We use CSS to change the styling, text, and hover text based on the access level. About the customization The component logic First, we mapped out which nodes match which access levels we want to highlight (Public, Private, Internal, Registered, and Archive). We created a series of "if" statements using the OR comparison operator (||) to evaluate the node in context. If the page being viewed belongs to a node that matches the condition of the comparison logic, then we display the related access signpost. In some cases, we evaluate based on the current node's ancestors (container nodes in the Community Structure like a category or group hub). In other cases, we evaluate by node ID. For example, we want the Events category and all of its child nodes to be Public. In the snippet below, the comparison using coreNode.hasAncestor handles the case where the node in context is a child node within the Events category. The comparison using coreNode.id handles the case when the current node in context is the Events category Category Page: coreNode.hasAncestor("events") == true || coreNode.id == "events" Let's go a step further. This next snippet shows the logic we use to evaluate whether a node meets the "Public" access level criteria. This logic displays our signpost on the Events Category Page, all subpages in the Events category, and our Contact Support board. We create and style the signpost using the public-logo-link class. Note: We simplified the code in this snippet. See the full component source code at the end of this article. <#assign public = "" /> <#-- Define the Public criteria: The node being viewed must be the "events" node, a child node within the "events" node, or the "contact-support" node. --> <#if coreNode.id == "events" || coreNode.hasAncestor("events") == true || coreNode.id == "contact-support"> <#-- If the node in context meets the Public criteria, display the Public indicator with a Help icon that reveals a tooltip describing the access level on hover over. --> <span class="public-logo-link" title="Visible to the public including non-registered users.">Public <span class="lia-img-icon-help lia-fa-icon lia-fa-help lia-fa" aria-label="Help Icon" role="img" id="display"></span></span> Learn more about FreeMarker with Khoros Communities Learn more about the coreNode FreeMarker context object Learn more about if statements with FreeMarker The component styling For each access level, we created a separate class. Each signpost class uses a different value for the color , border, and box-shadow attributes. Atlas uses a Responsive-based skin, so we put our custom CSS in _style.scss in Studio. See Skin architecture to learn more about custom SASS and Responsive skins. Here is our CSS for our Public signpost. .public-logo-link { background: white; border-radius: 5px; border: 1px solid #00004B; bottom: 20px; box-shadow: 0px 0px 5px #00004B; color: #00004B; display: inline-block; font-size: 28px; font-weight: 300; left: 20px; line-height: 28px; padding: 10px; position: fixed; z-index: 1000; @include media(phone-and-down) { font-size: 20px; } } Adding the component to a page We chose to put our custom signpost component in the Page Wrapper. This ensures that the logic will be evaluated on every page. We add the component to the wrapper code using the component FreeMarker directive. We put the following in the Page Header section of Studio > Community Style > {skin ID} > Wrapper > Page Header. <@component id="custom.location-logo" /> Future improvements Currently, we do not use any caching with this particular component. As a future enhancement, we plan to evaluate component caching and application caching to improve performance. Make this your own You can tailor this customization to fit your needs. Use the example code in this section to try this out in your stage environment. Create your component In Studio > Components, create a new component. Use this sample code to get you started. Also, see our examples to display the signpost based on registration status or role. <#--Create a new component and give it a unique name -- for example, custom.location-logo --> <#-- Create a variable for the first type of signpost. The variable below is used with our Public signpost. For a different indicator, you might name the variable "private" or "registered" --> <#assign public = "" /> <#-- Define your criteria for the first kind of signpost. For each node on which to display the indicator, add a separate instance coreNode.id == "{node-id}". Where {node-id} is the ID of a board, category, or group hub. Separate each case to evaluate with an OR symbol (||). To display all content within a node (including nested nodes), pass a container node ID (a category or group hub ID) to coreNode.hasAncestor. If you want the signpost to appear on the container node page, also include coreNode.id == "{container-node-id}" Remove the coreNode.hasAncestor condition if that case doesn't apply to your version of the customization. You may add as many conditions as needed. --> <#if coreNode.hasAncestor("{board id}") == true || coreNode.id == "{board id}" || ... > <#-- The class element must match a related style in your skin SCSS. Edit the title element to set the tooltip text. You can also set the text with a text key and the text.format method. If you do not want tooltip text, remove the "text" element. Replace Public with whatever text you would like to appear on the signpost or use a text key. Replace the span element with an href if you would prefer to have the signpost link elsewhere, such as a registration page for content specific to registered users. (E.g., <a href="/t5/my.registration.page" target = "_blank" class="registered-logo-link" title="This content is requires an Atlas login ">Registered Users</a> --> <span class="public-logo-link" title="This content is publicly available">Public</span> </#if> <#-- To add code for a second indicator, copy the full snippet above, paste it here, and then customize it for your next indicator use case --> Display the signpost to registered users only If you would like this to show only to signed-in users, you can wrap the code in the component with this: <#if user.registered == true> {component code goes here} </#if> Display the signpost to users with a specific role If you would like to limit it by role(s), you can wrap the code in the component with this: <#assign user_has_role = false /> <#if user.anonymous == false> <#list restadmin("/users/id/${user.id?c}/roles").roles.role as role> <#if role.name?? && ((role.name == "{role name")||(role.name == "{role name")||(role.name == "{role name}"))> <#-- add or remove the role.name === as needed <#assign user_has_role = true /> </#if> </#list></#if> <#if user_has_role > <#else> </#if> <#if user_has_role = true> {component text goes here} </#if> Add your custom styling Add the SCSS from the public-logo-link example in The component styling. Create a separate style for each signpost type you want to use. Be sure to reference the correct style in the class element in your component code. Add the component to your Page Wrapper Go to Studio > Community Style > {Skin} > Wrapper and place the following in the Page Header section. <#-- This can go anywhere in the Page Header section --> <#-- Use the same name as the component you created above. --> <@component id="custom.location-logo" /> Customization source code <#assign internal = "" /> <#if coreNode.hasAncestor("CommInternalDocs") == true || coreNode.id == "CommInternalDocs" || coreNode.hasAncestor("CareInternalDocs") == true || coreNode.id == "CareInternalDocs" || coreNode.hasAncestor("MktgInternalDocs") == true || coreNode.id == "MktgInternalDocs" > <a href="/t5/Internal-Community/ct-p/Employees" class="internal-logo-link">Internal</a> <#assign private = "" /> <#elseif coreNode.hasAncestor("communitydoc") == true || coreNode.id == "communitydoc" || coreNode.hasAncestor("lithiumjx") == true || coreNode.id == "lithiumjx" || coreNode.id == "relnote" || coreNode.id == "community-product-coaching" || coreNode.id == "Lithium_Ideas" || coreNode.hasAncestor("marketingdocs") == true || coreNode.id == "marketingdocs" || coreNode.id == "marketingreleasenotes" || coreNode.id == "marketing-product-coaching" || coreNode.id == "spredfastblog" || coreNode.id == "marketingideas" || coreNode.hasAncestor("smm") == true || coreNode.id == "smm" || coreNode.id == "smmreleasenotes" || coreNode.id == "care-product-coaching" || coreNode.id == "reachresponse" || coreNode.id == "smm-ideas" || coreNode.hasAncestor("customer-hub") == true || coreNode.id == "customer-hub" || coreNode.id == "Education@tkb" || coreNode.id == "lisupport"> <span class="private-logo-link" title="Visible to: Customers, Parters, and Khoros staff.">Private <span class="lia-img-icon-help lia-fa-icon lia-fa-help lia-fa" aria-label="Help Icon" role="img" id="display"></span> </#if> <#assign public = "" /> <#if coreNode.hasAncestor("events") == true || coreNode.id == "events" || coreNode.id == "spredfastdiscussions" || coreNode.id == "socialmediablog" || coreNode.id == "jx-blog" || coreNode.id == "onlinecommunitiesblog" || coreNode.id == "technology" || coreNode.id == "productcoachinghome" || coreNode.id == "kudosawards2020" || coreNode.id == "Help@tkb" || coreNode.id == "lithiumblog" || coreNode.id == "Careers-Forum" || coreNode.id == "Help" || coreNode.id == "SupportInformation" || coreNode.id == "contact-support"> <span class="public-logo-link" title="Visible to the public including non-registered users.">Public <span class="lia-img-icon-help lia-fa-icon lia-fa-help lia-fa" aria-label="Help Icon" role="img" id="display"></span> </#if> <#assign registered = "" /> <#if coreNode.hasAncestor("Developer") == true || coreNode.id == "Developer"> <span class="private-logo-link" title="Visible to: Registered Users, Customers, Partners, and Khoros staff.">Registered Users <span class="lia-img-icon-help lia-fa-icon lia-fa-help lia-fa" aria-label="Help Icon" role="img" id="display"></span> </#if> <#assign archives = "" /> <#if coreNode.hasAncestor("Archive") == true || coreNode.id == "Archive"> <span class="archives-logo-link" title="This content is archived">Archive <span class="lia-img-icon-help lia-fa-icon lia-fa-help lia-fa" aria-label="Help Icon" role="img" id="display"></span> </#if>3.3KViews
Sign in to react to this post15Comments
How We Built It: Private Member Info component
We all know that protecting Community users' personal information is vital. That's why the most sensitive user profile data is visible only to users with the Administrator role. That said, a few profile fields enable Khoros staff members to troubleshoot questions better and help customers more effectively. On Atlas, we allow a select set of Khoros employees to see a user's email, company, first name, last name, and roles in a Private Member Info component. We display this component on the View Profile Page. Clearly, we don't want every Khoros employee to see this protected data. Therefore, we display the component only to users with a specific role. Today, we're going to walk through how our Atlas engineers built the Private Member Info component. Component workflow APIs used Code walkthrough How I tested it Custom CSS Annotated component code Component workflow The logical flow of our Private Member Info components looks like this: APIs used Our Private Member Info component uses FreeMarker context objects to make requests to the Community REST API. These are the FreeMarker and Community APIs we use: Freemarker restadmin - to make a request to the Community REST API as an administrator restBuilder - to build and send a LiQL query as a request to the Community REST API page.context.user - to get information about the user associated with the user profile page being viewed user.id - to get the ID of a user Community REST API v1 /users/id/{id}/roles - to retrieve roles for a user /users/id/{id}/settings/name/{setting-name} - retrieve the value of a setting /users/id/{id}/profiles/name/{profile-field-name} - retrieve the value of a user profile field Code walkthrough I'm going to walk through our Private Member Info component section by section. You can check out the full component code with annotations and our custom styles at the end of the article. Determine whether a user meets the requirements Retrieve data for the profile being viewed Component styling Display the email and verification indicator Display first and last name Display company name Display roles Determine whether a user meets the requirements For this component to render, the user viewing the page must be registered and must have the Staff role assigned. The goal of this snippet is to verify that the user viewing the page meets these requirements. 1 <#if user.registered> 2 <#assign isStaff = false> 3 <#assign roles = restadmin("/users/id/" + user.id?c + "/roles").roles.role> 4 <#assign roleSize = roles?size> 5 <#if (roleSize > 0) > 6 <#list roles as role> 7 <#if role.name?trim == "Staff"> 8 <#assign isStaff = true> 9 </#if> 10 </#list> 11 </#if> Line 1: We call the user.registered FreeMarker method to determine the registration status. This method returns a boolean (true or false). Line 2: We create a variable named isStaff and set it to false until we determine whether the user has the required role. Line 3: We create a variable named roles. This will hold the roles of the user viewing the page. To set the value, we use the restadmin FreeMarker method to call the Community API v1 /users/id/<id>/roles endpoint. Pro Tip: Notice how we build the endpoint path passed to restadmin. We add the correct user ID to the endpoint path with the user.id FreeMarker method. That ?c you see in user.id?c is the FreeMarker c built-in. It is a convenience method that converts the ID to a proper format needed for processing the code. Lines 4-10: Finally, if the user has roles assigned, we loop through them with the list directive. If the role name matches "Staff", we set the isStaff variable to true. The ?trim you see in <#if role.name?trim == "Staff"> is another builtin. It removes any whitespace before and after the role name. This ensures that only the role "Staff" triggers setting isStaff to true. Retrieve data for the profile being viewed We've established whether the user viewing the profile page is both registered and has the Staff role. Now, we're going to get the email, first name, and last name of the user whose profile page is being viewed from the database. The goal of this snippet is to define a LiQL query and send it in a request to Community API v2. We use Community API v2 so that we can retrieve multiple fields with one request. 1 <#if isStaff> 2 <#assign userQry = "SELECT id, email, first_name, last_name FROM users WHERE id='${page.context.user.id}'" /> 3 <#assign pageUser = (restBuilder().admin(true).liql(userQry).data.items)![] /> 4 <#if pageUser?size gt 0> 5 <#assign pageUser = pageUser[0] /> Line 1: If isStaff is true, then proceed. The remainder of the component code is contained in this if statement. If isStaff is not true, the component is not rendered on the page. Line 2: We assign our LiQL query to a variable called userQry. This is the query: SELECT id, email, first_name, last_name FROM users WHERE id='${page.context.user.id}' Pro Tip: See how we use a FreeMarker interpolation (${page.context.user.id}) to define the value of the WHERE clause in our LiQL query? The page.context.user FreeMarker method lets us get the user object associated with the current page. Once we have that user object, we can use any of the methods on the FreeMarker user context object -- in this case, the id method. When the LiQL query is processed server-side, the query will return results based on the ID of the user whose profile page is being viewed. (If we had used ${user.id}, the query would return the ID of the user viewing the page, and we would receive and display the wrong user's data. Line 3: We make a request to Community API v2 to assign the response as the value of a variable called pageUser. We're going to use the pageUser variable later when we display the name and email address in the component. The request in this line uses the restBuilder FreeMarker context object. The methods on restBuilder include a handy way to pass a LiQL query and make the GET request as an administrator. Component styling This next snippet adds styling for the component. (We've actually reused classes from other components.) One thing to note is that we have hard-coded the component title: class="lia-panel-heading-bar-title">Private member info</span><sub>Staff only</sub> Consider creating a custom text key to hold the title text. You can use the text FreeMarker context object to retrieve and display the value of the text key. For example: class="lia-panel-heading-bar-title">${text.format("private-member-info-title")}</span><sub>${text.format("private-member-info-subtitle")}</sub> Here is our styling. See the CSS section for our custom style definitions. <div class="lia-panel lia-panel-standard PrivateStatisticsTaplet Chrome lia-component-users-widget-my-private-statistics"> <div class="lia-decoration-border"> <div class="lia-decoration-border-top"> <div> </div> </div> <div class="lia-decoration-border-content"> <div> <div class="lia-panel-heading-bar-wrapper"> <div class="lia-panel-heading-bar"><span class="lia-panel-heading-bar-title">Private member info</span><sub>Staff only</sub></div> </div> <div class="lia-panel-content-wrapper"> <div class="lia-panel-content"> <div id="myPrivateStatisticsTaplet" class="MyStatisticsTaplet"> <div class="MyStatisticsBeanDisplay"> Display the email and verification indicator Let's use that pageUser variable that holds the email address and first/last name of the profile being viewed. In this snippet, we display the email address, if one exists, and an icon indicating whether the email address is verified or not. Pro Tip: Check out the attempt/recover blocks used to catch and handle errors. For example, we call restadmin to retrieve the value of the email_verified setting in an attempt block. If that call fails, we set the value to false manually in a recover block. We talk about error handling more in the FreeMarker section of Community customization and performance best practices. 1 <#assign showEmail = (pageUser.email)!'' /> 2 <p><strong style="font-weight: bold">Email:</strong> <a href="mailto:${showEmail}">${showEmail}</a> 3 <#attempt> 4 <#assign email_verification = restadmin("/users/id/${page.context.user.id?url}/settings/name/user.email_verified").value!'false' /> 5 <#recover> 6 <#assign email_verification = false /> 7 </#attempt> 8 <#attempt> 9 <#assign email_verification = email_verification?boolean /> 10 <#recover> 11 <#assign email_verification = false /> 12 </#attempt> 13 <#if email_verification> 14 <span class="profile_email_verified" title="Email verified"></span> 15 <#else> 16 <span class="profile_email_not_verified" title="Email NOT verified"></span> 17 </#if> 18 </p> Lines 1-2: We create the showEmail variable and set it to the value of the email address ((pageUser.email)!''). We add the HTML to render and style the email address. If there is no email present, then we display an empty string. Lines 4-7: We determine whether or not the email address is verified. The email_verified setting contains this information, so we make a request to Community API v1 using the restadmin context object to retrieve the value. (API v2 does not support retrieving settings by name.) We send the request to the /users/id/<id>/settings/name/{name} endpoint and we store the response in a variable called email_verification. Lines 8-12: We convert the value of email_verification to a boolean. Lines 13-17: We set a "verified" icon next to the email address if verified; otherwise, we set an "unverified" icon. These are Font Awesome icons defined in our custom CSS. Display first and last name This section is pretty straightforward. All we're doing here is retrieving the first name and last name from the pageUser element and displaying them. If there is no value for the name, we display an empty string. <#attempt> <#assign nameFirst = (pageUser.first_name)!'' /> <#assign nameLast = (pageUser.last_name)!'' /> <p><strong style="font-weight: bold">First + Last Name:</strong> ${nameFirst!""} ${nameLast!""}</p> <#recover> </#attempt> Display company name Our Private Member Info component displays the company associated with the profile being viewed. Company is not a default field on the user profile. It is a customer profile field that was set up for Atlas. Your company might have custom profile fields as well. These are generally set up during launch. We retrieve the "company" profile field with a request to the Community API v1 /users/id/{id}/profiles/name/{profile_name} endpoint using the restadmin FreeMarker context object. After we retrieve the value, we display it with an interpolation (${showCompany}). <#assign showCompany = (restadmin("/users/id/${page.context.user.id?url}/profiles/name/company").value)!'N/A'> <p><strong style="font-weight: bold">Company:</strong> ${showCompany}</p> Display roles In this final snippet, we list the roles associated with the user profile being viewed. We retrieve the roles with a request to the Community API v1 /users/id/{id}/roles endpoint. From there, we loop through the roles returned and present them in a list. <#assign userroles = restadmin("/users/id/${page.context.user.id?url}/roles").roles.role> <strong style="font-weight: bold">Roles:</strong> <ul style="margin-left: 15px"> <#list userroles as userrole> <#assign userRoleName = userrole.name?trim /> <li style="list-style: inside">${userRoleName}</li> </#list> </ul> </div></div></div></div></div></div><div class="lia-decoration-border-bottom"><div> </div></div></div> </div> </#if> </#if> </#if> How I tested it To build and test this component in my test environment, I made sure that I had a login to a staging environment that enables you to assign a role to a test user and to access: Community Admin > Display > Skins Community Admin > Content > Custom Pages Studio > Community Style > CSS Studio > Components Studio > Page I followed these steps: Choose a role in Community Admin to use as the required role (e.g., Staff). Assign the role to at least one test user (preferably not one with the Administrator role). In Studio > Components, create a custom component called custom.profile.staff-visible-details. Copy/paste the annotated code into the component text area. Search for the following lines of code and replace "Staff" with the name of the role you want to test with. <#list roles as role> <#if role.name?trim == "Staff"> <#assign isStaff = true> </#if> </#list> Go to Studio > Page and place the custom.profile.staff-visible-details component on the View Profile Page used for your community. Pro Tip: Check to see whether your community uses a customer version of the View Profile Page in Admin > Content > Custom Pages. If your community uses a custom version of the View Profile Page, be sure to place the component on that quilt. Add the custom CSS for the component in the _style.scss file for my community skin in Studio > Community Style > CSS. Look in Community Admin > Display > Skins to view which skin your community uses. Test on your stage site. Log in as your test user and navigate to the View Profile Page for any community member. You should see the Private member Info component on the page in the location where you placed it on the quilt in step 6. Custom CSS These are the custom styles that we use for the component. Atlas uses the Support Theme. My test community does not, so when I tested this in my local environment, I replaced the values for the color variable with HEX values. I added these styles to the _style.scss file for my community skin in Studio > Community Style > Community Skins > CSS. #lia-body.ViewProfilePage { .lia-component-users-widget-my-private-statistics { .profile_email_verified:before { color: $theme-color-matcha; content: "\f058"; font: normal normal normal 16px/1 FontAwesome; } .profile_email_not_verified:before { color: $theme-color-cerise; content: "\f071"; font: normal normal normal 16px/1 FontAwesome; } #profile_sfdc_search_link:after { color: $theme-color-blue; content: "\f0c1"; font: normal normal normal 16px/1 FontAwesome; margin-left: 3px; } } } Annotated component code <#-- Display a user's name, company, email, and roles to Khoros employees with the Staff role. --> <#-- Verify whether the user in context meets requirements to see the component. User must be registered and have Staff role. --> <#if user.registered> <#assign isStaff = false> <#assign roles = restadmin("/users/id/" + user.id?c + "/roles").roles.role> <#assign roleSize = roles?size> <#if (roleSize > 0) > <#list roles as role> <#if role.name?trim == "Staff"> <#assign isStaff = true> </#if> </#list> </#if> <#-- If the user has the Staff role, retrieve the email, first/last name, ID for the user profile being viewed. First we build the LiQL query to retrieve the data. Then, we make a request to the Community REST API passing our query --> <#if isStaff> <#assign userQry = "SELECT id, email, first_name, last_name FROM users WHERE id='${page.context.user.id}'" /> <#assign pageUser = (restBuilder().admin(true).liql(userQry).data.items)![] /> <#if pageUser?size gt 0> <#assign pageUser = pageUser[0] /> <#-- Some styling to make this pretty. We're reusing the same styling as the out-of-the-box Private Statistics component. --> <div class="lia-panel lia-panel-standard PrivateStatisticsTaplet Chrome lia-component-users-widget-my-private-statistics"><div class="lia-decoration-border"><div class="lia-decoration-border-top"><div> </div></div><div class="lia-decoration-border-content"><div><div class="lia-panel-heading-bar-wrapper"><div class="lia-panel-heading-bar"><span class="lia-panel-heading-bar-title">Private member info</span><sub>Staff only</sub></div></div><div class="lia-panel-content-wrapper"><div class="lia-panel-content"><div id="myPrivateStatisticsTaplet" class="MyStatisticsTaplet"> <div class="MyStatisticsBeanDisplay"> <#-- Display the hyperlinked email address if one exists. If email isn't verified, display an indicator. --> <#assign showEmail = (pageUser.email)!'' /> <p><strong style="font-weight: bold">Email:</strong> <a href="mailto:${showEmail}">${showEmail}</a> <#attempt> <#assign email_verification = restadmin("/users/id/${page.context.user.id?url}/settings/name/user.email_verified").value!'false' /> <#recover> <#assign email_verification = false /> </#attempt> <#attempt> <#assign email_verification = email_verification?boolean /> <#recover> <#assign email_verification = false /> </#attempt> <#if email_verification> <span class="profile_email_verified" title="Email verified"></span> <#else> <span class="profile_email_not_verified" title="Email NOT verified"></span> </#if> </p> <#-- Display the first and last name of the user profile being viewed --> <#attempt> <#assign nameFirst = (pageUser.first_name)!'' /> <#assign nameLast = (pageUser.last_name)!'' /> <p><strong style="font-weight: bold">First + Last Name:</strong> ${nameFirst!""} ${nameLast!""}</p> <#recover> </#attempt> <#-- Display the name of the company associated with the user proview being viewed --> <#assign showCompany = (restadmin("/users/id/${page.context.user.id?url}/profiles/name/company").value)!'N/A'> <p><strong style="font-weight: bold">Company:</strong> ${showCompany}</p> <#-- Display the roles of the user being viewed --> <#assign userroles = restadmin("/users/id/${page.context.user.id?url}/roles").roles.role> <strong style="font-weight: bold">Roles:</strong> <ul style="margin-left: 15px"> <#list userroles as userrole> <#assign userRoleName = userrole.name?trim /> <li style="list-style: inside">${userRoleName}</li> </#list> </ul> </div></div></div></div></div></div><div class="lia-decoration-border-bottom"><div> </div></div></div> </div> </#if> </#if> </#if>1.9KViews
Sign in to react to this post13Comments
How We Built It: Since you were gone component
Overview This article explains how to build a custom component and attach it to the community hero element on your community home page. You can add this custom component to any element based on your preference. This custom component fetches recent notifications when the user is not signed in to the community. These recent notifications are rendered on your community home page based on whether the user is anonymous or registered. The purpose of this component is to notify visitors about recent activities performed on the user account for registered users and for anonymous users, a custom text based on your preference will be displayed. Building the Since you were gone component is done in three steps: Creating a custom component walkthrough Assigning the custom elements with custom text Adding the custom component to your community homepage Here is an image of the Since you were gone component in Atlas Community: Creating a custom component walkthrough Create a new custom component in Studio named since_you_were_gone (Studio > Component > New component). To add the below custom component code snippet, you must have community admin permission. <#if user.anonymous> <div class="hero-desc hero-desc-space-style"> <span>${text.format("custom.hero-desc.anonymous")}</span> <a href='${webUi.getUserRegistrationPageUrl("/")}'>${text.format("custom.hero-desc.anonymous.joinus")}</a> </div> <#else> <#assign notifications = rest("/users/self/notifications/unread/count").value?number/> <#assign notificationfeedCount = liql("SELECT count(*) FROM notification_feeds" )/> <#assign num=notificationfeedCount.data.count?number/> <#if (num> 0) && (notifications>0) > <div class="hero-desc hero-desc-space-style"> <div class="since">${text.format("custom.hero-desc.loggedin")}</div> <#attempt> <@component id="notificationfeed.notificationList" lazyLoad="true" /> <#recover> </#attempt> </div> <#else> <div class="hero-desc hero-desc-space-style"> <div class="hero-desc hero-desc-space-style">${text.format("custom.hero-desc.no.recent.notification")}</div> </div> </#if> </#if> We are going to break down the code into six sections. Section 1 (Line 1-6) <#if user.anonymous> <div class="hero-desc hero-desc-space-style"> <span>${text.format("custom.hero-desc.anonymous")}</span> <a href='${webUi.getUserRegistrationPageUrl("/")}'>${text.format("custom.hero-desc.anonymous.joinus")}</a> </div> <#else> If user.anonymous is true, then proceed. The remainder of the component code is contained in this if statement. If user.anonymous is not true, the component is not rendered on the page and moves to line 7 which is for registered user. The text of the following elements appears on the homepage. custom.hero-desc.anonymous custom.hero-desc.anonymous.joinus Section 2 (Line 7) <#assign notifications = rest("/users/self/notifications/unread/count").value?number/> From this line, the below-described codes are for the registered community user. We create a variable named notifications. This will hold the count of the unread notifications. To set the value, we use the rest FreeMarker method to call the Community API v1 /users/self/notifications/unread/count endpoint. This query retrieves the count of notifications for self as a number. Section 3 (Line8) <#assign notificationfeedCount = liql("SELECT count(*) FROM notification_feeds" )/> We assign our LiQL query to a variable called notificationfeedCount. This is the query: SELECT count(*) FROM notification_feeds This query retrieves the count from the notification feeds of the user as a string. Section 4(Line 9) <#assign num=notificationfeedCount.data.count?number/> We create a variable named num. This variable fetches the count of the notification feed which is a string that converts it to a number using the ?number function. Section 5(Line 10) <#if (num> 0) && (notifications>0) > <div class="hero-desc hero-desc-space-style"> We use the && - AND operator to combine the results of lines 7 and 9. If there is a value-based out of the && - AND operation, then the remainder of the component code is contained in this if statement. Section 6 (Line 11-22) <div class="since">${text.format("custom.hero-desc.loggedin")}</div> <#attempt> <@component id="notificationfeed.notificationList" lazyLoad="true" /> <#recover> </#attempt> </div> <#else> <div class="hero-desc hero-desc-space-style"> <div class="hero-desc hero-desc-space-style">${text.format("custom.hero-desc.no.recent.notification")}</div> </div> </#if> </#if> In these lines, we declare the custom.hero-desc.loggedin element. In line 13, we use the core component notificationfeed.notificationList to retrieve the latest article activity of the user. We also set the lazyLoad=true, so that it does not increase the initial page load time. Line 13 is the only core component and other parts of the code are custom based on your preference. Furthermore, we also declare the custom.hero-desc.no.recent.notification element so that if there is no recent notification, this element displays the No new recent activity text on your community home page. With lines 21 and 22, we close the if statements Assigning the custom elements with custom text In the above section, we have added some custom elements. In this section, we are going to assign those custom elements with text. Go to Studio > Text Editor > Community Text. Click Search. You can see the text properties. Assign the following custom elements to the text. custom.hero-desc.anonymous = To connect with the brightest leaders and practitioners of Digital Customer Engagement, custom.hero-desc.anonymous.joinus = join your community custom.hero.desc.loggedin = Since you were gone, custom.hero-desc.no.recent.notification = No new activity found udio Click Save. The above elements will be rendered based on the user and will display on the community home page. Adding the custom component to the community homepage In this section, we are going to add the since_you_were_gone component to your community element based on your preference which will appear on your community homepage in the Studio. Go to Studio and select the layout on which you want to add this component. We are adding it to the Community Hero Layout and click Save. Conclusion There you have it! The Since you were gone component appears on your community home page, customized for each visitor. Here is the image of the Since you were gone component for an anonymous user. Here is the image of the Since you were gone component for a community member(registered user).1.7KViews
Sign in to react to this post7Comments
How We Built the KB Audit Flow Pt. 1
Trust in the accuracy of your Knowledge Base articles is essential to building trust and confidence between your visitors and the content they're interacting with within your knowledge base. One way of ensuring that each KB article is current and accurate is frequent and routine audits on that content. The audit process should be straightforward for your authors, administrators, and moderators, and transparent for your readers. To make this process as seamless and easy as possible on Khoros Atlas, we created an auditing solution that enables members with the appropriate permissions to set the article as "audited" and to display the date and time of the last audit to visitors. In this three-part series, we're going to take a detailed look at how we built out this auditing solution in Khoros Atlas. We highly recommend reading the next two posts in this series to get a better understanding of the other components and prerequisites required for this component to function properly. How We Built the KB Audit Flow Pt. 2: Audit Checkbox How We Built the KB Audit Flow Pt. 3: Review Date Display Concept Our 'Last Reviewed' component enables members with the appropriate roles (defined in the 'Last Reviewed' component we will create in this guide series) to see a checkbox in the sidebar of each KB article enabling them to indicate that the article has been audited. This "Audited" checkbox. When the box is checked, the time and date are saved as part of the audit trail. Note: The role(s) required to see the Audited checkbox are set within the "Last Reviewed" information component we will create in the second part of this blog series. Once that data has been saved, it is then displayed in the sidebar. This enables visitors to see when the article was last reviewed. This enables them to gauge how up-to-date the information in the KB article is. Member Flow For permissioned members, auditing an article is as simple as reviewing it on the front end and selecting the Audited checkbox. Once done, a confirmation message appears enabling the member to confirm the audited state by selecting Yes, or cancel it by selecting No. Once confirmed, the date and time data is saved and that date and time become the new "Last Reviewed" date displayed to all visitors in the sidebar. Prerequisites Before this functionality can be added to the Khoros Communities instance, we need to make a few changes on the back end to create a space where metadata containing the audit timestamps and status for each message will exist. This requires a few steps that Support can assist you with. To get your instance ready to work with the Audit Flow, simply submit a support request using the Case Portal and request that your Communities instance be configured to support Audit Flow. Case Study: Khoros Information Experience Team Our Product Content Experience Team is responsible for writing and maintaining the knowledge base (KB) articles for Care, Community, Marketing, CX Insights, and Khoros Flow. This includes a library of hundreds of articles, each focused on information that evolves alongside the products. The Challenge As you could imagine, keeping all of this content updated is a huge responsibility. Outdated information in our knowledge base leads to confusion and wasted time. We needed a solution that accomplished the following: Instill confidence in visitors that the article they're reading is updated and accurate Enable team members to record their audit of existing KB articles from the front end Provide a visible label on each audited article with the time and date of last review Create a process for determining which articles should be prioritized for audit Execute audits in coordination with internal engineering and product teams The Technical Project To reach these goals, we partnered with the Atlas team, as well as our internal engineering and product partners, to create both a technical solution and team workflow. The technical side of the project required some development work on Atlas. This included: A new table in the database to store the "last reviewed" information Metadata fields to represent the information in each article A component that enables the frontend audit checkbox for users with specific roles A component that displayed the "last reviewed date" of each article to all visitors With these additions in place, our team members with the Administrator or Documentation role can mark a KB article as audited from the front end, and have the date and time of that review displayed to all visitors, regardless of their assigned role(s). This accomplished the feat of both simplifying the review process and making a functional improvement that our visitors can benefit from. Tackling the Audit Process The next thing we needed to achieve was the active auditing of over 1,000 TKB articles currently in Khoros' knowledge base. Many of these articles were written years ago, and our team (at the time) was very small. We partnered with various teams across Khoros to assign articles to subject matter experts, so it wasn't just our content team that was reviewing content. If the content was 100% accurate, it would be marked as audited. If not, we created a ticket in Jira and assigned a content team member to update it. This cross-functional auditing pass enabled us to audit ⅓ of the KB articles in our library in six months. This approach brought with it a number of challenges. It was a logistical feat to coordinate with all of the teams and ensure deadlines were met. Additionally, we weren't entirely sure that every article receiving the bulk of our focus was actually being used by our customers. So, we adjusted our approach for the second phase of our audit. Instead of tackling the entire library of articles at once, we instead focused on the following articles: Articles with the highest average views per day Articles with an average of at least one view per day since it was published This was accomplished by examining the publishing date of each article, as well as its total views. With this information, we could determine the average number of views for each day since the article was published. Articles with the most average views per day received the highest priority. The rest of the articles with at least one visit per day would receive an audit once the first batch of articles is complete. Now that our team has grown, we are able to set achievable goals and divide the work amongst our writing staff so that our efforts are targeted so each audit has the maximum possible impact on our overall customer experience.933Views
Sign in to react to this post1Comment
How We Built It: Custom Community Banners
Custom community banners are a great way to enhance the user experience throughout your Community. In Atlas, we use custom content to draw attention to important features, direct users to useful pages, and share news from our team. In this post, we take a look at some of the ways you can leverage Khoros Communities' Custom Content features to enhance your user's experience. You can customize Custom Content with a little scripting in several ways: Show or hide the component based on the user's role(s) Add CSS styling unique to the component Display images hosted externally or through your Community's Asset Library Direct users to a specific link or area of your Community In this example exercise, we're adding the banner (below) to our community's pages. We will go through the process of uploading the image asset, creating the custom content, and adding the Custom Content component to our Community Page's quilt. Upload Image Assets to your Community We start with a simple PNG image that we want to display in the custom community banner. This image file needs to be hosted somewhere. This could be an external hosting arrangement, or directly hosted from within your community using the Community Asset Library. You can upload image assets to the Asset Library by doing the following: Go to Studio > Community Style Tab. Select Asset Library from the Work With drop-down menu. Click the Other Assets tab. Click the Choose File button and select the file you want to upload. Click Upload. After you have uploaded the new asset, you can locate the file in the list and discover its unique URL. Make note of this URL since you will need it when crafting the Custom Content in the next step. In our example, our file is located at /html/assets/thankyou2.png. Add Custom Content to the Quilt This last step is pretty straightforward. We need to add our new Custom Content to the quilt for the page(s) we want the banner to appear on. In our example, we're adding it to the Community Page. Here's how: Go to Studio > Page. Expand the Custom Content area of the Components list. Select the Custom Content item you want to use for this banner by clicking the plus (+) symbol next to it as you hover over it with the cursor. Drag the Custom Content component to the area of the quilt in which you want the banner to display. Scroll down to the bottom of the page and click Save. Once you have configured the custom content, your new custom community banner should appear where you configured it to go and for the users with the role(s) you specify in the content's scripting. Create Custom Content Now that you have the image asset(s) you want to display in the custom community banner uploaded, you can create the custom content that the image(s) will be displayed through. Here's how: Go to Community Admin > Content > Custom Content. Select a Custom Content Setting that you want to use. Ideally, this would be one that doesn't already have content in it, matching the item that you selected in the previous step. In our case, this is Custom Content 2. Add your unique content to the Text field. See our example in the Custom Content Example section below. Click Save. Note: The name of the Custom Content Setting. It should match the one you placed in the quilt in the Add Custom Content to the Quilt section, above. Custom Content Example In our example, we are creating a custom community banner that only appears for users with specific roles and that has styling that matches our needs. Here is the content we used in the Text field: <style> .image-responsive-style { width: auto; margin-top: 40px; margin-left: -12px; cursor: pointer; } @media only screen and (min-width: 1024px) { .desktop-margin-image { margin-top: -40px; } } </style> <#assign user_has_role=false /> <#if !user.anonymous> <#list restadmin("/users/id/${user.id?c}/roles").roles.role as role> <#if role.name?? && ((role.name=="Administrator" )||(role.name=="Example" ))> <#assign user_has_role=true /> </#if> </#list> </#if> <#if user_has_role> <center style="margin-bottom: -25px;" class="desktop-margin-image"> <a href="/t5/forums/searchpage/tab/message?filter=labels&q=%22example%22&noSynonym=false&advanced=true&collapse_discussion=true&search_type=thread&labels=example "> <picture> <source media="(min-width: 1024px)" srcset="/html/assets/thankyou2.png"> <img src="/html/assets/thankyou2.png" class="image-responsive-style"> </picture> </a> </center> </#if> To understand how this works, let's break down this custom content piece-by-piece. <style> ... </style> Everything within the <style></style> tags is there to add additional CSS styling to the custom content component itself. This is a great way to add a little extra visual flare to your banner, or to ensure that it sits exactly where you need it to on the page. <#assign user_has_role=false /> This if statement uses the restadmin context object to make an API call that checks the user's roles to see if they have either the Administrator or Example role applied to their profile. If they do, then the user_has_role variable is set to true. Otherwise, no changes are made and the value remains false. <#if user_has_role> <center style="margin-bottom: -25px;" class="desktop-margin-image"> <a href="/t5/forums/searchpage/tab/message?filter=labels&q=%22example%22&noSynonym=false&advanced=true&collapse_discussion=true&search_type=thread&labels=example "> <picture> <source media="(min-width: 1024px)" srcset="/html/assets/thankyou2.png"> <img src="/html/assets/thankyou2.png" class="image-responsive-style"> </picture> </a> </center> </#if> Finally, we have an if statement that displays the banner image (along with other HTML properties such as its HREF link) if the user_has_role variable is true. In our example, we're directing users that click the banner to a search page within the Community. The image source location is the relative URL that we noted earlier upon uploading the asset to the Asset Library. Summary That's it! Your banner should now appear wherever a user with the right role(s) visits the page(s) you have configured the banner for. This technique isn't just useful for simple image banners. It can be used for virtually any user experience customization you need, from visual fair between components to important announcements you want to make to your visitors. You can use Freemarker to supercharge this content and leverage information about your community to create a more dynamic experience for your audience.901Views
Sign in to react to this post1Comment
How we built it: Search Anywhere and the Resource Center
In 2019, the Customer Experience team at Khoros planned to improve the digital experience of our customers through a series of initiatives. One of the primary tasks was to make Atlas the focal point of documentation and support articles for Marketing products, just like it was for Care products. The change involved two steps: Moving all our product technical and functional documentation from third-party software to Atlas Maintain the ability to search for documentation from within the product websites i.e. search for documentation from Marketing and Care platforms from the in-app Resource Center What is the Resource Center? The Resource Center in this post refers to the tool powered by third-party software (Pendo) to house additional contextual help and include standard help articles or FAQs, as well as in-app Guides that will walk users through specific processes. Most of the existing Marketing customers were frequent users of the search within the app tool. So, it was important for us to retain this experience. However, these changes were due in less than two months, given the licensing deadline with a third-party application. This presented two key challenges: How to look up information from the Community? How to embed the Search Anywhere tool (yes, that’s what we named it internally) into the Resource Center and into the product website? How we solved looking up information from Community: Community platform supports the awesome API layer, known in developer circles, as LiQL - the Lithium Query Language LiQL enables you to search Community information based on tags, title etc. However, you must have the “correct” permissions via the API keys in order to pull the information The Triumph team built the oAuth based mechanism to provide the API access from a web application The premise was that we could register the Search Anywhere tool as an app on the Community platform and use the specific keys to make API calls and this is how it works currently A key feature that was necessary was to limit the ability to only lookup information limited to only certain “boards” (or nodes if you happen to know Community well). We achieved this by attaching a unique role, based on the application (Marketing or Care) We registered unique apps for Care and Marketing to “sandbox” access to relevant information within each of the products We needed the ability to show some posts by “default”, for example, if you land on the “Social Marketing” page in the Marketing product, the tool would show “top” posts from the “Social Marketing” board in our Community We achieved this using “tags”, a way to label the posts in Community. This required identifying and tagging the relevant posts in specific boards in the Community. Our Product Content Experience team helped with this laborious but important task. Going the extra mile -- Integrating Resource Center into Community and Community Analytics Following the introduction of the Resource Center embedded into the Marketing product, we wished to integrate Pendo and the Resource Center in Community and Community Analytics in order to provide a consistent help experience for our customers across all of Khoros products. The community Admin, Studio, Moderation Manager, and Toolbox areas have a plethora of options, and community admins are required to understand their use and purpose for configuring community as per their brand’s needs. To improve the customer experience, Pendo integration is done with Community to provide dynamic help on the admin section. We leveraged the Search Anywhere work to identify the right resources for Pendo and created the Community integration. Abhishek Gupta, Vaibhav Chawla, and John Dowden collaborated for integrating and creating Search Anywhere built with Atlas tags associated with hundreds of Atlas articles. Search Anywhere makes use of Tags (not Custom Tags like we have in the legacy Admin/Studio Help drawer) to fetch the right information. The sheer number of Tags required to cover the Admin, Studio, Moderation Manager, API Browser, and Toolbox sections was quite a challenge. To top that up, Community Analytics had to be identified through a single Search Anywhere build. The team put in a lot of effort and created a JSON mapping to make it dynamic and easy to upgrade as well as completing a small but very important task to improve the customer experience. For the technically inclined: Vanilla is the way The tool is written in JavaScript and modularized for easy readability Tokens The tool uses Community’s OAuth APIs to obtain and refresh access tokens Build We dockerized the build to make it easy for incremental updates This also helps in developing and testing the tool on any machine Since different apps were registered, we parameterized the build for the code to “know” the context and use appropriate access keys to invoke APIs. If you build this tool with the parameter “CARE”, the output code can only access information from the “Care” board in our Community Deployment The binaries (word for the files that is output from all the code we write) are pushed to an S3 bucket from where the code is copied into the resource center The project was a unique and proactive collaboration between Engineering, Customer Experience and Product Content Experience teams. It did lay the foundation for great working relationships among those involved in the project, very much embodying our value: We win and grow as one team. Callouts: Santosh Shaastry ( SantoshS) Abhinn Gautam ( AbhinnG ) Kokil Jain ( KokilJ ) Narendra Prabhu ( NarendraG ) Gunaalan ( gunaas ) Keerthana ( KeerthanaS ) Annu A ( AnnuA ) Abhishek Gupta ( AbhishekGu ) Vaibhav Chawla ( VaibhavC ) Akash Navani ( AkashN ) John Dowden ( JohnD ) Scott Scarborough ( ScottSc ) Travis Berryhill ( TravisB )900Views
Sign in to react to this post7Comments
How We Built It: User Profile Hover Card
Overview User Profile Hover Card is used to display user information such as pronouns, roles, number of posts, kudos, and solutions. With the User Profile Hover Card, you see user information while viewing any article in your community by hovering your mouse cursor over either their avatar or the user name. Note: The User Profile Hover Card will not be displayed in the user profile. A JavaScript function will be attached to certain elements on the page that makes an AJAX call to the server when the mousenter event is triggered. This AJAX call request will return all the information necessary to display a hovercard with the user profile information. You can create a Profile Hover Card using these three steps covered in this guide. Building an endpoint Creating a custom javascript function Adding a javascript function to a quilt to display the user information Here is an image of the User Profile Hover Card in Atlas Community: Building the endpoint Create a new endpoint in Studio named profile-card (Studio > Endpoints > New Endpoint). Here is the overall structure of our code. After some initialization code and setting up some variables, we have two main steps outlined in the comments. <#compress> <#-- Set up number format and import some functions you will use later on --> <#setting number_format="0.######"/> <#include "theme-lib.common-functions" /> <#-- The user whose profile information we would like to display --> <#assign userId = getRequestField("userId", "-1", true)?number /> <#assign unqId = getRequestField("unqId", "")?string /> <#assign badgeSize = 5 /> <#if userId gt 0 && validEndpointRequest(false, true,false)> <#-- Step 1. Make REST call for the data required for the component --> <#-- . . . see below code snippet . . . --> <#-- Step 2. Render the actual card markup --> <#-- . . . see below code snippet . . . --> </#if> </#compress> Step 1 To fetch the user’s profile information, we just need to write a LiQL request and make the request assigned to a variable. <#assign userQry = "SELECT login, view_href, rank.name, rank.color, user_badges FROM users WHERE id='${userId}'" /> <#assign userProfileData = executeLiQLQuery(userQry) /> Step 2 In step 2, we will use the data we just fetched to create some variables and return the markup required for the hovercard. <#if userProfileData?size gt 0> <#assign userProfileData = userProfileData[0] /> <div> <section> <span><span id="cardTitle-${unqId}"><a href="${userProfileData.view_href}">${(userProfileData.login)!""}</a></span> <span id="cardDesc-${unqId}">${(userProfileData.rank.name)!""}</span> </section> <#-- display badges --> <#if userProfileData.user_badges?? && userProfileData.user_badges.size gt 0> <section> <#assign badgeCount = 0 /> <#list userProfileData.user_badges.items as userBadge> <#if userBadge.earned_date??> <div> <img title="${userBadge.badge.title}" alt="${userBadge.badge.title}" src="${userBadge.badge.icon_url}"> </div> <#assign badgeCount = badgeCount + 1 /> <#if badgeCount == badgeSize> <#break> </#if> </#if> </#list> </section> </#if> <#-- posts, kudos, solutions --> <#assign postCount = 0 /> <#assign kudoCount = 0 /> <#assign solutionCount = 0 /> <#assign metricsQry = "SELECT * FROM metrics WHERE user.id = '${userId}' AND id IN ('net_overall_posts','net_accepted_solutions','kudos_weight_given','kudos_weight_received')" /> <#assign metrics = executeLiQLQuery(metricsQry, false, true) /> <#if metrics?size gt 0> <#list metrics as metric> <#switch metric.id> <#case "net_overall_posts"> <#assign postCount = metric.value?number /> <#break /> <#case "net_accepted_solutions"> <#assign solutionCount = metric.value?number /> <#break /> <#case "kudos_weight_received"> <#assign kudoCount = metric.value?number /> <#break /> </#switch> </#list> </#if> <section> <ul> <li>${postCount} <span><#if postCount == 1>${text.format("general.Post")} <#else>${text.format("general.Posts")}</#if></span></li> <li>${kudoCount} <span><#if kudoCount == 1>${text.format("general.Kudo")} <#else>${text.format("general.Kudos")}</#if></span></li> <li>${solutionCount} <span><#if solutionCount == 1>${text.format("general.solution")} <#else>${text.format("general.Solutions")}</#if></span></li> </ul> </section> </div> </#if> </#if> It looks like a lot of code, but it’s pretty straightforward once you get the hang of it. Now that we’ve got a backend endpoint that can talk to the Community's backend and render markup, we will turn our attention to the client so we can make use of it. Create a custom javascript function In Studio > Components > New Component, create a component named custom.profile-card.script. This is where we will attach an event to elements on the page to trigger the hovercard which we wrote in the Building the endpoint section. We will be using @liaAddScript to insert our jQuery, but first, we need to set up the component by adding some common functions and preventing this script from running if we are on the user profile page. <#include "theme-lib.common-functions" /> <#if page.name != 'ViewProfilePage'> <@liaAddScript> <#-- This is where the main javascript will go --> </@liaAddScript> </#if> Now we can focus on what goes in between our @liaAddScript tags. We’ll start with an Immediately Invoked Function Expression (IIFE), like any other custom script. Here we’ve got two things to accomplish: first, we need to define a helper function to generate a unique id for the hovercard so we can keep track of it in the backend. Second, we’ll attach mouse events to certain elements of the page to make our AJAX calls. Note: You will see references to CSS classes that handle animation for the appearance and dismissal of the card. This tutorial does not include styling, but the classes should be named in such a way that you can add animations to them or simply show or hide the element. ;(function($) { <#-- Make Unique Id makeid() : string () --> function makeid() { var text = ""; var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; } $(document).ready(function () { $('body').on({ mouseenter: function(evt) { <#-- break out early and don't render the card --> if ($(this).parents('.lia-component-users-widget-menu').length > 0) { return; } if ($(this).hasClass("disable-hovercard"){ return; } evt.preventDefault(); evt.stopPropagation(); $('.hc-user-profile').removeClass('hc-animate-in hc-is-shown'); <#-- if the card already exists for this avatar/link, just reshow it (no need to go fetch another one) --> if ($('.hc-user-profile', this).length > 0) { $('.hc-user-profile', this).addClass('hc-animate-in hc-is-shown'); return; } var unqId = makeid(); var userId = $(this).attr('href').substring($(this).attr('href').lastIndexOf("/")+1, $(this).attr('href').length); <#-- prepare a container for the card we will render --> var divContainer = $('<div class="hc-user-profile user-profile-card" role="dialog" aria-labelledby="cardTitle-'+unqId+'" aria-describedby="cardDesc-'+unqId+'"><div class="hc-main-container info-container"><div class="spinner"></div></div></div>'); $(this).append(divContainer); $(divContainer).addClass('hc-animate-in hc-is-shown'); <#-- request to the endpoint we created earlier --> <#-- passing back to the userid for our backend LiQL call --> $.ajax({ url: '${getEndpointUrl("profile-card")}', type: 'post', dataType: 'html', data: {"userId": userId, "unqId": unqId}, beforeSend: function() {}, success: function(data) { $('.info-container', divContainer).append(data); }, error: function() { console.log('your error message should go here.'); $('.info-container', divContainer).append('<div class=""><i class="lia-fa fa-var-close" /></div>'); }, complete: function() { $('.spinner', divContainer).remove(); } }); }, mouseleave: function() { $('.hc-user-profile').removeClass('hc-animate-in hc-is-shown'); } }, 'a.lia-link-navigation.lia-page-link.lia-user-name-link,.UserAvatar.lia-link-navigation'); })(LITHIUM.jQuery); You can see at the end, that we attach the jQuery function to .lia-user-name-link and .UserAvatar.lia-link-navigation. These are the elements that will be hoverable. Adding the custom javascript function to the quilt Finally, we will attach the custom script we just wrote to a quilt that is always present on the page: Footer.quilt.xml <add to="ui-scripts"> <component id="custom.profile-card.script"/> </add> Conclusion There you have it! A hover action that, when triggered, will make a request to the backend, fetch information about the user in question, and render a user profile hovercard to get quick information about that user.700Views
Sign in to react to this post0Comments
How We Built It: Platform Status Banner
While we prefer the Khoros platform to be operational at all times, there are times when our product experiences a service disruption. When this happens, it is critical that we inform our customers. We do this by presenting a platform status banner in our Atlas Community. Here is an image example of the Khoros Platform Status Banner: We had some interest here to explain how we achieved this functionality. Let's get into the details! Since your systems will vary from ours, we have made the integration explanation more generic so that you can more easily adapt it to your needs. On its surface, this is a pretty simple feature. It is a custom component that conditionally shows markup based on a JSON response. That is what we will walk you through. Data Source Each product that we want to monitor has a data source that provides the operational status. For the Atlas status banner, Community, Care, Marketing, Flow, and CX Insights are all data sources. First, we need to connect to those data sources via an API. Like many SAAS companies, we use both internal and external monitoring services. You'll need to identify what monitoring services your company uses and find the API that returns the statuses you need. A sample response from a monitoring service could look like the following: [ { id: "SERVICE_ID" name: "SERVICE_NAME", platform: "PLATFORM_NAME" status: "operational", }, { id: "SERVICE_ID" name: "SERVICE_NAME", platform: "PLATFORM_NAME" status: "degraded_performance", }, { id: "SERVICE_ID" name: "SERVICE_NAME", platform: "PLATFORM_NAME" status: "major_outage", }, ... ] Now we need to interpret the above response and render our banner to your community page. Custom Component To display the operation status retrieved from the monitoring service, create a custom component in Community > Studio that displays the platform status banner with markup and Javascript (using the LiaAddScript FreeMarker directive). Markup In your new custom component, set up the markup you want to display in your community. Note that we have set display:none upon initial render. This hides the component until JavaScript logic (described later) determines if any products have a degraded service status to report. <div class="my-fancy-status-component-container" id="StatusComponent" style="display: none;"> <span class="status_container"> <span class="status-name">PLATFORM</span> <span class="status-separator">:</span> <span class="PLATFORM-indicator"></span> </span> <!-- ... repeat for other platforms --> </div> LiaAddScript In a <@liaAddScript>, make a request to fetch your data and modify your markup. <@liaAddScript> ;(function($) { $.ajax({ type: 'GET', url: 'http://example.com/your-status-endpoint', dataType: 'json', success: function(listOfPlatformStatuses) { listOfPlatformStatuses.forEach(platformStatus => { $(`.${platformStatus.platform}$-indicator`).toggleClass(`${platformStatus.status}`); $(`.${PLATFORM}-indicator`).after(platformStatus.status.split('_').join(' ')); $('#StatusComponent').show(); }); }); })(LITHIUM.jQuery); </@liaAddScript> This liaAddScript directive describes the GET endpoint to retrieve the status of the various products you want to monitor. The url represents the status page of the product. The success function tags the listOfPlatformStatuses. The listOfPlatformStatuses contain all the responses of the platform from the GET API call. The forEach function is a built-in function that helps us to trace the list elements one by one (i.e) the Platform status of your product. The toggleClass function toggles the CSS classes of the platform as per your custom styling. In the next line, we split the platform status based on ‘_’ and join with ‘ ‘. The $('#StatusComponent').show() displays the status of your product in the platform status banner. You will need to modify the above code to fit your specific use case. Final Step Add the customized component to the header of your community page and the platform status banner component is integrated into your community. Example If your platform status is degraded_performance, then based on split and join operation and the codes, we make it degraded performance. Then, the toggleClass operation is used to render the degraded performance based on your CSS styling. Finally, the degraded performance is displayed in the Platform Status banner component.613Views
Sign in to react to this post1Comment