diff --git a/src/components/LBDashboard/LBDashboard.jsx b/src/components/LBDashboard/LBDashboard.jsx index 49c2b55c42..b94d799f41 100644 --- a/src/components/LBDashboard/LBDashboard.jsx +++ b/src/components/LBDashboard/LBDashboard.jsx @@ -12,6 +12,7 @@ import { Card, } from 'reactstrap'; import ReviewWordCloud from './ReviewWordCloud/ReviewWordCloud'; +import RatingDistribution from './RatingDistribution/RatingDistribution'; import styles from './LBDashboard.module.css'; import DemandOverTime from './LbAnalytics/DemandOverTime/DemandOverTime'; import moment from 'moment'; @@ -343,7 +344,14 @@ export function LBDashboard() { - + + + + + + + + ); diff --git a/src/components/LBDashboard/RatingDistribution/RatingDistribution.jsx b/src/components/LBDashboard/RatingDistribution/RatingDistribution.jsx new file mode 100644 index 0000000000..266d1ac8a5 --- /dev/null +++ b/src/components/LBDashboard/RatingDistribution/RatingDistribution.jsx @@ -0,0 +1,410 @@ +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 { VILLAGE_OPTIONS, PROPERTY_OPTIONS, getCustomSelectStyles } from '../constants'; +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 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(VILLAGE_OPTIONS); + const [selectedProperties, setSelectedProperties] = useState([]); + const [fromDate, setFromDate] = useState(''); + const [toDate, setToDate] = useState(''); + const [chartData, setChartData] = useState(null); + + const customSelectStyles = getCustomSelectStyles(darkMode); + + 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 + + + + Date Range: + + + + + + {/* Custom Date Range Inputs */} + {selectedDateRange.value === 'custom' && ( + + + + From: + + setFromDate(e.target.value)} + className={`${styles.dateInput} ${darkMode ? styles.darkInput : ''}`} + /> + + + + To: + + setToDate(e.target.value)} + className={`${styles.dateInput} ${darkMode ? styles.darkInput : ''}`} + /> + + + )} + + {/* Filters Section */} + + + + + Category: + + { + setSelectedCategory(option); + // Reset selections when category changes + if (option.value === 'village') { + setSelectedVillages(VILLAGE_OPTIONS); + setSelectedProperties([]); + } else { + setSelectedVillages([]); + setSelectedProperties([]); + } + }} + options={categoryOptions} + styles={customSelectStyles} + isSearchable={false} + /> + + + {selectedCategory.value === 'village' ? ( + + + Select Villages: + + + + ) : ( + + + Select Properties: + + + + )} + + + + {/* Chart */} + + {chartData && } + + + + ); +} + +RatingDistribution.propTypes = { + darkMode: PropTypes.bool, +}; + +export default RatingDistribution; diff --git a/src/components/LBDashboard/RatingDistribution/RatingDistribution.module.css b/src/components/LBDashboard/RatingDistribution/RatingDistribution.module.css new file mode 100644 index 0000000000..3e0bf61b26 --- /dev/null +++ b/src/components/LBDashboard/RatingDistribution/RatingDistribution.module.css @@ -0,0 +1,165 @@ +.ratingCard { + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + padding: 16px; + transition: all 0.3s ease; + min-height: 220px; +} + +.darkCard { + background-color: #1c2541; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + flex-wrap: wrap; + gap: 10px; +} + +.title { + font-size: 18px; + font-weight: 600; + color: #333; + margin: 0; +} + +.darkText { + color: #ffffff; +} + +.dateRangeSelector { + display: flex; + align-items: center; + gap: 10px; +} + +.label { + font-size: 14px; + font-weight: 500; + color: #555; + margin: 0; + white-space: nowrap; +} + +.selectInput { + min-width: 180px; +} + +.customDateRange { + display: flex; + gap: 15px; + margin-bottom: 12px; + flex-wrap: wrap; +} + +.dateInputGroup { + display: flex; + align-items: center; + gap: 10px; +} + +.dateInput { + padding: 8px 12px; + border: 1px solid #ccc; + border-radius: 6px; + font-size: 14px; + background-color: #fff; + color: #333; + min-width: 150px; +} + +.darkInput { + background-color: #1c2541; + border-color: #225163; + color: #fff; +} + +.dateInput:focus, +.darkInput:focus { + outline: none; + border-color: #8b5cf6; + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); +} + +.filtersSection { + margin-bottom: 12px; + padding: 10px; + background-color: #f8f9fa; + border-radius: 8px; +} + +.darkCard .filtersSection { + background-color: #0d1b2a; +} + +.filtersSection .label { + display: block; + margin-bottom: 8px; +} + +.chartContainer { + height: 350px; + margin-top: 12px; + position: relative; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .header { + flex-direction: column; + align-items: flex-start; + } + + .dateRangeSelector { + width: 100%; + } + + .selectInput { + flex: 1; + min-width: auto; + } + + .customDateRange { + flex-direction: column; + gap: 15px; + } + + .dateInputGroup { + width: 100%; + } + + .dateInput { + flex: 1; + } + + .chartContainer { + height: 300px; + } + + .title { + font-size: 20px; + } +} + +@media (max-width: 576px) { + .ratingCard { + padding: 15px; + } + + .title { + font-size: 18px; + } + + .label { + font-size: 13px; + } + + .chartContainer { + height: 250px; + } +} diff --git a/src/components/LBDashboard/constants.js b/src/components/LBDashboard/constants.js new file mode 100644 index 0000000000..adcf612b93 --- /dev/null +++ b/src/components/LBDashboard/constants.js @@ -0,0 +1,89 @@ +// Shared constants for LBDashboard components + +export const VILLAGE_OPTIONS = [ + { 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' }, +]; + +export const PROPERTY_OPTIONS = [ + { + 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' }, + ], + }, +]; + +export const getCustomSelectStyles = darkMode => ({ + 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) => ({ + ...provided, + backgroundColor: state.isFocused + ? darkMode + ? '#3A506B' + : '#f0f0f0' + : darkMode + ? '#1C2541' + : '#fff', + 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', + }), + input: provided => ({ + ...provided, + color: darkMode ? '#fff' : '#333', + }), +});