Restore translations + fix CSS for new components

- Restored Arabic/English translations from overhaul
- Restored LanguageContext for language switching
- Restored updated components with i18n support
- Kept original CSS from f17e19f (working desktop styling)
- Added missing CSS for new components:
  - .page-title-with-actions
  - .toggle-with-label
  - .period-display-banner

Desktop and Arabic translations both working.
This commit is contained in:
fahed
2026-02-03 15:32:52 +03:00
parent b2fcb16d12
commit eb477158cb
11 changed files with 700 additions and 338 deletions

View File

@@ -1,6 +1,7 @@
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,
@@ -12,20 +13,21 @@ import {
} from '../services/dataService';
import JSZip from 'jszip';
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 = [
{ id: 'revenue', label: 'Revenue', field: 'revenue_incl_tax' },
{ id: 'visitors', label: 'Visitors', field: 'visits' },
{ id: 'tickets', label: 'Tickets', field: 'tickets' }
];
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 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 [slides, setSlides] = useState([]);
const [editingSlide, setEditingSlide] = useState(null);
const [previewMode, setPreviewMode] = useState(false);
@@ -171,6 +173,7 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
currentSlide={currentPreviewSlide}
setCurrentSlide={setCurrentPreviewSlide}
onExit={() => setPreviewMode(false)}
metrics={METRICS}
/>
);
}
@@ -178,8 +181,8 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
return (
<div className="slides-builder">
<div className="page-title">
<h1>Presentation Builder</h1>
<p>Create slides with charts and export as HTML or PDF</p>
<h1>{t('slides.title')}</h1>
<p>{t('slides.subtitle')}</p>
</div>
<div className="slides-toolbar">
@@ -187,7 +190,7 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Add Slide
{t('slides.addSlide')}
</button>
{slides.length > 0 && (
<>
@@ -195,13 +198,13 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>
</svg>
Preview
{t('slides.preview')}
</button>
<button className="btn-secondary" onClick={exportAsHTML}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Export HTML
{t('slides.exportHtml')}
</button>
</>
)}
@@ -209,11 +212,11 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
<div className="slides-workspace">
<div className="slides-list">
<h3>Slides ({slides.length})</h3>
<h3>{t('slides.slidesCount')} ({slides.length})</h3>
{slides.length === 0 ? (
<div className="empty-slides">
<p>No slides yet</p>
<button onClick={addSlide}>Add your first slide</button>
<p>{t('slides.noSlides')}</p>
<button onClick={addSlide}>{t('slides.addFirst')}</button>
</div>
) : (
<div className="slides-thumbnails">
@@ -245,6 +248,8 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
districts={districts}
districtMuseumMap={districtMuseumMap}
data={data}
chartTypes={CHART_TYPES}
metrics={METRICS}
/>
)}
</div>
@@ -252,7 +257,8 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
);
}
function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data }) {
function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, chartTypes, metrics }) {
const { t } = useLanguage();
const availableMuseums = useMemo(() =>
getMuseumsForDistrict(districtMuseumMap, slide.district),
[districtMuseumMap, slide.district]
@@ -261,19 +267,19 @@ function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data }) {
return (
<div className="slide-editor">
<div className="editor-section">
<label>Slide Title</label>
<label>{t('slides.slideTitle')}</label>
<input
type="text"
value={slide.title}
onChange={e => onUpdate({ title: e.target.value })}
placeholder="Enter slide title"
placeholder={t('slides.slideTitle')}
/>
</div>
<div className="editor-section">
<label>Chart Type</label>
<label>{t('slides.chartType')}</label>
<div className="chart-type-grid">
{CHART_TYPES.map(type => (
{chartTypes.map(type => (
<button
key={type.id}
className={`chart-type-btn ${slide.chartType === type.id ? 'active' : ''}`}
@@ -287,35 +293,35 @@ function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data }) {
</div>
<div className="editor-section">
<label>Metric</label>
<label>{t('slides.metric')}</label>
<select value={slide.metric} onChange={e => onUpdate({ metric: e.target.value })}>
{METRICS.map(m => <option key={m.id} value={m.id}>{m.label}</option>)}
{metrics.map(m => <option key={m.id} value={m.id}>{m.label}</option>)}
</select>
</div>
<div className="editor-row">
<div className="editor-section">
<label>Start Date</label>
<label>{t('slides.startDate')}</label>
<input type="date" value={slide.startDate} onChange={e => onUpdate({ startDate: e.target.value })} />
</div>
<div className="editor-section">
<label>End Date</label>
<label>{t('slides.endDate')}</label>
<input type="date" value={slide.endDate} onChange={e => onUpdate({ endDate: e.target.value })} />
</div>
</div>
<div className="editor-row">
<div className="editor-section">
<label>District</label>
<label>{t('filters.district')}</label>
<select value={slide.district} onChange={e => onUpdate({ district: e.target.value, museum: 'all' })}>
<option value="all">All Districts</option>
<option value="all">{t('filters.allDistricts')}</option>
{districts.map(d => <option key={d} value={d}>{d}</option>)}
</select>
</div>
<div className="editor-section">
<label>Museum</label>
<label>{t('filters.museum')}</label>
<select value={slide.museum} onChange={e => onUpdate({ museum: e.target.value })}>
<option value="all">All Museums</option>
<option value="all">{t('filters.allMuseums')}</option>
{availableMuseums.map(m => <option key={m} value={m}>{m}</option>)}
</select>
</div>
@@ -329,20 +335,28 @@ function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data }) {
checked={slide.showComparison}
onChange={e => onUpdate({ showComparison: e.target.checked })}
/>
Show Year-over-Year Comparison
{t('slides.showYoY')}
</label>
</div>
)}
<div className="slide-preview-box">
<h4>Preview</h4>
<SlidePreview slide={slide} data={data} districts={districts} districtMuseumMap={districtMuseumMap} />
<h4>{t('slides.preview')}</h4>
<SlidePreview slide={slide} data={data} districts={districts} districtMuseumMap={districtMuseumMap} metrics={metrics} />
</div>
</div>
);
}
function SlidePreview({ slide, data, districts, districtMuseumMap }) {
// 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();
const filteredData = useMemo(() =>
filterDataByDateRange(data, slide.startDate, slide.endDate, {
district: slide.district,
@@ -351,7 +365,7 @@ function SlidePreview({ slide, data, districts, districtMuseumMap }) {
[data, slide.startDate, slide.endDate, slide.district, slide.museum]
);
const metrics = useMemo(() => calculateMetrics(filteredData), [filteredData]);
const metricsData = useMemo(() => calculateMetrics(filteredData), [filteredData]);
const baseOptions = useMemo(() => createBaseOptions(false), []);
const getMetricValue = useCallback((rows, metric) => {
@@ -369,10 +383,11 @@ function SlidePreview({ slide, data, districts, districtMuseumMap }) {
});
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: METRICS.find(m => m.id === slide.metric)?.label,
label: metricLabel,
data: sortedDates.map(d => getMetricValue(grouped[d], slide.metric)),
borderColor: chartColors.primary,
backgroundColor: chartColors.primary + '20',
@@ -380,7 +395,7 @@ function SlidePreview({ slide, data, districts, districtMuseumMap }) {
tension: 0.4
}]
};
}, [filteredData, slide.metric, getMetricValue]);
}, [filteredData, slide.metric, getMetricValue, metrics]);
const museumData = useMemo(() => {
const byMuseum = {};
@@ -391,31 +406,32 @@ function SlidePreview({ slide, data, districts, districtMuseumMap }) {
});
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: METRICS.find(m => m.id === slide.metric)?.label,
label: metricLabel,
data: museums.map(m => getMetricValue(byMuseum[m], slide.metric)),
backgroundColor: chartColors.primary,
borderRadius: 6
}]
};
}, [filteredData, slide.metric, getMetricValue]);
}, [filteredData, slide.metric, getMetricValue, metrics]);
if (slide.chartType === 'kpi-cards') {
return (
<div className="preview-kpis">
<div className="preview-kpi">
<div className="kpi-value">{formatCompactCurrency(metrics.revenue)}</div>
<div className="kpi-label">Revenue</div>
<div className="kpi-value">{formatCompactCurrency(metricsData.revenue)}</div>
<div className="kpi-label">{t('metrics.revenue')}</div>
</div>
<div className="preview-kpi">
<div className="kpi-value">{formatCompact(metrics.visitors)}</div>
<div className="kpi-label">Visitors</div>
<div className="kpi-value">{formatCompact(metricsData.visitors)}</div>
<div className="kpi-label">{t('metrics.visitors')}</div>
</div>
<div className="preview-kpi">
<div className="kpi-value">{formatCompact(metrics.tickets)}</div>
<div className="kpi-label">Tickets</div>
<div className="kpi-value">{formatCompact(metricsData.tickets)}</div>
<div className="kpi-label">{t('metrics.tickets')}</div>
</div>
</div>
);
@@ -436,7 +452,8 @@ function SlidePreview({ slide, data, districts, districtMuseumMap }) {
);
}
function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide, setCurrentSlide, onExit }) {
function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide, setCurrentSlide, onExit, metrics }) {
const { t } = useLanguage();
const handleKeyDown = useCallback((e) => {
if (e.key === 'ArrowRight' || e.key === ' ') {
setCurrentSlide(prev => Math.min(prev + 1, slides.length - 1));
@@ -459,7 +476,7 @@ function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide,
<div className="preview-slide">
<h1 className="preview-title">{slide?.title}</h1>
<div className="preview-content">
{slide && <SlidePreview slide={slide} data={data} districts={districts} districtMuseumMap={districtMuseumMap} />}
{slide && <SlidePreview slide={slide} data={data} districts={districts} districtMuseumMap={districtMuseumMap} metrics={metrics} />}
</div>
<div className="preview-footer">
<span>{currentSlide + 1} / {slides.length}</span>
@@ -468,7 +485,7 @@ function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide,
<div className="preview-controls">
<button onClick={() => setCurrentSlide(prev => Math.max(prev - 1, 0))} disabled={currentSlide === 0}></button>
<button onClick={() => setCurrentSlide(prev => Math.min(prev + 1, slides.length - 1))} disabled={currentSlide === slides.length - 1}></button>
<button onClick={onExit}>Exit</button>
<button onClick={onExit}>{t('slides.exit')}</button>
</div>
</div>
);