Skip to content

Commit

Permalink
Merge pull request #2219 from airqo-platform/analytics-ui-improvements
Browse files Browse the repository at this point in the history
QA feedback on the chart and card names on truncations
  • Loading branch information
Baalmart authored Nov 12, 2024
2 parents ca9b237 + 773f151 commit 0bead1c
Show file tree
Hide file tree
Showing 10 changed files with 8,417 additions and 8,536 deletions.
5 changes: 5 additions & 0 deletions platform/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
1 change: 1 addition & 0 deletions platform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"eslint-plugin-react-hooks": "^4.6.2",
"globals": "^15.9.0",
"prettier": "^3.3.3",
"typescript": "5.6.3",
"webpack": "^5.74.0"
},
"engines": {
Expand Down
79 changes: 66 additions & 13 deletions platform/src/common/components/AQNumberCard/index.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IconMap } from './IconMap';
import CustomTooltip from '../Tooltip';
import { useWindowSize } from '@/lib/windowSize';
import { setOpenModal, setModalType } from '@/lib/store/services/downloadModal';
import { fetchRecentMeasurementsData } from '@/lib/store/services/deviceRegistry/RecentMeasurementsSlice';
import PropTypes from 'prop-types';

// Constants
const AIR_QUALITY_LEVELS = [
Expand All @@ -25,8 +26,8 @@ const AIR_QUALITY_LEVELS = [
];

const MAX_CARDS = 4;
const TRUNCATE_LENGTH = 12;

// SkeletonCard Component
const SkeletonCard = () => (
<div className="w-full border border-gray-200 bg-white rounded-xl px-4 py-6">
<div className="flex flex-col justify-between h-[168px]">
Expand All @@ -51,6 +52,7 @@ const SkeletonCard = () => (
</div>
);

// SiteCard Component
const SiteCard = ({ site, onOpenModal, windowWidth, pollutantType }) => {
const measurements = useSelector(
(state) => state.recentMeasurements.measurements,
Expand All @@ -77,11 +79,37 @@ const SiteCard = ({ site, onOpenModal, windowWidth, pollutantType }) => {
}, [reading]);

const AirQualityIcon = IconMap[status];
const truncatedName = !site.name
? '---'
: site.name.length > TRUNCATE_LENGTH
? `${site.name.slice(0, TRUNCATE_LENGTH)}...`
: site.name;

// Ref and state for detecting text truncation
const nameRef = useRef(null);
const [isTruncated, setIsTruncated] = useState(false);

useEffect(() => {
const checkTruncation = () => {
const el = nameRef.current;
if (el) {
setIsTruncated(el.scrollWidth > el.clientWidth);
}
};

// Initial check
checkTruncation();

// Re-check on window resize
window.addEventListener('resize', checkTruncation);
return () => {
window.removeEventListener('resize', checkTruncation);
};
}, [site.name, windowWidth]);

const siteNameElement = (
<div
ref={nameRef}
className="text-gray-800 text-lg font-medium capitalize text-left w-full max-w-[185px] md:max-w-full lg:max-w-[185px] overflow-hidden text-ellipsis whitespace-nowrap"
>
{site.name || '---'}
</div>
);

return (
<button
Expand All @@ -91,12 +119,16 @@ const SiteCard = ({ site, onOpenModal, windowWidth, pollutantType }) => {
<div className="relative w-full flex flex-col justify-between bg-white border border-gray-200 rounded-xl px-4 py-6 h-[200px] shadow-sm hover:shadow-md transition-shadow duration-200 ease-in-out cursor-pointer">
<div className="flex justify-between items-center mb-4">
<div>
<div
className="text-gray-800 text-lg font-medium capitalize text-left max-w-full"
title={site.name || 'No Location Data'}
>
{truncatedName}
</div>
{isTruncated ? (
<CustomTooltip
tooltipsText={site.name || 'No Location Data'}
position="top"
>
{siteNameElement}
</CustomTooltip>
) : (
siteNameElement
)}
<div className="text-base text-left text-slate-400 capitalize">
{site.country || '---'}
</div>
Expand Down Expand Up @@ -139,6 +171,18 @@ const SiteCard = ({ site, onOpenModal, windowWidth, pollutantType }) => {
);
};

SiteCard.propTypes = {
site: PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string,
country: PropTypes.string,
}).isRequired,
onOpenModal: PropTypes.func.isRequired,
windowWidth: PropTypes.number.isRequired,
pollutantType: PropTypes.string.isRequired,
};

// AddLocationCard Component
const AddLocationCard = ({ onOpenModal }) => (
<button
onClick={() => onOpenModal('addLocation')}
Expand All @@ -149,6 +193,11 @@ const AddLocationCard = ({ onOpenModal }) => (
</button>
);

AddLocationCard.propTypes = {
onOpenModal: PropTypes.func.isRequired,
};

// AQNumberCard Component
const AQNumberCard = ({ className = '' }) => {
const dispatch = useDispatch();
const { width: windowWidth } = useWindowSize();
Expand Down Expand Up @@ -238,4 +287,8 @@ const AQNumberCard = ({ className = '' }) => {
);
};

AQNumberCard.propTypes = {
className: PropTypes.string,
};

export default React.memo(AQNumberCard);
24 changes: 2 additions & 22 deletions platform/src/common/components/Charts/ChartContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ const ChartContainer = ({
const [loadingFormat, setLoadingFormat] = useState(null);
const [downloadComplete, setDownloadComplete] = useState(null);

// State to control skeleton loader display
const [showSkeleton, setShowSkeleton] = useState(chartLoading);

// Handle click outside for dropdown
useEffect(() => {
const handleClickOutside = (event) => {
Expand All @@ -53,28 +50,11 @@ const ChartContainer = ({
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

// TEMPORARY: Handle loading issue, remove later
useEffect(() => {
let timer;

if (chartLoading) {
setShowSkeleton(true);
} else {
timer = setTimeout(() => {
setShowSkeleton(false);
}, 4000);
}

return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [chartLoading]);
// Update skeleton loader based on loading state
const showSkeleton = chartLoading;

/**
* Exports the chart in the specified format.
*
* @param {string} format - The format to export the chart (png, jpg, pdf).
*/
const exportChart = useCallback(async (format) => {
Expand Down
47 changes: 27 additions & 20 deletions platform/src/common/components/Charts/components/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ export const truncate = (str) => {
return str.length > 20 ? str.substr(0, 20 - 1) + '...' : str;
};

const truncate2 = (value) => {
return value.length > 10 ? `${value.substring(0, 10)}...` : value;
};

/**
* @param {Number} value
* @returns {Object}
Expand Down Expand Up @@ -210,12 +206,13 @@ const CustomDot = (props) => {
* Customized legend component
* @param {Object} props
*/
const renderCustomizedLegend = (props) => {
const { payload } = props;
const renderCustomizedLegend = ({ payload }) => {
// Determine if truncation is needed based on the number of locations
const shouldTruncate = payload.length > 3;

// Sort the payload array from darkest to lightest color
const sortedPayload = React.useMemo(() => {
return payload.sort((a, b) => {
return [...payload].sort((a, b) => {
const colorToGrayscale = (color) => {
if (color) {
const hex = color.replace('#', '');
Expand All @@ -231,24 +228,34 @@ const renderCustomizedLegend = (props) => {
}, [payload]);

return (
<div className="py-4 flex flex-wrap gap-2 justify-end w-full">
<div className="relative flex flex-wrap justify-end gap-2 w-full p-2">
{sortedPayload.map((entry, index) => (
<div
key={index}
style={{
color: '#485972',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
}}
className="tooltip tooltip-top gap-1 w-auto text-[12px] leading-[14px] font-normal outline-none text-[#485972]"
data-tip={entry.value}
className="flex items-center gap-1 text-xs text-gray-700 whitespace-nowrap relative"
>
<div
className="w-2 h-2 m-0 p-0 rounded-full inline-block"
<span
className="w-2 h-2 rounded-full"
style={{ backgroundColor: entry.color }}
></div>
<p className="m-0 p-0">{truncate2(entry.value)}</p>
></span>

{/* Only truncate and add tooltip if shouldTruncate is true */}
<span
className={`${shouldTruncate ? 'truncate max-w-[100px] group' : ''}`}
title={shouldTruncate ? entry.value : null}
>
{entry.value}
</span>

{/* Tooltip appears only if truncation is applied */}
{shouldTruncate && (
<div className="absolute bottom-full mb-1 hidden group-hover:flex flex-col items-center">
<span className="text-xs text-white bg-gray-700 px-2 py-1 rounded-md shadow-md">
{entry.value}
</span>
<span className="w-2 h-2 bg-gray-700 rotate-45 transform -translate-y-1/2"></span>
</div>
)}
</div>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import jsPDF from 'jspdf';
import 'jspdf-autotable';
import { saveAs } from 'file-saver';
import CustomToast from '../../../Toast/CustomToast';
import { startOfDay, endOfDay } from 'date-fns';

/**
* Header component for the Download Data modal.
Expand Down Expand Up @@ -110,13 +109,6 @@ const DataDownload = ({ onClose }) => {
setFormData((prevData) => ({ ...prevData, [id]: option }));
}, []);

const toLocalISOString = (date) => {
const offsetDate = new Date(
date.getTime() - date.getTimezoneOffset() * 60000,
);
return offsetDate.toISOString().slice(0, 19); // Remove milliseconds and 'Z'
};

/**
* Handles the submission of the form.
* Prepares data and calls the exportDataApi to download the data.
Expand All @@ -126,7 +118,7 @@ const DataDownload = ({ onClose }) => {
async (e) => {
e.preventDefault();
setDownloadLoading(true);
setFormError(''); // Reset previous errors
setFormError('');

// Validate form data
if (
Expand All @@ -149,17 +141,16 @@ const DataDownload = ({ onClose }) => {

// Prepare data for API
const apiData = {
startDateTime:
toLocalISOString(startOfDay(formData.duration.name.start)) + ':00',
endDateTime:
toLocalISOString(endOfDay(formData.duration.name.end)) + ':59',
startDateTime: new Date(formData.duration.name.start).toISOString(),
endDateTime: new Date(formData.duration.name.end).toISOString(),
sites: selectedSites.map((site) => site._id),
network: formData.network.name,
datatype: formData.dataType.name.toLowerCase(),
pollutants: [formData.pollutant.name.toLowerCase().replace('.', '_')],
resolution: formData.frequency.name.toLowerCase(),
downloadType: formData.fileType.name.toLowerCase(),
outputFormat: 'airqo-standard',
minimum: true,
};

try {
Expand Down
4 changes: 2 additions & 2 deletions platform/src/common/components/TopBar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SettingsIcon from '@/icons/SideBar/SettingsIcon';
import UserIcon from '@/icons/Topbar/userIcon';
import ChartIcon from '@/icons/Topbar/chartIcon';
import CustomDropdown from '../Dropdowns/CustomDropdown';
import TopBarSearch from '../search/TopBarSearch';
// import TopBarSearch from '../search/TopBarSearch';
import { setOpenModal, setModalType } from '@/lib/store/services/downloadModal';
import {
setToggleDrawer,
Expand Down Expand Up @@ -217,7 +217,7 @@ const TopBar = ({ topbarTitle, noBorderBottom, showSearch = false }) => {
handleOpenModal('search');
}}
>
<TopBarSearch customWidth="md:max-w-[192px]" />
{/* <TopBarSearch customWidth="md:max-w-[192px]" /> */}
</button>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion platform/src/pages/Home/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const Home = () => {
}, [cardCheckList, steps]);

// console.info('checkListData', checkListData);
console.info('cardCheckList', cardCheckList);
// console.info('cardCheckList', cardCheckList);

return (
<Layout noBorderBottom pageTitle="Home" topbarTitle="Home">
Expand Down
Loading

0 comments on commit 0bead1c

Please sign in to comment.