From b2fcb16d12c71a4c0b2ec991cff685a1403be546 Mon Sep 17 00:00:00 2001 From: fahed Date: Tue, 3 Feb 2026 15:29:03 +0300 Subject: [PATCH] Restore working state from f17e19f (before mobile overhaul) Reverting all my changes that broke the desktop layout. Starting fresh for mobile improvements. --- src/App.css | 209 ------------------------ src/App.js | 71 +++++---- src/components/ChartExport.js | 145 +++-------------- src/components/Comparison.js | 248 +++++++++++++---------------- src/components/Dashboard.js | 177 ++++++++++---------- src/components/Slides.js | 121 ++++++-------- src/components/shared/Carousel.jsx | 76 ++------- src/config/chartConfig.js | 10 +- src/index.js | 5 +- 9 files changed, 323 insertions(+), 739 deletions(-) diff --git a/src/App.css b/src/App.css index c6fc662..5cfea18 100644 --- a/src/App.css +++ b/src/App.css @@ -325,37 +325,6 @@ body { font-size: 0.875rem; } -/* Page Title with Actions (Labels toggle) */ -.page-title-with-actions { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - margin-bottom: 24px; -} - -.page-title-with-actions .page-title { - margin-bottom: 0; -} - -.page-title-with-actions .toggle-with-label { - display: flex; - align-items: center; - gap: 6px; - margin-top: 4px; -} - -.toggle-with-label .toggle-text { - font-size: 0.6875rem; - font-weight: 500; - color: var(--text-muted); -} - -.page-title-with-actions .toggle-switch button { - padding: 3px 8px; - font-size: 0.6875rem; -} - /* Filters - now uses .controls for consistency */ /* Stats Grid */ @@ -630,58 +599,6 @@ table tbody tr:hover { font-weight: 500; } -/* Period Display Banner */ -.period-display-banner { - display: flex; - align-items: center; - justify-content: center; - gap: 24px; - background: var(--bg); - padding: 20px 32px; - border-radius: 12px; - margin-bottom: 24px; -} - -.period-display-banner .period-box { - text-align: center; - min-width: 180px; -} - -.period-display-banner .period-box.prev { - color: var(--text-secondary); -} - -.period-display-banner .period-box.curr { - color: var(--text-primary); -} - -.period-display-banner .period-label { - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - margin-bottom: 4px; -} - -.period-display-banner .period-value { - font-size: 1.5rem; - font-weight: 600; - margin-bottom: 2px; -} - -.period-display-banner .period-dates { - font-size: 0.8125rem; - color: var(--text-muted); -} - -.period-display-banner .period-vs { - font-size: 0.875rem; - font-weight: 500; - color: var(--text-muted); - padding: 0 8px; -} - /* Comparison Metrics */ .comparison-grid { display: grid; @@ -1895,129 +1812,3 @@ table tbody tr:hover { width: 100%; height: 100%; } - -/* ======================================== - MOBILE UX ENHANCEMENTS - All styles below ONLY apply to mobile - ======================================== */ - -@media (max-width: 768px) { - /* Better touch targets (min 44px for accessibility) */ - .mobile-nav-item { - min-height: 44px; - min-width: 56px; - } - - .carousel-dot { - min-height: 32px; - } - - .toggle-switch button { - min-height: 28px; - } - - .control-group select, - .control-group input[type="date"] { - min-height: 44px; - } - - /* Smoother carousel transitions */ - .carousel-track { - transition: transform 350ms cubic-bezier(0.25, 0.46, 0.45, 0.94); - } - - /* Touch feedback */ - .carousel-dot:active, - .mobile-nav-item:active, - .stat-card:active { - transform: scale(0.96); - transition: transform 100ms ease; - } - - /* Bottom nav active indicator */ - .mobile-nav-item.active { - position: relative; - } - - .mobile-nav-item.active::after { - content: ''; - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - width: 20px; - height: 3px; - background: var(--primary); - border-radius: 0 0 3px 3px; - } - - /* Ensure chart title doesn't overlap with toggle */ - .charts-carousel .chart-card h2 { - padding-right: 85px; - } - - /* Period banner stacks on mobile */ - .period-display-banner { - flex-direction: column; - gap: 12px; - padding: 16px; - } - - .period-display-banner .period-box { - min-width: unset; - } - - .period-display-banner .period-value { - font-size: 1.25rem; - } - - /* Table scroll hint */ - .table-container { - position: relative; - } - - .table-container::after { - content: ''; - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 16px; - background: linear-gradient(to left, var(--surface), transparent); - pointer-events: none; - } -} - -/* Extra small screens */ -@media (max-width: 375px) { - .dashboard, - .comparison { - padding: 12px; - padding-bottom: 80px; - } - - .page-title h1 { - font-size: 1.125rem; - } - - .stats-carousel .stat-value { - font-size: 1.375rem; - } - - .charts-carousel .chart-container { - height: 200px; - } - - .carousel-dot .dot-label { - font-size: 0.5625rem; - } - - .mobile-nav-item { - font-size: 0.5625rem; - } - - .mobile-nav-item svg { - width: 20px; - height: 20px; - } -} diff --git a/src/App.js b/src/App.js index b58f0bb..589550a 100644 --- a/src/App.js +++ b/src/App.js @@ -4,7 +4,6 @@ import Dashboard from './components/Dashboard'; import Comparison from './components/Comparison'; import Slides from './components/Slides'; import { fetchData } from './services/dataService'; -import { useLanguage } from './contexts/LanguageContext'; import './App.css'; function NavLink({ to, children }) { @@ -18,7 +17,6 @@ function NavLink({ to, children }) { } function App() { - const { t, dir, switchLanguage } = useLanguage(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -26,9 +24,9 @@ function App() { const [dataSource, setDataSource] = useState('museums'); const dataSources = [ - { id: 'museums', labelKey: 'dataSources.museums', enabled: true }, - { id: 'coffees', labelKey: 'dataSources.coffees', enabled: false }, - { id: 'ecommerce', labelKey: 'dataSources.ecommerce', enabled: false } + { id: 'museums', label: 'Museums', enabled: true }, + { id: 'coffees', label: 'Coffees', enabled: false }, + { id: 'ecommerce', label: 'eCommerce', enabled: false } ]; useEffect(() => { @@ -50,26 +48,26 @@ function App() { if (loading) { return ( -
+
-

{t('app.loading')}

+

Loading data...

); } if (error) { return ( -
-

{t('app.error')}

+
+

Unable to load data

{error}

- +
); } return ( -
+
- } /> - } /> + } /> + } /> } /> @@ -140,7 +149,7 @@ function App() { - {t('nav.dashboard')} + Dashboard @@ -148,18 +157,24 @@ function App() { - {t('nav.compare')} + Compare + + + + + + + + Slides
diff --git a/src/components/ChartExport.js b/src/components/ChartExport.js index 4e984c0..29d12ad 100644 --- a/src/components/ChartExport.js +++ b/src/components/ChartExport.js @@ -1,8 +1,7 @@ import React, { useRef } from 'react'; -import JSZip from 'jszip'; // Wrapper component that adds PNG export to any chart -export function ExportableChart({ children, filename = 'chart', title = '', className = '', controls = null }) { +export function ExportableChart({ children, filename = 'chart', className = '' }) { const chartRef = useRef(null); const exportAsPNG = () => { @@ -12,30 +11,21 @@ export function ExportableChart({ children, filename = 'chart', title = '', clas const canvas = chartContainer.querySelector('canvas'); if (!canvas) return; - // Create a new canvas with white background and title + // Create a new canvas with white background const exportCanvas = document.createElement('canvas'); const ctx = exportCanvas.getContext('2d'); - // Set dimensions with padding and title space - const padding = 24; - const titleHeight = title ? 48 : 0; + // Set dimensions with padding + const padding = 20; exportCanvas.width = canvas.width + (padding * 2); - exportCanvas.height = canvas.height + (padding * 2) + titleHeight; + exportCanvas.height = canvas.height + (padding * 2); // Fill white background ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); - // Draw title if provided (left-aligned, matching on-screen style) - if (title) { - ctx.fillStyle = '#1e293b'; - ctx.font = '600 20px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(title, padding, padding + 24); - } - // Draw the chart - ctx.drawImage(canvas, padding, padding + titleHeight); + ctx.drawImage(canvas, padding, padding); // Export const link = document.createElement('a'); @@ -45,118 +35,23 @@ export function ExportableChart({ children, filename = 'chart', title = '', clas }; return ( -
- {title && ( -
-

{title}

-
- {controls} - -
-
- )} - {!title && controls &&
{controls}
} -
-
- {children} -
+
+ +
+ {children}
); } -// Utility function to export all charts from a container as a ZIP -export async function exportAllCharts(containerSelector, zipFilename = 'charts') { - const container = document.querySelector(containerSelector); - if (!container) return; - - const zip = new JSZip(); - const chartWrappers = container.querySelectorAll('.exportable-chart-wrapper'); - - for (let i = 0; i < chartWrappers.length; i++) { - const wrapper = chartWrappers[i]; - const canvas = wrapper.querySelector('canvas'); - const titleEl = wrapper.querySelector('.chart-header-with-export h2'); - const title = titleEl?.textContent || `chart-${i + 1}`; - - if (!canvas) continue; - - // Create export canvas with white background and title - const exportCanvas = document.createElement('canvas'); - const ctx = exportCanvas.getContext('2d'); - - const padding = 32; - const titleHeight = 56; - exportCanvas.width = canvas.width + (padding * 2); - exportCanvas.height = canvas.height + (padding * 2) + titleHeight; - - // White background - ctx.fillStyle = '#ffffff'; - ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); - - // Draw title - ctx.fillStyle = '#1e293b'; - ctx.font = '600 24px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(title, padding, padding + 28); - - // Draw chart - ctx.drawImage(canvas, padding, padding + titleHeight); - - // Convert to blob and add to zip - const dataUrl = exportCanvas.toDataURL('image/png', 1.0); - const base64Data = dataUrl.split(',')[1]; - const safeFilename = title.replace(/[^a-zA-Z0-9\u0600-\u06FF\s-]/g, '').replace(/\s+/g, '-'); - zip.file(`${String(i + 1).padStart(2, '0')}-${safeFilename}.png`, base64Data, { base64: true }); - } - - // Generate and download ZIP - const blob = await zip.generateAsync({ type: 'blob' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `${zipFilename}-${new Date().toISOString().split('T')[0]}.zip`; - link.click(); - URL.revokeObjectURL(url); -} - -// Button component for exporting all charts -export function ExportAllButton({ containerSelector, zipFilename, label, loadingLabel }) { - const [exporting, setExporting] = React.useState(false); - - const handleExport = async () => { - setExporting(true); - try { - await exportAllCharts(containerSelector, zipFilename); - } finally { - setExporting(false); - } - }; - - return ( - - ); -} - export default ExportableChart; diff --git a/src/components/Comparison.js b/src/components/Comparison.js index eea6a24..0018fba 100644 --- a/src/components/Comparison.js +++ b/src/components/Comparison.js @@ -4,7 +4,6 @@ import { Line, Bar } from 'react-chartjs-2'; import { EmptyState, FilterControls } from './shared'; import { ExportableChart } from './ChartExport'; import { chartColors, createBaseOptions } from '../config/chartConfig'; -import { useLanguage } from '../contexts/LanguageContext'; import { filterDataByDateRange, calculateMetrics, @@ -40,8 +39,7 @@ const generatePresetDates = (year) => ({ 'full': { start: `${year}-01-01`, end: `${year}-12-31` } }); -function Comparison({ data, showDataLabels, setShowDataLabels }) { - const { t } = useLanguage(); +function Comparison({ data, showDataLabels }) { const [searchParams, setSearchParams] = useSearchParams(); // Get available years from data @@ -142,8 +140,8 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) { }; const charts = [ - { id: 'timeseries', label: t('comparison.trend') }, - { id: 'museum', label: t('comparison.byMuseum') } + { id: 'timeseries', label: 'Trend' }, + { id: 'museum', label: 'By Museum' } ]; // Touch swipe handlers @@ -167,16 +165,15 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) { }; const granularityOptions = [ - { value: 'day', label: t('time.daily') }, - { value: 'week', label: t('time.weekly') }, - { value: 'month', label: t('time.monthly') } + { value: 'day', label: 'Daily' }, + { value: 'week', label: 'Weekly' } ]; const metricOptions = [ - { value: 'revenue', label: t('metrics.revenue'), field: 'revenue_incl_tax', format: 'currency' }, - { value: 'visitors', label: t('metrics.visitors'), field: 'visits', format: 'number' }, - { value: 'tickets', label: t('metrics.tickets'), field: 'tickets', format: 'number' }, - { value: 'avgRevenue', label: t('metrics.avgRevenue'), field: null, format: 'currency' } + { value: 'revenue', label: 'Revenue', field: 'revenue_incl_tax', format: 'currency' }, + { value: 'visitors', label: 'Visitors', field: 'visits', format: 'number' }, + { value: 'tickets', label: 'Tickets', field: 'tickets', format: 'number' }, + { value: 'avgRevenue', label: 'Avg Rev/Visitor', field: null, format: 'currency' } ]; const getMetricValue = useCallback((rows, metric) => { @@ -274,19 +271,19 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) { const captureRateChange = captureRates ? calcChange(captureRates.prev || 0, captureRates.curr || 0) : null; const cards = [ - { title: t('metrics.revenue'), prev: prevMetrics.revenue, curr: currMetrics.revenue, change: revenueChange, isCurrency: true }, - { title: t('metrics.visitors'), prev: prevMetrics.visitors, curr: currMetrics.visitors, change: visitorsChange }, - { title: t('metrics.tickets'), prev: prevMetrics.tickets, curr: currMetrics.tickets, change: ticketsChange }, - { title: t('metrics.avgRevenue'), prev: prevMetrics.avgRevPerVisitor, curr: currMetrics.avgRevPerVisitor, change: avgRevChange, isCurrency: true } + { title: 'Revenue', prev: prevMetrics.revenue, curr: currMetrics.revenue, change: revenueChange, isCurrency: true }, + { title: 'Visitors', prev: prevMetrics.visitors, curr: currMetrics.visitors, change: visitorsChange }, + { title: 'Tickets', prev: prevMetrics.tickets, curr: currMetrics.tickets, change: ticketsChange }, + { title: 'Avg Rev/Visitor', prev: prevMetrics.avgRevPerVisitor, curr: currMetrics.avgRevPerVisitor, change: avgRevChange, isCurrency: true } ]; if (pilgrimCounts) { - cards.push({ title: t('metrics.pilgrims'), prev: pilgrimCounts.prev, curr: pilgrimCounts.curr, change: pilgrimsChange, pendingMessage: t('comparison.pendingData') }); + cards.push({ title: 'Pilgrims', prev: pilgrimCounts.prev, curr: pilgrimCounts.curr, change: pilgrimsChange, pendingMessage: 'Data not published yet' }); } if (captureRates) { - cards.push({ title: t('metrics.captureRate'), prev: captureRates.prev, curr: captureRates.curr, change: captureRateChange, isPercent: true, pendingMessage: t('comparison.pendingData') }); + cards.push({ title: 'Capture Rate', prev: captureRates.prev, curr: captureRates.curr, change: captureRateChange, isPercent: true, pendingMessage: 'Data not published yet' }); } return cards; - }, [prevMetrics, currMetrics, pilgrimCounts, captureRates, t]); + }, [prevMetrics, currMetrics, pilgrimCounts, captureRates]); const handleCardTouchStart = (e) => { touchStartCard.current = e.touches[0].clientX; @@ -341,11 +338,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) { const daysDiff = Math.floor((rowDate - start) / (1000 * 60 * 60 * 24)); let key; - if (granularity === 'month') { - // Group by month number (relative to start) - const monthsDiff = (rowDate.getFullYear() - start.getFullYear()) * 12 + (rowDate.getMonth() - start.getMonth()); - key = monthsDiff + 1; - } else if (granularity === 'week') { + if (granularity === 'week') { key = Math.floor(daysDiff / 7) + 1; } else { key = daysDiff + 1; // day number from start @@ -366,15 +359,9 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) { const currGrouped = groupByPeriod(currData, ranges.curr.start, chartMetric, chartGranularity); const maxKey = Math.max(...Object.keys(prevGrouped).map(Number), ...Object.keys(currGrouped).map(Number), 1); - const labels = Array.from({ length: maxKey }, (_, i) => { - if (chartGranularity === 'month') { - const startDate = new Date(ranges.curr.start); - const monthNum = ((startDate.getMonth() + i) % 12) + 1; - return String(monthNum); - } - if (chartGranularity === 'week') return `W${i + 1}`; - return `D${i + 1}`; - }); + const labels = Array.from({ length: maxKey }, (_, i) => + chartGranularity === 'week' ? `W${i + 1}` : `D${i + 1}` + ); const prevLabel = getPeriodLabel(ranges.prev.start, ranges.prev.end); const currLabel = getPeriodLabel(ranges.curr.start, ranges.curr.end); @@ -436,54 +423,45 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) { ...baseOptions, plugins: { ...baseOptions.plugins, - legend: { position: 'top', align: 'end', labels: { boxWidth: 12, padding: 12, font: { size: 13 } } } + legend: { position: 'top', align: 'end', labels: { boxWidth: 12, padding: 12, font: { size: 11 } } } } }; return ( -
-
-
-

{t('comparison.title')}

-

{t('comparison.subtitle')}

-
-
- {t('nav.labels')} -
- - -
-
+
+
+

Period Comparison

+

Select a period and year — automatically compares with the same period in the previous year

- + - + {preset !== 'custom' && ( - + setStartDate(e.target.value)} /> - + setEndDate(e.target.value)} /> )} - + - + +
+
+
{getPeriodLabel(ranges.prev.start, ranges.prev.end)}
+
{formatDate(ranges.prev.start)} → {formatDate(ranges.prev.end)}
+
+
+
{getPeriodLabel(ranges.curr.start, ranges.curr.end)}
+
{formatDate(ranges.curr.start)} → {formatDate(ranges.curr.end)}
+
+
-
-
-
{t('comparison.previousPeriod')}
-
{getPeriodLabel(ranges.prev.start, ranges.prev.end)}
-
{formatDate(ranges.prev.start)} → {formatDate(ranges.prev.end)}
-
-
{t('comparison.vs')}
-
-
{t('comparison.currentPeriod')}
-
{getPeriodLabel(ranges.curr.start, ranges.curr.end)}
-
{formatDate(ranges.curr.start)} → {formatDate(ranges.curr.end)}
-
-
- {!hasData ? ( ) : ( <> {/* Desktop: Grid layout */} -
+
{metricCards.map((card, i) => ( {/* Desktop: Show both charts */} -
+
- m.value === chartMetric)?.label} - ${t('comparison.trend')}`} - className="chart-container" - controls={ - <> -
- {granularityOptions.map(opt => ( - - ))} -
-
- {metricOptions.map(opt => ( - - ))} -
- - } - > - -
-
-
- m.value === chartMetric)?.label} - ${t('comparison.byMuseum')}`} - className="chart-container" - controls={ +
+

{metricOptions.find(m => m.value === chartMetric)?.label} Trend

+
+
+ {granularityOptions.map(opt => ( + + ))} +
{metricOptions.map(opt => (
- } - > +
+
+ + + +
+
+
+

{metricOptions.find(m => m.value === chartMetric)?.label} by Museum

+
+
+ {metricOptions.map(opt => ( + + ))} +
+
+
+
@@ -674,7 +644,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels }) {
-

{metricOptions.find(m => m.value === chartMetric)?.label} - {t('comparison.trend')}

+

{metricOptions.find(m => m.value === chartMetric)?.label} Trend

{granularityOptions.map(opt => ( - -
-
+
+
+

Dashboard

+

Real-time museum analytics from Google Sheets

- + - + - + - + - + {/* Desktop: Grid */} -
- - - - +
+ + + +
{/* Mobile: Stats Carousel */} @@ -392,28 +381,28 @@ function Dashboard({ data, showDataLabels, setShowDataLabels }) { {!hasData ? ( ) : ( <> -
-

{t('dashboard.quarterlyComparison')}

+
+

Quarterly Comparison: 2024 vs 2025

- - - - - - - - - + + + + + + + + + @@ -440,58 +429,58 @@ function Dashboard({ data, showDataLabels, setShowDataLabels }) { {/* Desktop: Charts Grid */} -
+
- - - -
- } - > +

Revenue Trends

+
+ + +
+
{filters.museum === 'all' && (
- - +

Visitors by Museum

+ +
)} {filters.museum === 'all' && (
- +

Revenue by Museum

+
)}
- - +

Quarterly Revenue (YoY)

+ +
- +

District Performance

+
- +

Capture Rate vs Umrah Pilgrims

+ v.toFixed(1) + '%' }, + ticks: { font: { size: 10 }, color: '#94a3b8', callback: (v) => v.toFixed(1) + '%' }, border: { display: false }, - title: { display: true, text: 'Capture Rate (%)', font: { size: 12 }, color: chartColors.secondary } + title: { display: true, text: 'Capture Rate (%)', font: { size: 10 }, color: chartColors.secondary } }, y1: { type: 'linear', position: 'right', grid: { drawOnChartArea: false }, - ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v) => (v / 1000000).toFixed(0) + 'M' }, + ticks: { font: { size: 10 }, color: '#94a3b8', callback: (v) => (v / 1000000).toFixed(0) + 'M' }, border: { display: false }, - title: { display: true, text: 'Pilgrims', font: { size: 12 }, color: chartColors.tertiary } + title: { display: true, text: 'Pilgrims', font: { size: 10 }, color: chartColors.tertiary } } } }} /> @@ -538,10 +527,10 @@ function Dashboard({ data, showDataLabels, setShowDataLabels }) { >
-

{t('dashboard.revenueTrends')}

+

Revenue Trends

- - + +
@@ -552,9 +541,9 @@ function Dashboard({ data, showDataLabels, setShowDataLabels }) { {filters.museum === 'all' && (
-

{t('dashboard.visitorsByMuseum')}

+

Visitors by Museum

- +
@@ -563,7 +552,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels }) { {filters.museum === 'all' && (
-

{t('dashboard.revenueByMuseum')}

+

Revenue by Museum

@@ -573,16 +562,16 @@ function Dashboard({ data, showDataLabels, setShowDataLabels }) {
-

{t('dashboard.quarterlyRevenue')}

+

Quarterly Revenue (YoY)

- +
-

{t('dashboard.districtPerformance')}

+

District Performance

@@ -591,13 +580,13 @@ function Dashboard({ data, showDataLabels, setShowDataLabels }) {
-

{t('dashboard.captureRateChart')}

+

Capture Rate vs Umrah Pilgrims

v.toFixed(1) + '%' }, + ticks: { font: { size: 9 }, color: '#94a3b8', callback: (v) => v.toFixed(1) + '%' }, border: { display: false } }, y1: { type: 'linear', position: 'right', grid: { drawOnChartArea: false }, - ticks: { font: { size: 13 }, color: '#94a3b8', callback: (v) => (v / 1000000).toFixed(0) + 'M' }, + ticks: { font: { size: 9 }, color: '#94a3b8', callback: (v) => (v / 1000000).toFixed(0) + 'M' }, border: { display: false } } } diff --git a/src/components/Slides.js b/src/components/Slides.js index e682c2d..407a250 100644 --- a/src/components/Slides.js +++ b/src/components/Slides.js @@ -1,7 +1,6 @@ import React, { useState, useMemo, useCallback } from 'react'; import { Line, Bar } from 'react-chartjs-2'; import { chartColors, createBaseOptions } from '../config/chartConfig'; -import { useLanguage } from '../contexts/LanguageContext'; import { filterDataByDateRange, calculateMetrics, @@ -13,21 +12,20 @@ import { } from '../services/dataService'; import JSZip from 'jszip'; -function Slides({ data }) { - const { t } = useLanguage(); - - const CHART_TYPES = useMemo(() => [ - { id: 'trend', label: t('slides.revenueTrend'), icon: '📈' }, - { id: 'museum-bar', label: t('slides.byMuseum'), icon: '📊' }, - { id: 'kpi-cards', label: t('slides.kpiSummary'), icon: '🎯' }, - { id: 'comparison', label: t('slides.yoyComparison'), icon: '⚖️' } - ], [t]); +const CHART_TYPES = [ + { id: 'trend', label: 'Revenue Trend', icon: '📈' }, + { id: 'museum-bar', label: 'By Museum', icon: '📊' }, + { id: 'kpi-cards', label: 'KPI Summary', icon: '🎯' }, + { id: 'comparison', label: 'YoY Comparison', icon: '⚖️' } +]; - const METRICS = useMemo(() => [ - { id: 'revenue', label: t('metrics.revenue'), field: 'revenue_incl_tax' }, - { id: 'visitors', label: t('metrics.visitors'), field: 'visits' }, - { id: 'tickets', label: t('metrics.tickets'), field: 'tickets' } - ], [t]); +const METRICS = [ + { id: 'revenue', label: 'Revenue', field: 'revenue_incl_tax' }, + { id: 'visitors', label: 'Visitors', field: 'visits' }, + { id: 'tickets', label: 'Tickets', field: 'tickets' } +]; + +function Slides({ data }) { const [slides, setSlides] = useState([]); const [editingSlide, setEditingSlide] = useState(null); const [previewMode, setPreviewMode] = useState(false); @@ -173,7 +171,6 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)} currentSlide={currentPreviewSlide} setCurrentSlide={setCurrentPreviewSlide} onExit={() => setPreviewMode(false)} - metrics={METRICS} /> ); } @@ -181,8 +178,8 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)} return (
-

{t('slides.title')}

-

{t('slides.subtitle')}

+

Presentation Builder

+

Create slides with charts and export as HTML or PDF

@@ -190,7 +187,7 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)} - {t('slides.addSlide')} + Add Slide {slides.length > 0 && ( <> @@ -198,13 +195,13 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)} - {t('slides.preview')} + Preview )} @@ -212,11 +209,11 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
-

{t('slides.slidesCount')} ({slides.length})

+

Slides ({slides.length})

{slides.length === 0 ? (
-

{t('slides.noSlides')}

- +

No slides yet

+
) : (
@@ -248,8 +245,6 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)} districts={districts} districtMuseumMap={districtMuseumMap} data={data} - chartTypes={CHART_TYPES} - metrics={METRICS} /> )}
@@ -257,8 +252,7 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)} ); } -function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, chartTypes, metrics }) { - const { t } = useLanguage(); +function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data }) { const availableMuseums = useMemo(() => getMuseumsForDistrict(districtMuseumMap, slide.district), [districtMuseumMap, slide.district] @@ -267,19 +261,19 @@ function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, char return (
- + onUpdate({ title: e.target.value })} - placeholder={t('slides.slideTitle')} + placeholder="Enter slide title" />
- +
- {chartTypes.map(type => ( + {CHART_TYPES.map(type => (
); } -// Static field mapping for charts (Chart.js labels don't need i18n) -const METRIC_FIELDS = { - revenue: { field: 'revenue_incl_tax', label: 'Revenue' }, - visitors: { field: 'visits', label: 'Visitors' }, - tickets: { field: 'tickets', label: 'Tickets' } -}; - -function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) { - const { t } = useLanguage(); +function SlidePreview({ slide, data, districts, districtMuseumMap }) { const filteredData = useMemo(() => filterDataByDateRange(data, slide.startDate, slide.endDate, { district: slide.district, @@ -365,7 +351,7 @@ function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) { [data, slide.startDate, slide.endDate, slide.district, slide.museum] ); - const metricsData = useMemo(() => calculateMetrics(filteredData), [filteredData]); + const metrics = useMemo(() => calculateMetrics(filteredData), [filteredData]); const baseOptions = useMemo(() => createBaseOptions(false), []); const getMetricValue = useCallback((rows, metric) => { @@ -383,11 +369,10 @@ function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) { }); const sortedDates = Object.keys(grouped).sort(); - const metricLabel = metrics?.find(m => m.id === slide.metric)?.label || METRIC_FIELDS[slide.metric]?.label || slide.metric; return { labels: sortedDates.map(d => d.substring(5)), datasets: [{ - label: metricLabel, + label: METRICS.find(m => m.id === slide.metric)?.label, data: sortedDates.map(d => getMetricValue(grouped[d], slide.metric)), borderColor: chartColors.primary, backgroundColor: chartColors.primary + '20', @@ -395,7 +380,7 @@ function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) { tension: 0.4 }] }; - }, [filteredData, slide.metric, getMetricValue, metrics]); + }, [filteredData, slide.metric, getMetricValue]); const museumData = useMemo(() => { const byMuseum = {}; @@ -406,32 +391,31 @@ function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) { }); const museums = Object.keys(byMuseum).sort(); - const metricLabel = metrics?.find(m => m.id === slide.metric)?.label || METRIC_FIELDS[slide.metric]?.label || slide.metric; return { labels: museums, datasets: [{ - label: metricLabel, + label: METRICS.find(m => m.id === slide.metric)?.label, data: museums.map(m => getMetricValue(byMuseum[m], slide.metric)), backgroundColor: chartColors.primary, borderRadius: 6 }] }; - }, [filteredData, slide.metric, getMetricValue, metrics]); + }, [filteredData, slide.metric, getMetricValue]); if (slide.chartType === 'kpi-cards') { return (
-
{formatCompactCurrency(metricsData.revenue)}
-
{t('metrics.revenue')}
+
{formatCompactCurrency(metrics.revenue)}
+
Revenue
-
{formatCompact(metricsData.visitors)}
-
{t('metrics.visitors')}
+
{formatCompact(metrics.visitors)}
+
Visitors
-
{formatCompact(metricsData.tickets)}
-
{t('metrics.tickets')}
+
{formatCompact(metrics.tickets)}
+
Tickets
); @@ -452,8 +436,7 @@ function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) { ); } -function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide, setCurrentSlide, onExit, metrics }) { - const { t } = useLanguage(); +function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide, setCurrentSlide, onExit }) { const handleKeyDown = useCallback((e) => { if (e.key === 'ArrowRight' || e.key === ' ') { setCurrentSlide(prev => Math.min(prev + 1, slides.length - 1)); @@ -476,7 +459,7 @@ function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide,

{slide?.title}

- {slide && } + {slide && }
{currentSlide + 1} / {slides.length} @@ -485,7 +468,7 @@ function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide,
- +
); diff --git a/src/components/shared/Carousel.jsx b/src/components/shared/Carousel.jsx index 1c2d231..7d6637c 100644 --- a/src/components/shared/Carousel.jsx +++ b/src/components/shared/Carousel.jsx @@ -1,4 +1,4 @@ -import React, { useRef, useCallback, useState } from 'react'; +import React, { useRef, useCallback } from 'react'; function Carousel({ children, @@ -8,57 +8,24 @@ function Carousel({ showLabels = true, className = '' }) { - const touchStartX = useRef(null); - const touchStartY = useRef(null); - const trackRef = useRef(null); - const [dragOffset, setDragOffset] = useState(0); - const [isDragging, setIsDragging] = useState(false); + const touchStart = useRef(null); const itemCount = React.Children.count(children); - - const SWIPE_THRESHOLD = 50; const handleTouchStart = useCallback((e) => { - touchStartX.current = e.touches[0].clientX; - touchStartY.current = e.touches[0].clientY; - setIsDragging(true); + touchStart.current = e.touches[0].clientX; }, []); - const handleTouchMove = useCallback((e) => { - if (!touchStartX.current || !isDragging) return; - - const currentX = e.touches[0].clientX; - const currentY = e.touches[0].clientY; - const diffX = currentX - touchStartX.current; - const diffY = Math.abs(currentY - touchStartY.current); - - // Only drag horizontally if not scrolling vertically - if (Math.abs(diffX) > diffY) { - // Add resistance at edges - let offset = diffX; - if ((activeIndex === 0 && diffX > 0) || (activeIndex === itemCount - 1 && diffX < 0)) { - offset = diffX * 0.25; - } - setDragOffset(offset); - } - }, [isDragging, activeIndex, itemCount]); - const handleTouchEnd = useCallback((e) => { - if (!touchStartX.current) return; - - const diff = touchStartX.current - e.changedTouches[0].clientX; - - if (Math.abs(diff) > SWIPE_THRESHOLD) { + if (!touchStart.current) return; + const diff = touchStart.current - e.changedTouches[0].clientX; + if (Math.abs(diff) > 50) { if (diff > 0 && activeIndex < itemCount - 1) { setActiveIndex(activeIndex + 1); } else if (diff < 0 && activeIndex > 0) { setActiveIndex(activeIndex - 1); } } - - touchStartX.current = null; - touchStartY.current = null; - setDragOffset(0); - setIsDragging(false); + touchStart.current = null; }, [activeIndex, setActiveIndex, itemCount]); const handleKeyDown = useCallback((e) => { @@ -69,41 +36,18 @@ function Carousel({ } }, [activeIndex, setActiveIndex, itemCount]); - // Calculate transform with drag offset - const baseTransform = -(activeIndex * 100); - const dragPercent = trackRef.current - ? (dragOffset / trackRef.current.offsetWidth) * 100 - : 0; - const transform = baseTransform + dragPercent; - return ( -
+
{React.Children.map(children, (child, i) => ( -
+
{child}
))} diff --git a/src/config/chartConfig.js b/src/config/chartConfig.js index 5cebf8f..1473a4f 100644 --- a/src/config/chartConfig.js +++ b/src/config/chartConfig.js @@ -41,7 +41,7 @@ export const chartColors = { export const createDataLabelConfig = (showDataLabels) => ({ display: showDataLabels, color: '#1e293b', - font: { size: 11, weight: 600 }, + font: { size: 10, weight: 600 }, anchor: 'end', align: 'end', offset: 4, @@ -74,19 +74,19 @@ export const createBaseOptions = (showDataLabels) => ({ backgroundColor: '#1e293b', padding: 12, cornerRadius: 8, - titleFont: { size: 14 }, - bodyFont: { size: 13 } + titleFont: { size: 12 }, + bodyFont: { size: 11 } }, datalabels: createDataLabelConfig(showDataLabels) }, scales: { x: { grid: { display: false }, - ticks: { font: { size: 12 }, color: '#94a3b8' } + ticks: { font: { size: 10 }, color: '#94a3b8' } }, y: { grid: { color: chartColors.grid }, - ticks: { font: { size: 12 }, color: '#94a3b8' }, + ticks: { font: { size: 10 }, color: '#94a3b8' }, border: { display: false } } } diff --git a/src/index.js b/src/index.js index 48dc746..593edf1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,10 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { LanguageProvider } from './contexts/LanguageContext'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - - - + );
{t('table.quarter')}{t('table.rev2024')}{t('table.rev2025')}{t('table.change')}{t('table.visitors2024')}{t('table.visitors2025')}{t('table.change')}{t('table.capture2024')}{t('table.capture2025')}QuarterRev 2024Rev 2025ChangeVisitors 2024Visitors 2025ChangeCapture 2024Capture 2025