Development
App Pages
PulseChecks - Done
16 min
view live example https //userfirst factory webflow\ io/360pulse/pulsechecks overview the pulsechecks dashboard serves as the primary entry point for users after authentication this page displays all of a user's pulsechecks in a vertical tile layout, providing quick access to key metrics and allowing users to monitor multiple surveys simultaneously key components the pulsechecks dashboard consists of several key components working together to create a cohesive user experience page title document title "pulsechecks | 360pulse" breadcrumb navigation shows hierarchy context ("pulsechecks") page heading "pulsechecks" (prominent h1 element) page description "configure and manage your pulsecheck surveys" action controls new pulsecheck button prominently positioned in the top right corner primary button using brand color triggers the pulsecheck creation wizard consistently accessible throughout the interface pulsecheck cards each pulsecheck is represented as an interactive card displaying essential metrics card header contains title, metadata, and action buttons metrics display quadrant layout with four key performance indicators card footer contains timing information and view details link interactive elements charts, trend indicators, and action buttons add pulsecheck "ghost tile" implements a "ghost tile" design pattern for creating new items visually distinct from regular tiles (lighter styling) contains plus icon and "add pulsecheck" label same interaction target size as regular tiles for consistency layout structure responsive grid adapts to different screen sizes vertical stack cards arranged in a vertical flow card anatomy consistent internal structure across all pulsecheck cards visual hierarchy emphasizes important metrics with size and positioning pulsecheck card component each pulsecheck card is a self contained component that displays four key metrics in a quadrant layout card header title (e g , "client happiness", "employee pulse") frequency and audience information (e g , "monthly • clients", "quarterly • employees") action buttons for viewing dashboard and accessing settings metrics grid the card contains four main metric sections arranged in a 2×2 grid pulse score (top left) large numerical display (e g , "84") list of color coded question items with labels circular chart visualization with trend indicator net promoter score (top right) score with trend indicator (e g , "32") color coded nps scale showing current position range indicators ("needs improvement", "good", "great", "excellent") likely to recommend (bottom left) percentage display (e g , "48%") horizontal stacked bar showing promoter/passive/detractor distribution circular chart visualization response rate (bottom right) percentage with trend indicator (e g , "24%") respondent counts (e g , "34 of 50 responded") progress bar visualization with percentage completion card footer survey timing information ("last sent 2 weeks ago • next in 2 weeks") "view details" link data attributes and visualization the pulsechecks component uses a well defined set of data attributes to control its visualizations chart elements and their attributes circular progress charts ( pulsecheck tile chart) data chart value current value (0 100) to display data chart value previous previous value for trend comparison implementation svg circle with animated stroke dashoffset trend indicators (up/down/no change) based on comparison with previous value nps indicator ( nps legend) data chart value nps score ( 100 to 100) implementation animated caret that positions itself along the nps scale css transitions for smooth animation color coded ranges with descriptive labels stacked bar charts implementation three segments for detractors, passives, and promoters color coded segments (red, yellow, green) interactive tooltips with detailed breakdowns css transforms for animation progress bars ( progress bar container) data chart value percentage value (0 100) implementation animated width transition color mapping based on percentage value numerical overlay showing exact percentage score indicators ( pulsecheck tile pulsescore area) data chart value score value to display and use for color coding implementation color coded icons using css variables tooltips showing exact score percentages page structure html pulsechecks pulsechecks configure and manage your pulsecheck surveys new pulsecheck client happiness {pulsecheck frequency (e g monthly)} • {pulsecheck audience name plural (e g clients)} pulse score {overall pulse score} helpdesk net promoter score® {nps score} needs improvement( 100 0) good(0 30) great(30 70) excellent(70 100) likely to recommend {percentage of promoters (e g 48%)} response rate {response rate percentage e g 24%} {number of respondents who completed the survey e g 34} of {total number of respondents invited 50} responded {response rate percentage e g 24%} last sent {when was it last sent e g "2 weeks ago"} • next {when will it send next e g "in 2 weeks"} view details add pulsecheck powered by user first privacy terms of use javascript // consolidated pulsecheck dashboard script (function() { // add minimal css const styleelement = document createelement('style'); styleelement textcontent = ` progress bar progress { height 100%; transition width 0 5s ease out; width 0%; } nps caret { position absolute; top 0; width 0; height 0; border left 8px solid transparent; border right 8px solid transparent; border top 8px solid currentcolor; transform translatey( 100%); z index 2; transition left 500ms ease out; } `; document head appendchild(styleelement); // svg icons for trends const trendsvg = { up `\<svg xmlns="http //www w3 org/2000/svg" width="16" height="16" viewbox="0 0 24 24" fill="none" stroke="var( 360pulse trend up)" stroke width="2" stroke linecap="round" stroke linejoin="round">\<polyline points="23 6 13 5 15 5 8 5 10 5 1 18">\</polyline>\<polyline points="17 6 23 6 23 12">\</polyline>\</svg>`, dn `\<svg xmlns="http //www w3 org/2000/svg" width="16" height="16" viewbox="0 0 24 24" fill="none" stroke="var( 360pulse trend down)" stroke width="2" stroke linecap="round" stroke linejoin="round">\<polyline points="23 18 13 5 8 5 8 5 13 5 1 6">\</polyline>\<polyline points="17 18 23 18 23 12">\</polyline>\</svg>`, nc `\<svg xmlns="http //www w3 org/2000/svg" width="16" height="16" viewbox="0 0 24 24" fill="none" stroke="var( 360pulse trend no change)" stroke width="2" stroke linecap="round" stroke linejoin="round">\<circle cx="12" cy="12" r="1">\</circle>\<circle cx="19" cy="12" r="1">\</circle>\<circle cx="5" cy="12" r="1">\</circle>\</svg>` }; // color function function getcolorvariable(score) { if (score < 0 || score > 100) return ' 360pulse score color neutral'; if (score >= 0 && score <= 9) return ' 360pulse score color 0 9'; else if (score >= 10 && score <= 19) return ' 360pulse score color 10 19'; else if (score >= 20 && score <= 29) return ' 360pulse score color 20 29'; else if (score >= 30 && score <= 39) return ' 360pulse score color 30 39'; else if (score >= 40 && score <= 49) return ' 360pulse score color 40 49'; else if (score >= 50 && score <= 59) return ' 360pulse score color 50 59'; else if (score >= 60 && score <= 69) return ' 360pulse score color 60 69'; else if (score >= 70 && score <= 79) return ' 360pulse score color 70 79'; else if (score >= 80 && score <= 89) return ' 360pulse score color 80 89'; else if (score >= 90 && score <= 99) return ' 360pulse score color 90 99'; else if (score === 100) return ' 360pulse score color 100'; } function easeoutquad(t) { return t (2 t); } function parsenumberwithpercent(text) { const haspercent = text endswith('%'); let cleantext = haspercent ? text substring(0, text length 1) trim() text; const value = parsefloat(cleantext); return !isnan(value) ? { value, haspercent } null; } function inittooltips(selector) { if (typeof tippy !== 'undefined') { tippy(selector, { allowhtml true, animation 'fade' }); } } function initdonutcharts() { const charts = document queryselectorall(' pulsecheck tile chart\ empty'); if (charts length === 0) return; charts foreach(function(chart) { let currentvalue = null; let previousvalue = null; if (chart hasattribute('data chart value')) { currentvalue = parsefloat(chart getattribute('data chart value')); } else if (chart hasattribute('data chart value current')) { currentvalue = parsefloat(chart getattribute('data chart value current')); } if (chart hasattribute('data chart value previous')) { previousvalue = parsefloat(chart getattribute('data chart value previous')); } if (currentvalue === null || isnan(currentvalue)) { currentvalue = 70; } renderdonutchart(chart, currentvalue, previousvalue); }); settimeout(() => inittooltips(' pulsecheck tile chart svg'), 100); } function renderdonutchart(container, currentvalue, previousvalue) { currentvalue = math max(0, math min(100, currentvalue)); const colorvar = getcolorvariable(currentvalue); const radius = 40; const circumference = 2 math pi radius; const strokedashoffset = circumference (1 currentvalue / 100); let trendiconhtml = ""; let tooltipcontent = `current ${currentvalue}%`; if (previousvalue !== null && !isnan(previousvalue)) { tooltipcontent += `\<br>previous ${previousvalue}%`; let trendkey; if (currentvalue > previousvalue) { trendkey = 'up'; tooltipcontent += `\<br>\<span style="color var( 360pulse trend up)">▲ up ${(currentvalue previousvalue) tofixed(1)}%\</span>`; } else if (currentvalue < previousvalue) { trendkey = 'dn'; tooltipcontent += `\<br>\<span style="color var( 360pulse trend down)">▼ down ${(previousvalue currentvalue) tofixed(1)}%\</span>`; } else { trendkey = 'nc'; tooltipcontent += `\<br>\<span style="color var( 360pulse trend no change)">no change\</span>`; } trendiconhtml = `\<g transform="translate(50, 50) scale(1 2) translate( 8, 8)">${trendsvg\[trendkey]}\</g>`; } const animationid = math random() tostring(36) substr(2, 9); const svg = ` \<svg width="100%" height="100%" viewbox="0 0 100 100" data tippy content="${tooltipcontent}"> \<circle cx="50" cy="50" r="${radius}" fill="none" stroke="var( brand colors gray 200, #d1d5db)" stroke width="8" stroke linecap="round"/> \<circle cx="50" cy="50" r="${radius}" fill="none" stroke="var(${colorvar}, #22c55e)" stroke width="8" stroke dasharray="${circumference}" stroke dashoffset="${circumference}" stroke linecap="round" transform="rotate( 90 50 50)"> \<animate attributename="stroke dashoffset" from="${circumference}" to="${strokedashoffset}" dur="500ms" fill="freeze" begin="indefinite" id="arc animation ${animationid}"/> \</circle> ${trendiconhtml} \</svg> `; container innerhtml = svg; settimeout(() => { const circle = container queryselector(`#arc animation ${animationid}`); if (circle && typeof circle beginelement === 'function') { circle beginelement(); } }, 50); } function initnpscarets() { const legends = document queryselectorall(' nps legend\ not(\ has( nps caret))'); if (legends length === 0) return; legends foreach(function(legend) { const score = legend hasattribute('data chart value') ? parsefloat(legend getattribute('data chart value')) null; if (score !== null && !isnan(score)) { addcarettolegend(score, legend); } }); } function addcarettolegend(score, legendelement) { score = math max( 100, math min(100, score)); const percentage = ((score + 100) / 200) 100; const existingcarets = legendelement queryselectorall(' nps caret'); existingcarets foreach(caret => caret remove()); const caret = document createelement('div'); caret classname = 'nps caret'; caret style left = 'calc(0% 8px)'; legendelement appendchild(caret); if (window\ getcomputedstyle(legendelement) position !== 'relative') { legendelement style position = 'relative'; } settimeout(() => caret style left = `calc(${percentage}% 8px)`, 10); } function initprogressbars() { setprogressbarcolors(); settimeout(animateprogressbars, 50); } function setprogressbarcolors() { const containers = document queryselectorall(' progress bar container\ not( colored)'); if (containers length === 0) return; containers foreach(function(container) { const value = container hasattribute('data chart value') ? parsefloat(container getattribute('data chart value')) null; if (value !== null && !isnan(value)) { const safevalue = math max(0, math min(100, value)); const progressbarelement = container queryselector(' progress bar progress'); if (progressbarelement) { const colorvar = getcolorvariable(safevalue); progressbarelement style backgroundcolor = `var(${colorvar})`; progressbarelement setattribute('data target width', safevalue); progressbarelement innerhtml = `\<div>${safevalue}%\</div>`; container setattribute('data tippy content', `progress ${safevalue}%`); container classlist add('colored'); } } }); inittooltips(' progress bar container'); } function animateprogressbars() { const containers = document queryselectorall(' progress bar container colored\ not( animated)'); if (containers length === 0) return; containers foreach(function(container) { const progressbarelement = container queryselector(' progress bar progress'); if (progressbarelement) { const targetvalue = progressbarelement getattribute('data target width'); if (targetvalue !== null) { progressbarelement style width = targetvalue + '%'; container classlist add('animated'); } } }); } function initnumbercounters() { const statcontainers = document queryselectorall(' pulsecheck tile stat\ not( counted)'); if (statcontainers length === 0) return; statcontainers foreach(function(container) { const textcontainer = container queryselector('\ scope > div\ first child'); if (!textcontainer) return; const originaltext = textcontainer textcontent trim(); const parsedvalue = parsenumberwithpercent(originaltext); if (parsedvalue) { if (!textcontainer hasattribute('data original value')) { textcontainer setattribute('data original value', originaltext); textcontainer setattribute('data target value', parsedvalue value); textcontainer setattribute('data has percent', parsedvalue haspercent ? 'true' 'false'); animatenumber(textcontainer, parsedvalue value, parsedvalue haspercent); } container classlist add('counted'); } }); } function animatenumber(element, targetvalue, haspercent) { const duration = 500; const framespersecond = 60; const totalframes = duration / 500 framespersecond; let currentframe = 0; const startvalue = 0; const isinteger = number isinteger(targetvalue); const interval = setinterval(function() { const progress = currentframe / totalframes; const easedprogress = easeoutquad(progress); let currentvalue = startvalue + (targetvalue startvalue) easedprogress; if (isinteger) { currentvalue = math round(currentvalue); } else { currentvalue = math round(currentvalue 10) / 10; } element textcontent = currentvalue + (haspercent ? '%' ''); currentframe++; if (currentframe > totalframes) { clearinterval(interval); element textcontent = targetvalue + (haspercent ? '%' ''); } }, 500 / framespersecond); } function initpulsescoreareas() { const pulsescoreareas = document queryselectorall(' pulsecheck tile pulsescore area'); if (pulsescoreareas length === 0) return; pulsescoreareas foreach(function(area) { const chartvalue = parsefloat(area getattribute('data chart value')); if (isnan(chartvalue)) return; const colorvariable = `var(${getcolorvariable(chartvalue)})`; const iconelement = area queryselector(' icon'); if (iconelement) { iconelement style color = colorvariable; } area setattribute('data tippy content', math round(chartvalue) + '%'); area setattribute('data tippy placement', 'top start'); }); inittooltips(' pulsecheck tile pulsescore area'); } function initall() { initdonutcharts(); initnpscarets(); initprogressbars(); initnumbercounters(); initpulsescoreareas(); } if (document readystate === 'loading') { document addeventlistener('domcontentloaded', initall); } else { initall(); } window\ addeventlistener('load', initall); })(); interactive components element page dasboard icon and "view details" link docid\ qlger943dlncvfmkaqvmb settings icon docid fmgs5ceolgp3cezlrs5e "new pulsecheck" button or "new pulsecheck" tile docid\ fp1amypz6ibofu s566nw