From a85ad28824bb76df51e20371ad9af91075dd5e17 Mon Sep 17 00:00:00 2001 From: Namitha Pawar <176259712+Namitha7070@users.noreply.github.com> Date: Fri, 26 Dec 2025 15:44:13 -0800 Subject: [PATCH 1/3] Add Rating Distribution bar chart to LB Dashboard --- src/components/LBDashboard/LBDashboard.jsx | 10 +- .../RatingDistribution/RatingDistribution.jsx | 493 ++++++++++++++++++ .../RatingDistribution.module.css | 165 ++++++ 3 files changed, 667 insertions(+), 1 deletion(-) create mode 100644 src/components/LBDashboard/RatingDistribution/RatingDistribution.jsx create mode 100644 src/components/LBDashboard/RatingDistribution/RatingDistribution.module.css diff --git a/src/components/LBDashboard/LBDashboard.jsx b/src/components/LBDashboard/LBDashboard.jsx index ea20dc1f73..31783593ff 100644 --- a/src/components/LBDashboard/LBDashboard.jsx +++ b/src/components/LBDashboard/LBDashboard.jsx @@ -15,6 +15,7 @@ import { CardBody, } from 'reactstrap'; import ReviewWordCloud from './ReviewWordCloud/ReviewWordCloud'; +import RatingDistribution from './RatingDistribution/RatingDistribution'; import styles from './LBDashboard.module.css'; const METRIC_OPTIONS = { @@ -244,7 +245,14 @@ export function LBDashboard() { Insights from Reviews
- + + + + + + + +
diff --git a/src/components/LBDashboard/RatingDistribution/RatingDistribution.jsx b/src/components/LBDashboard/RatingDistribution/RatingDistribution.jsx new file mode 100644 index 0000000000..813cb8c6bc --- /dev/null +++ b/src/components/LBDashboard/RatingDistribution/RatingDistribution.jsx @@ -0,0 +1,493 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { Bar } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import Select from 'react-select'; +import { Row, Col, Card, CardBody } from 'reactstrap'; +import styles from './RatingDistribution.module.css'; + +ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); + +function RatingDistribution({ darkMode }) { + // Mock data - Replace with actual API data + const mockReviewsData = [ + { + id: 1, + rating: 5, + village: 'Eco Village', + property: 'Mountain View', + date: '2025-12-01', + }, + { + id: 2, + rating: 5, + village: 'Eco Village', + property: 'Solar Haven', + date: '2025-12-05', + }, + { + id: 3, + rating: 4, + village: 'Forest Retreat', + property: 'Lakeside Cottage', + date: '2025-12-10', + }, + { + id: 4, + rating: 5, + village: 'Desert Oasis', + property: 'Tiny Home', + date: '2025-12-12', + }, + { + id: 5, + rating: 3, + village: 'River Valley', + property: 'Riverside Cabin', + date: '2025-11-15', + }, + { + id: 6, + rating: 4, + village: 'City Sanctuary', + property: 'Urban Garden Apartment', + date: '2025-11-20', + }, + { + id: 7, + rating: 5, + village: 'Eco Village', + property: 'Mountain View', + date: '2025-11-25', + }, + { + id: 8, + rating: 2, + village: 'Forest Retreat', + property: 'Woodland Cabin', + date: '2025-10-05', + }, + { + id: 9, + rating: 3, + village: 'Desert Oasis', + property: 'Earth Ship', + date: '2025-10-10', + }, + { + id: 10, + rating: 1, + village: 'River Valley', + property: 'Floating House', + date: '2025-09-15', + }, + ]; + + const villageOptions = [ + { value: 'Eco Village', label: 'Eco Village' }, + { value: 'Forest Retreat', label: 'Forest Retreat' }, + { value: 'Desert Oasis', label: 'Desert Oasis' }, + { value: 'River Valley', label: 'River Valley' }, + { value: 'City Sanctuary', label: 'City Sanctuary' }, + ]; + + const propertyOptions = [ + { + label: 'Eco Village', + options: [ + { value: 'Mountain View', label: 'Mountain View' }, + { value: 'Solar Haven', label: 'Solar Haven' }, + ], + }, + { + label: 'Forest Retreat', + options: [ + { value: 'Lakeside Cottage', label: 'Lakeside Cottage' }, + { value: 'Woodland Cabin', label: 'Woodland Cabin' }, + ], + }, + { + label: 'Desert Oasis', + options: [ + { value: 'Tiny Home', label: 'Tiny Home' }, + { value: 'Earth Ship', label: 'Earth Ship' }, + ], + }, + { + label: 'River Valley', + options: [ + { value: 'Riverside Cabin', label: 'Riverside Cabin' }, + { value: 'Floating House', label: 'Floating House' }, + ], + }, + { + label: 'City Sanctuary', + options: [ + { value: 'Urban Garden Apartment', label: 'Urban Garden Apartment' }, + { value: 'Eco Loft', label: 'Eco Loft' }, + ], + }, + ]; + + const dateRangeOptions = [ + { value: 'all', label: 'All Time' }, + { value: 'last30', label: 'Last 30 Days' }, + { value: 'last60', label: 'Last 60 Days' }, + { value: 'last90', label: 'Last 90 Days' }, + { value: 'custom', label: 'Custom Range' }, + ]; + + const categoryOptions = [ + { value: 'village', label: 'By Village' }, + { value: 'property', label: 'By Property' }, + ]; + + const [selectedDateRange, setSelectedDateRange] = useState(dateRangeOptions[0]); + const [selectedCategory, setSelectedCategory] = useState(categoryOptions[0]); + const [selectedVillages, setSelectedVillages] = useState(villageOptions); + const [selectedProperties, setSelectedProperties] = useState([]); + const [fromDate, setFromDate] = useState(''); + const [toDate, setToDate] = useState(''); + const [chartData, setChartData] = useState(null); + + const customSelectStyles = { + control: provided => ({ + ...provided, + backgroundColor: darkMode ? '#1C2541' : '#fff', + borderColor: darkMode ? '#225163' : '#ccc', + color: darkMode ? '#fff' : '#333', + minHeight: '38px', + }), + menu: provided => ({ + ...provided, + backgroundColor: darkMode ? '#1C2541' : '#fff', + zIndex: 1000, + }), + option: (provided, state) => { + let optionBg; + if (state.isFocused) { + optionBg = darkMode ? '#3A506B' : '#f0f0f0'; + } else { + optionBg = darkMode ? '#1C2541' : '#fff'; + } + return { + ...provided, + backgroundColor: optionBg, + color: darkMode ? '#fff' : '#333', + }; + }, + multiValue: provided => ({ + ...provided, + backgroundColor: darkMode ? '#3A506B' : '#e2e3fc', + }), + multiValueLabel: provided => ({ + ...provided, + color: darkMode ? '#fff' : '#333', + }), + singleValue: provided => ({ + ...provided, + color: darkMode ? '#fff' : '#333', + }), + }; + + useEffect(() => { + // Filter reviews based on selected filters + let filteredReviews = [...mockReviewsData]; + + // Apply date filter + if (selectedDateRange.value !== 'all') { + const today = new Date(); + let startDate; + + if (selectedDateRange.value === 'last30') { + startDate = new Date(today.setDate(today.getDate() - 30)); + } else if (selectedDateRange.value === 'last60') { + startDate = new Date(today.setDate(today.getDate() - 60)); + } else if (selectedDateRange.value === 'last90') { + startDate = new Date(today.setDate(today.getDate() - 90)); + } else if (selectedDateRange.value === 'custom' && fromDate && toDate) { + startDate = new Date(fromDate); + const endDate = new Date(toDate); + filteredReviews = filteredReviews.filter(review => { + const reviewDate = new Date(review.date); + return reviewDate >= startDate && reviewDate <= endDate; + }); + } + + if (selectedDateRange.value !== 'custom') { + filteredReviews = filteredReviews.filter(review => new Date(review.date) >= startDate); + } + } + + // Apply category filter + if (selectedCategory.value === 'village' && selectedVillages.length > 0) { + const villageValues = selectedVillages.map(v => v.value); + filteredReviews = filteredReviews.filter(review => villageValues.includes(review.village)); + } else if (selectedCategory.value === 'property' && selectedProperties.length > 0) { + const propertyValues = selectedProperties.map(p => p.value); + filteredReviews = filteredReviews.filter(review => propertyValues.includes(review.property)); + } + + // Calculate rating distribution + const ratingCounts = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; + filteredReviews.forEach(review => { + ratingCounts[review.rating] = (ratingCounts[review.rating] || 0) + 1; + }); + + // Prepare chart data with gradient colors + const data = { + labels: ['1★', '2★', '3★', '4★', '5★'], + datasets: [ + { + label: 'Number of Reviews', + data: [ + ratingCounts[1], + ratingCounts[2], + ratingCounts[3], + ratingCounts[4], + ratingCounts[5], + ], + backgroundColor: [ + 'rgba(139, 92, 246, 0.3)', // 1 star - lightest purple + 'rgba(139, 92, 246, 0.45)', // 2 stars + 'rgba(139, 92, 246, 0.6)', // 3 stars + 'rgba(139, 92, 246, 0.75)', // 4 stars + 'rgba(139, 92, 246, 0.9)', // 5 stars - darkest purple + ], + borderColor: [ + 'rgba(139, 92, 246, 0.6)', + 'rgba(139, 92, 246, 0.7)', + 'rgba(139, 92, 246, 0.8)', + 'rgba(139, 92, 246, 0.9)', + 'rgba(139, 92, 246, 1)', + ], + borderWidth: 1, + borderRadius: 8, + }, + ], + }; + + setChartData(data); + }, [selectedDateRange, selectedCategory, selectedVillages, selectedProperties, fromDate, toDate]); + + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + title: { + display: false, + }, + tooltip: { + backgroundColor: darkMode ? '#1C2541' : '#fff', + titleColor: darkMode ? '#fff' : '#333', + bodyColor: darkMode ? '#fff' : '#333', + borderColor: darkMode ? '#225163' : '#ccc', + borderWidth: 1, + callbacks: { + label: context => { + const label = context.dataset.label || ''; + const value = context.parsed.y; + return `${label}: ${value}`; + }, + }, + }, + }, + scales: { + x: { + grid: { + display: false, + }, + ticks: { + color: darkMode ? '#fff' : '#333', + font: { + size: 14, + }, + }, + }, + y: { + beginAtZero: true, + grid: { + display: true, + color: darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.15)', + lineWidth: 1, + }, + ticks: { + color: darkMode ? '#fff' : '#333', + font: { + size: 12, + }, + stepSize: 1, + }, + title: { + display: true, + text: 'Number of Reviews', + color: darkMode ? '#fff' : '#333', + font: { + size: 14, + weight: 'bold', + }, + }, + }, + }, + }; + + return ( + + + {/* Header with Title and Date Range */} +
+

+ Rating Distribution +

+
+ + setFromDate(e.target.value)} + className={`${styles.dateInput} ${darkMode ? styles.darkInput : ''}`} + /> +
+
+ + setToDate(e.target.value)} + className={`${styles.dateInput} ${darkMode ? styles.darkInput : ''}`} + /> +
+
+ )} + + {/* Filters Section */} +
+ + + + + + ) : ( + + +