diff --git a/package-lock.json b/package-lock.json
index 33f59c2..98edffb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@testing-library/user-event": "^13.5.0",
"chart.js": "^4.5.1",
"chartjs-plugin-datalabels": "^2.2.0",
+ "jszip": "^3.10.1",
"react": "^19.2.4",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.4",
@@ -9078,6 +9079,12 @@
"node": ">= 4"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
@@ -10933,6 +10940,54 @@
"node": ">=4.0"
}
},
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/jszip/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/jszip/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/jszip/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/jszip/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -11019,6 +11074,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@@ -11835,6 +11899,12 @@
"node": ">=6"
}
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -14731,6 +14801,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "license": "MIT"
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
diff --git a/package.json b/package.json
index eda1806..af216cb 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"@testing-library/user-event": "^13.5.0",
"chart.js": "^4.5.1",
"chartjs-plugin-datalabels": "^2.2.0",
+ "jszip": "^3.10.1",
"react": "^19.2.4",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.4",
diff --git a/public/hihala-logo-horizontal.svg b/public/hihala-logo-horizontal.svg
new file mode 100755
index 0000000..0f98da6
--- /dev/null
+++ b/public/hihala-logo-horizontal.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/public/hihala-logo-vertical.svg b/public/hihala-logo-vertical.svg
new file mode 100755
index 0000000..b2dbc2c
--- /dev/null
+++ b/public/hihala-logo-vertical.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 599bc04..af9c9e9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -15,6 +15,9 @@
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
+
+
+
-
HiHala Museums Dashboard
+ HiHala Data – Museums
diff --git a/public/manifest.json b/public/manifest.json
index 080d6c7..62c515d 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -1,6 +1,6 @@
{
- "short_name": "React App",
- "name": "Create React App Sample",
+ "short_name": "HiHala Data",
+ "name": "HiHala Data – Museums",
"icons": [
{
"src": "favicon.ico",
diff --git a/src/App.css b/src/App.css
index 1b027ad..0b5eb21 100644
--- a/src/App.css
+++ b/src/App.css
@@ -152,29 +152,74 @@ body {
height: 64px;
display: flex;
align-items: center;
- justify-content: space-between;
+ justify-content: center;
position: sticky;
top: 0;
z-index: 100;
}
+.nav-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ max-width: 1400px;
+}
+
.nav-brand {
display: flex;
align-items: center;
- gap: 8px;
- font-size: 1rem;
+ gap: 10px;
+}
+
+.nav-brand-icon {
+ color: #3b82f6;
+}
+
+.nav-brand-text {
+ font-family: 'DM Sans', 'Inter', -apple-system, sans-serif;
+ font-size: 1.125rem;
font-weight: 600;
- color: var(--text-secondary);
+ color: #1e3a5f;
letter-spacing: -0.02em;
}
-.nav-logo {
- height: 24px;
- width: auto;
+.data-source-select {
+ appearance: none;
+ -webkit-appearance: none;
+ background: transparent;
+ border: none;
+ color: #3b82f6;
+ font-family: inherit;
+ font-size: inherit;
+ font-weight: 500;
+ cursor: pointer;
+ padding: 2px 20px 2px 6px;
+ margin-left: 4px;
+ border-radius: 6px;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%233b82f6' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 4px center;
+ transition: background-color 0.15s ease;
}
-.nav-brand span {
- color: var(--text-secondary);
+.data-source-select:hover {
+ background-color: rgba(59, 130, 246, 0.08);
+}
+
+.data-source-select:focus {
+ outline: none;
+ background-color: rgba(59, 130, 246, 0.12);
+}
+
+.data-source-select option {
+ color: #1e3a5f;
+ background: white;
+ font-weight: 500;
+}
+
+.data-source-select option:disabled {
+ color: #94a3b8;
}
.nav-links {
@@ -1015,15 +1060,25 @@ table tbody tr:hover {
.nav-bar {
padding: 12px 16px;
+ height: auto;
+ }
+
+ .nav-content {
justify-content: center;
}
- .nav-brand {
- font-size: 0.9rem;
+ .nav-brand-text {
+ font-size: 1rem;
}
- .nav-logo {
- height: 20px;
+ .nav-brand-icon {
+ width: 18px;
+ height: 18px;
+ }
+
+ .data-source-select {
+ font-size: 0.9rem;
+ padding: 2px 18px 2px 4px;
}
/* Mobile Bottom Navigation */
@@ -1310,3 +1365,412 @@ table tbody tr:hover {
gap: 4px;
}
}
+
+/* ========== Slides Builder ========== */
+.slides-builder {
+ padding: 24px;
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+.slides-toolbar {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 24px;
+ flex-wrap: wrap;
+}
+
+.btn-primary, .btn-secondary {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 18px;
+ border-radius: 8px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ border: none;
+}
+
+.btn-primary {
+ background: var(--accent);
+ color: white;
+}
+
+.btn-primary:hover {
+ background: #2563eb;
+}
+
+.btn-secondary {
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ border: 1px solid var(--border);
+}
+
+.btn-secondary:hover {
+ background: var(--bg-tertiary);
+}
+
+.slides-workspace {
+ display: grid;
+ grid-template-columns: 280px 1fr;
+ gap: 24px;
+ min-height: 600px;
+}
+
+.slides-list {
+ background: var(--bg-secondary);
+ border-radius: 12px;
+ padding: 16px;
+ border: 1px solid var(--border);
+}
+
+.slides-list h3 {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-bottom: 16px;
+ font-weight: 500;
+}
+
+.empty-slides {
+ text-align: center;
+ padding: 40px 20px;
+ color: var(--text-secondary);
+}
+
+.empty-slides button {
+ margin-top: 16px;
+ padding: 8px 16px;
+ background: var(--accent);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.slides-thumbnails {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.slide-thumbnail {
+ display: grid;
+ grid-template-columns: 32px 32px 1fr auto;
+ align-items: center;
+ gap: 8px;
+ padding: 12px;
+ background: var(--bg-primary);
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ border: 2px solid transparent;
+}
+
+.slide-thumbnail:hover {
+ border-color: var(--border);
+}
+
+.slide-thumbnail.active {
+ border-color: var(--accent);
+ background: rgba(59, 130, 246, 0.05);
+}
+
+.slide-thumbnail .slide-number {
+ width: 24px;
+ height: 24px;
+ background: var(--bg-tertiary);
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+.slide-thumbnail .slide-icon {
+ font-size: 18px;
+}
+
+.slide-thumbnail .slide-title-preview {
+ font-size: 13px;
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.slide-thumbnail .slide-actions {
+ display: flex;
+ gap: 4px;
+ opacity: 0;
+ transition: opacity 0.15s ease;
+}
+
+.slide-thumbnail:hover .slide-actions {
+ opacity: 1;
+}
+
+.slide-actions button {
+ width: 24px;
+ height: 24px;
+ border: none;
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.slide-actions button:hover {
+ background: var(--border);
+}
+
+.slide-actions button.delete:hover {
+ background: #fee2e2;
+ color: #dc2626;
+}
+
+/* Slide Editor */
+.slide-editor {
+ background: var(--bg-secondary);
+ border-radius: 12px;
+ padding: 24px;
+ border: 1px solid var(--border);
+}
+
+.editor-section {
+ margin-bottom: 20px;
+}
+
+.editor-section label {
+ display: block;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+}
+
+.editor-section input[type="text"],
+.editor-section input[type="date"],
+.editor-section select {
+ width: 100%;
+ padding: 10px 12px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ font-size: 14px;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+}
+
+.editor-section input:focus,
+.editor-section select:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.editor-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+}
+
+.chart-type-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 8px;
+}
+
+.chart-type-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ padding: 16px 12px;
+ border: 2px solid var(--border);
+ border-radius: 10px;
+ background: var(--bg-primary);
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.chart-type-btn:hover {
+ border-color: var(--accent);
+}
+
+.chart-type-btn.active {
+ border-color: var(--accent);
+ background: rgba(59, 130, 246, 0.05);
+}
+
+.chart-type-btn .chart-icon {
+ font-size: 24px;
+}
+
+.chart-type-btn span:last-child {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text-secondary);
+}
+
+.slide-preview-box {
+ margin-top: 24px;
+ padding-top: 24px;
+ border-top: 1px solid var(--border);
+}
+
+.slide-preview-box h4 {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--text-secondary);
+ margin-bottom: 16px;
+}
+
+.preview-chart {
+ background: var(--bg-primary);
+ border-radius: 8px;
+ padding: 16px;
+ height: 200px;
+}
+
+.preview-kpis {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 12px;
+}
+
+.preview-kpi {
+ background: var(--bg-primary);
+ border-radius: 8px;
+ padding: 16px;
+ text-align: center;
+}
+
+.preview-kpi .kpi-value {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--accent);
+}
+
+.preview-kpi .kpi-label {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-top: 4px;
+}
+
+/* Preview Fullscreen */
+.preview-fullscreen {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+}
+
+.preview-slide {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 60px;
+}
+
+.preview-title {
+ color: #f8fafc;
+ font-size: 2.5rem;
+ font-weight: 600;
+ margin-bottom: 40px;
+ text-align: center;
+}
+
+.preview-content {
+ width: 100%;
+ max-width: 900px;
+}
+
+.preview-content .preview-chart {
+ height: 350px;
+ background: rgba(255, 255, 255, 0.03);
+ border-radius: 16px;
+ padding: 30px;
+}
+
+.preview-content .preview-kpis .preview-kpi {
+ background: rgba(255, 255, 255, 0.05);
+ padding: 30px;
+}
+
+.preview-content .preview-kpis .kpi-value {
+ font-size: 2.5rem;
+}
+
+.preview-content .preview-kpis .kpi-label {
+ color: #94a3b8;
+}
+
+.preview-footer {
+ color: #64748b;
+ font-size: 14px;
+ margin-top: 40px;
+}
+
+.preview-controls {
+ display: flex;
+ justify-content: center;
+ gap: 12px;
+ padding: 20px;
+ background: rgba(0, 0, 0, 0.3);
+}
+
+.preview-controls button {
+ padding: 10px 24px;
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.preview-controls button:hover:not(:disabled) {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.preview-controls button:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+@media (max-width: 768px) {
+ .slides-workspace {
+ grid-template-columns: 1fr;
+ }
+
+ .slides-list {
+ max-height: 200px;
+ overflow-y: auto;
+ }
+
+ .editor-row {
+ grid-template-columns: 1fr;
+ }
+
+ .chart-type-grid {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ .preview-slide {
+ padding: 30px;
+ }
+
+ .preview-title {
+ font-size: 1.5rem;
+ }
+}
diff --git a/src/App.js b/src/App.js
index 4c1a3be..589550a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
import Dashboard from './components/Dashboard';
import Comparison from './components/Comparison';
+import Slides from './components/Slides';
import { fetchData } from './services/dataService';
import './App.css';
@@ -20,6 +21,13 @@ function App() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showDataLabels, setShowDataLabels] = useState(false);
+ const [dataSource, setDataSource] = useState('museums');
+
+ const dataSources = [
+ { id: 'museums', label: 'Museums', enabled: true },
+ { id: 'coffees', label: 'Coffees', enabled: false },
+ { id: 'ecommerce', label: 'eCommerce', enabled: false }
+ ];
useEffect(() => {
async function loadData() {
@@ -61,46 +69,75 @@ function App() {