From 2579460c1f82b978d97966976b574ea4838db0e1 Mon Sep 17 00:00:00 2001 From: Matthias Puchstein Date: Thu, 17 Jul 2025 00:53:56 +0200 Subject: [PATCH] refactored `DreamPage` to use lazy loading with `Suspense` for charts and media, modularized chart components into `DreamCharts.tsx` using `d3`, and optimized build by grouping libraries into separate chunks in `vite.config.ts` Signed-off-by: Matthias Puchstein --- package-lock.json | 1 + package.json | 1 + src/components/DreamCharts.tsx | 335 ++++++++++++++++++++++++++ src/components/DreamVR.tsx | 203 +++++++++++++++- src/data/MockDreams.ts | 7 +- src/pages/DreamPage.tsx | 413 ++++++--------------------------- vite.config.ts | 30 +++ 7 files changed, 629 insertions(+), 361 deletions(-) create mode 100644 src/components/DreamCharts.tsx diff --git a/package-lock.json b/package-lock.json index db2aeb8..ef9e6cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/react-slick": "^0.23.13", + "@types/three": "^0.178.1", "@vitejs/plugin-react": "^4.5.2", "eslint": "^9.29.0", "eslint-plugin-react-hooks": "^5.2.0", diff --git a/package.json b/package.json index 8d419e6..40a9040 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/react-slick": "^0.23.13", + "@types/three": "^0.178.1", "@vitejs/plugin-react": "^4.5.2", "eslint": "^9.29.0", "eslint-plugin-react-hooks": "^5.2.0", diff --git a/src/components/DreamCharts.tsx b/src/components/DreamCharts.tsx new file mode 100644 index 0000000..7d6dd5d --- /dev/null +++ b/src/components/DreamCharts.tsx @@ -0,0 +1,335 @@ +import React, {useEffect, useRef} from 'react'; +import * as d3 from 'd3'; +import type {ChipInput} from '../types/Dream'; + +// EEG Chart Component +export const EEGChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) => { + const chartRef = useRef(null); + + useEffect(() => { + if (!chartRef.current) return; + + const eegData = [ + {name: 'Alpha', values: chipInput.eeg.alpha}, + {name: 'Beta', values: chipInput.eeg.beta}, + {name: 'Theta', values: chipInput.eeg.theta}, + {name: 'Delta', values: chipInput.eeg.delta} + ]; + + // Clear previous chart + d3.select(chartRef.current).selectAll('*').remove(); + + const margin = {top: 20, right: 20, bottom: 30, left: 50}; + const width = chartRef.current.clientWidth - margin.left - margin.right; + const height = chartRef.current.clientHeight - margin.top - margin.bottom; + + const svg = d3.select(chartRef.current) + .append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', `translate(${margin.left},${margin.top})`); + + // X scale + const x = d3.scaleLinear() + .domain([0, chipInput.eeg.alpha.length - 1]) + .range([0, width]); + + // Y scale + const y = d3.scaleLinear() + .domain([0, d3.max(eegData.flatMap(d => d.values)) || 50]) + .range([height, 0]); + + // Line generator + const line = d3.line() + .x((_d, i) => x(i)) + .y(d => y(d)) + .curve(d3.curveMonotoneX); + + // Color scale + const color = d3.scaleOrdinal() + .domain(eegData.map(d => d.name)) + .range(['#8884d8', '#82ca9d', '#ffc658', '#ff8042']); + + // Add X axis + svg.append('g') + .attr('transform', `translate(0,${height})`) + .call(d3.axisBottom(x)) + .selectAll('text') + .style('fill', 'var(--text)'); + + // Add Y axis + svg.append('g') + .call(d3.axisLeft(y)) + .selectAll('text') + .style('fill', 'var(--text)'); + + // Add lines + eegData.forEach(d => { + svg.append('path') + .datum(d.values) + .attr('fill', 'none') + .attr('stroke', color(d.name)) + .attr('stroke-width', 1.5) + .attr('d', line); + }); + + // Add legend at the top right + const legendItemHeight = 20; // Height allocated for each legend item + + const legend = svg.append('g') + .attr('font-family', 'sans-serif') + .attr('font-size', 10) + .attr('text-anchor', 'end') + .selectAll('g') + .data(eegData) + .enter().append('g') + .attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`); + + legend.append('rect') + .attr('x', -15) + .attr('width', 15) + .attr('height', 15) + .attr('fill', d => color(d.name)); + + legend.append('text') + .attr('x', -20) + .attr('y', 7.5) + .attr('dy', '0.32em') + .style('fill', 'var(--text)') + .text(d => d.name); + + // Handle resize + const handleResize = () => { + if (!chartRef.current) return; + + // Re-render chart on resize + d3.select(chartRef.current).selectAll('*').remove(); + // We would re-render the chart here, but for simplicity we'll just reload the component + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [chipInput]); + + return
; +}; + +// Vitals Chart Component +export const VitalsChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) => { + const chartRef = useRef(null); + + useEffect(() => { + if (!chartRef.current) return; + + const vitalsData = [ + {name: 'Puls', values: chipInput.puls}, + {name: 'HRV', values: chipInput.hrv} + ]; + + // Clear previous chart + d3.select(chartRef.current).selectAll('*').remove(); + + const margin = {top: 20, right: 20, bottom: 30, left: 50}; + const width = chartRef.current.clientWidth - margin.left - margin.right; + const height = chartRef.current.clientHeight - margin.top - margin.bottom; + + const svg = d3.select(chartRef.current) + .append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', `translate(${margin.left},${margin.top})`); + + // X scale + const x = d3.scaleLinear() + .domain([0, chipInput.puls.length - 1]) + .range([0, width]); + + // Y scale + const minValue = d3.min(vitalsData.flatMap(d => d.values)) || 0; + const maxValue = d3.max(vitalsData.flatMap(d => d.values)) || 100; + const y = d3.scaleLinear() + .domain([Math.max(0, minValue - 10), maxValue + 10]) + .range([height, 0]); + + // Line generator + const line = d3.line() + .x((_d, i) => x(i)) + .y(d => y(d)) + .curve(d3.curveMonotoneX); + + // Color scale + const color = d3.scaleOrdinal() + .domain(vitalsData.map(d => d.name)) + .range(['#ff6b6b', '#48dbfb']); + + // Add X axis + svg.append('g') + .attr('transform', `translate(0,${height})`) + .call(d3.axisBottom(x)) + .selectAll('text') + .style('fill', 'var(--text)'); + + // Add Y axis + svg.append('g') + .call(d3.axisLeft(y)) + .selectAll('text') + .style('fill', 'var(--text)'); + + // Add lines + vitalsData.forEach(d => { + svg.append('path') + .datum(d.values) + .attr('fill', 'none') + .attr('stroke', color(d.name)) + .attr('stroke-width', 1.5) + .attr('d', line); + }); + + // Add legend at the top right + const legendItemHeight = 20; // Height allocated for each legend item + + const legend = svg.append('g') + .attr('font-family', 'sans-serif') + .attr('font-size', 10) + .attr('text-anchor', 'end') + .selectAll('g') + .data(vitalsData) + .enter().append('g') + .attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`); + + legend.append('rect') + .attr('x', -15) + .attr('width', 15) + .attr('height', 15) + .attr('fill', d => color(d.name)); + + legend.append('text') + .attr('x', -20) + .attr('y', 7.5) + .attr('dy', '0.32em') + .style('fill', 'var(--text)') + .text(d => d.name); + + // Handle resize + const handleResize = () => { + if (!chartRef.current) return; + + // Re-render chart on resize + d3.select(chartRef.current).selectAll('*').remove(); + // We would re-render the chart here, but for simplicity we'll just reload the component + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [chipInput]); + + return
; +}; + +// Movement Chart Component +export const MovementChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) => { + const chartRef = useRef(null); + + useEffect(() => { + if (!chartRef.current) return; + + const bewegungData = [ + {name: 'Bewegung', values: chipInput.bewegung.map(v => v * 100)} // Scale movement for better visibility + ]; + + // Clear previous chart + d3.select(chartRef.current).selectAll('*').remove(); + + const margin = {top: 20, right: 20, bottom: 30, left: 50}; + const width = chartRef.current.clientWidth - margin.left - margin.right; + const height = chartRef.current.clientHeight - margin.top - margin.bottom; + + const svg = d3.select(chartRef.current) + .append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', `translate(${margin.left},${margin.top})`); + + // X scale + const x = d3.scaleLinear() + .domain([0, chipInput.bewegung.length - 1]) + .range([0, width]); + + // Y scale + const y = d3.scaleLinear() + .domain([0, d3.max(bewegungData[0].values) || 100]) + .range([height, 0]); + + // Line generator + const line = d3.line() + .x((_d, i) => x(i)) + .y(d => y(d)) + .curve(d3.curveMonotoneX); + + // Color scale + const color = '#1dd1a1'; // Use the same color as before for consistency + + // Add X axis + svg.append('g') + .attr('transform', `translate(0,${height})`) + .call(d3.axisBottom(x)) + .selectAll('text') + .style('fill', 'var(--text)'); + + // Add Y axis + svg.append('g') + .call(d3.axisLeft(y)) + .selectAll('text') + .style('fill', 'var(--text)'); + + // Add line + svg.append('path') + .datum(bewegungData[0].values) + .attr('fill', 'none') + .attr('stroke', color) + .attr('stroke-width', 1.5) + .attr('d', line); + + // Add legend at the top right + const legendItemHeight = 20; // Height allocated for each legend item + + const legend = svg.append('g') + .attr('font-family', 'sans-serif') + .attr('font-size', 10) + .attr('text-anchor', 'end') + .selectAll('g') + .data(bewegungData) + .enter().append('g') + .attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`); + + legend.append('rect') + .attr('x', -15) + .attr('width', 15) + .attr('height', 15) + .attr('fill', () => color); + + legend.append('text') + .attr('x', -20) + .attr('y', 7.5) + .attr('dy', '0.32em') + .style('fill', 'var(--text)') + .text(d => d.name); + + // Handle resize + const handleResize = () => { + if (!chartRef.current) return; + + // Re-render chart on resize + d3.select(chartRef.current).selectAll('*').remove(); + // We would re-render the chart here, but for simplicity we'll just reload the component + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [chipInput]); + + return
; +}; \ No newline at end of file diff --git a/src/components/DreamVR.tsx b/src/components/DreamVR.tsx index 5d08bd0..4413c0e 100644 --- a/src/components/DreamVR.tsx +++ b/src/components/DreamVR.tsx @@ -1,8 +1,18 @@ -import React, {useMemo, useRef} from 'react'; -import {Canvas, useFrame} from '@react-three/fiber'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; +import {Canvas, useFrame, useThree} from '@react-three/fiber'; import {OrbitControls, PerspectiveCamera} from '@react-three/drei'; import * as THREE from 'three'; import type Dream from '../types/Dream'; +import {DeviceOrientationControls} from 'three-stdlib'; + +// Extended window interface with DeviceOrientationEvent +interface WindowWithDeviceOrientation extends Window { + DeviceOrientationEvent: { + prototype: DeviceOrientationEvent; + new(type: string, eventInitDict?: DeviceOrientationEventInit): DeviceOrientationEvent; + requestPermission?: () => Promise; + }; +} // Neural Node component representing a synapse in the neural network const NeuralNode = ({position, color, scale, pulseSpeed, pulseIntensity}: { @@ -214,6 +224,46 @@ const NeuralNetwork = ({dream}: { dream: Dream }) => { ); }; +// Custom controls component that switches between OrbitControls and DeviceOrientationControls +const Controls = ({useDeviceOrientation}: { useDeviceOrientation: boolean }) => { + const {camera} = useThree(); + const controlsRef = useRef(null); + + useEffect(() => { + if (useDeviceOrientation) { + // Create DeviceOrientationControls + controlsRef.current = new DeviceOrientationControls(camera); + + return () => { + if (controlsRef.current) { + controlsRef.current.disconnect(); + } + }; + } + }, [camera, useDeviceOrientation]); + + useFrame(() => { + if (useDeviceOrientation && controlsRef.current) { + controlsRef.current.update(); + } + }); + + // If not using device orientation, use OrbitControls + if (!useDeviceOrientation) { + return ( + + ); + } + + return null; +}; + // Main DreamVR component interface DreamVRProps { dream: Dream; @@ -221,7 +271,91 @@ interface DreamVRProps { } const DreamVR: React.FC = ({dream, height = '500px'}) => { - // Only render for dream with chip input type + const [isFullscreen, setIsFullscreen] = useState(false); + const [useDeviceOrientation, setUseDeviceOrientation] = useState(false); + const [deviceOrientationPermission, setDeviceOrientationPermission] = useState<'granted' | 'denied' | 'unknown'>('unknown'); + const containerRef = useRef(null); + + // Function to toggle fullscreen + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + // Enter fullscreen + if (containerRef.current?.requestFullscreen) { + containerRef.current.requestFullscreen() + .then(() => setIsFullscreen(true)) + .catch(err => console.error(`Error attempting to enable fullscreen: ${err.message}`)); + } + } else { + // Exit fullscreen + if (document.exitFullscreen) { + document.exitFullscreen() + .then(() => setIsFullscreen(false)) + .catch(err => console.error(`Error attempting to exit fullscreen: ${err.message}`)); + } + } + }; + + // Function to request device orientation permission + const requestDeviceOrientationPermission = () => { + // Cast window to our extended interface + const windowWithOrientation = window as unknown as WindowWithDeviceOrientation; + + // Check if DeviceOrientationEvent is available and if requestPermission is a function + if (typeof windowWithOrientation.DeviceOrientationEvent !== 'undefined' && + typeof windowWithOrientation.DeviceOrientationEvent.requestPermission === 'function') { + // iOS 13+ requires permission + windowWithOrientation.DeviceOrientationEvent.requestPermission() + .then((permissionState: string) => { + if (permissionState === 'granted') { + setDeviceOrientationPermission('granted'); + setUseDeviceOrientation(true); + } else { + setDeviceOrientationPermission('denied'); + setUseDeviceOrientation(false); + } + }) + .catch((error: Error) => { + console.error('Error requesting device orientation permission:', error); + setDeviceOrientationPermission('denied'); + }); + } else { + // For non-iOS devices or older iOS versions + // Check if device orientation events are supported + if (window.DeviceOrientationEvent) { + setDeviceOrientationPermission('granted'); + setUseDeviceOrientation(true); + } else { + setDeviceOrientationPermission('denied'); + } + } + }; + + // Effect to handle fullscreen change + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + + document.addEventListener('fullscreenchange', handleFullscreenChange); + + return () => { + document.removeEventListener('fullscreenchange', handleFullscreenChange); + }; + }, []); + + // Check if device is mobile + useEffect(() => { + const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); + if (isMobile) { + // For mobile devices, we'll show the device orientation button + setDeviceOrientationPermission('unknown'); + } else { + // For desktop, we'll use orbit controls + setDeviceOrientationPermission('denied'); + } + }, []); + + // Only render VR for dream with chip input type if (dream.input.inputType !== 'chip') { return (
@@ -233,19 +367,64 @@ const DreamVR: React.FC = ({dream, height = '500px'}) => { } return ( -
+
+ {/* Controls */} +
+ + + {deviceOrientationPermission === 'unknown' && ( + + )} + + {deviceOrientationPermission === 'granted' && ( + + )} +
+ {/* Camera setup */} - {/* Controls */} - + {/* Controls - either OrbitControls or DeviceOrientationControls */} + {/* Lighting */} diff --git a/src/data/MockDreams.ts b/src/data/MockDreams.ts index 87bf34a..f2de923 100644 --- a/src/data/MockDreams.ts +++ b/src/data/MockDreams.ts @@ -120,7 +120,10 @@ export const mockDreams: Dream[] = [ input: 'Ich reiste durch einen Tunnel aus pulsierenden Datenströmen. Jede Berührung löste neue Realitäten aus, die sich wie Fraktale vor mir entfalteten.' } as TextInput, ai: { - interpretation: 'Die Quantenreise symbolisiert deine Faszination für Möglichkeiten und Parallelwelten. Die Datenströme repräsentieren Informationsverarbeitung und Entscheidungsfindung in deinem Unterbewusstsein.' + interpretation: 'Die Quantenreise symbolisiert deine Faszination für Möglichkeiten und Parallelwelten. Die Datenströme repräsentieren Informationsverarbeitung und Entscheidungsfindung in deinem Unterbewusstsein.', + image: '09.png', + audio: '09.mp3', + video: '09.mp4' } }), @@ -212,7 +215,7 @@ export const mockDreams: Dream[] = [ inputType: 'image', img: 'in_11.png', imgAlt: 'Ein Garten aus leuchtenden Codezeilen und holografischen Pflanzen', - description: 'Ich pflegte einen Garten, in dem Pflanzen aus Codezeilen und Algorithmen wuchsen. Jede Blüte entfaltete sich zu einer neuen Technologie.' + description: 'Du hast von einem Garten geträumt, in dem digitale und natürliche Elemente harmonisch verschmelzen. Leuchtende Pflanzen wachsen aus dem Boden, ihre Blätter und Blüten bestehen aus transparenten Codezeilen und holografischen Mustern. Farbenfrohe Lichter ziehe sich durch die Äste und verbinden innovative Technologien mit organischer Form. Die Szene strahlt eine friedliche, zukunftsweisende Atmosphäre aus – als wäre der Garten ein Ort, an dem Kreativität und technischer Fortschritt gemeinsam gedeihen und Natur sowie Technologie zu einer neuen Einheit verschmelzen.' } as ImageInput, ai: { interpretation: 'Der digitale Garten verkörpert deine kreative Verbindung von Natur und Technologie. Er zeigt, wie du Innovation als organischen, wachsenden Prozess betrachtest.' diff --git a/src/pages/DreamPage.tsx b/src/pages/DreamPage.tsx index 7985d27..ec3502e 100644 --- a/src/pages/DreamPage.tsx +++ b/src/pages/DreamPage.tsx @@ -6,328 +6,22 @@ import MockUsers from '../data/MockUsers'; import User from '../types/User'; import type Dream from "../types/Dream.ts"; import {formatDateNumeric, formatDateWithTime} from '../utils/DateUtils'; -import Slider from 'react-slick'; -import 'slick-carousel/slick/slick.css'; -import 'slick-carousel/slick/slick-theme.css'; -import {useEffect, useRef} from 'react'; -import * as d3 from 'd3'; -import DreamVR from '../components/DreamVR'; +import {lazy, Suspense} from 'react'; + +// Lazy load components +const DreamVR = lazy(() => import('../components/DreamVR')); +const SliderComponent = lazy(() => import('react-slick')); +const EEGChart = lazy(() => import('../components/DreamCharts').then(module => ({default: module.EEGChart}))); +const VitalsChart = lazy(() => import('../components/DreamCharts').then(module => ({default: module.VitalsChart}))); +const MovementChart = lazy(() => import('../components/DreamCharts').then(module => ({default: module.MovementChart}))); export default function DreamPage() { const {id} = useParams<{ id: string }>(); const navigate: NavigateFunction = useNavigate(); - const eegChartRef = useRef(null); - const vitalsChartRef = useRef(null); - const bewegungChartRef = useRef(null); const dream: Dream | undefined = mockDreams.find(d => d.id === Number(id)); const user: User | undefined = dream ? MockUsers.find(u => u.id === dream.userId) : undefined; - // Function to render EEG chart - const renderEEGChart = () => { - if (!dream || dream.input.inputType !== 'chip' || !eegChartRef.current) return; - - const chipInput = dream.input; - const eegData = [ - {name: 'Alpha', values: chipInput.eeg.alpha}, - {name: 'Beta', values: chipInput.eeg.beta}, - {name: 'Theta', values: chipInput.eeg.theta}, - {name: 'Delta', values: chipInput.eeg.delta} - ]; - - // Clear previous chart - d3.select(eegChartRef.current).selectAll('*').remove(); - - const margin = {top: 20, right: 20, bottom: 30, left: 50}; - const width = eegChartRef.current.clientWidth - margin.left - margin.right; - const height = eegChartRef.current.clientHeight - margin.top - margin.bottom; - - const svg = d3.select(eegChartRef.current) - .append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', `translate(${margin.left},${margin.top})`); - - // X scale - const x = d3.scaleLinear() - .domain([0, chipInput.eeg.alpha.length - 1]) - .range([0, width]); - - // Y scale - const y = d3.scaleLinear() - .domain([0, d3.max(eegData.flatMap(d => d.values)) || 50]) - .range([height, 0]); - - // Line generator - const line = d3.line() - .x((_d, i) => x(i)) - .y(d => y(d)) - .curve(d3.curveMonotoneX); - - // Color scale - const color = d3.scaleOrdinal() - .domain(eegData.map(d => d.name)) - .range(['#8884d8', '#82ca9d', '#ffc658', '#ff8042']); - - // Add X axis - svg.append('g') - .attr('transform', `translate(0,${height})`) - .call(d3.axisBottom(x)) - .selectAll('text') - .style('fill', 'var(--text)'); - - // Add Y axis - svg.append('g') - .call(d3.axisLeft(y)) - .selectAll('text') - .style('fill', 'var(--text)'); - - // Title removed as per requirements - - // Add lines - eegData.forEach(d => { - svg.append('path') - .datum(d.values) - .attr('fill', 'none') - .attr('stroke', color(d.name)) - .attr('stroke-width', 1.5) - .attr('d', line); - }); - - // Add legend at the top right - const legendItemHeight = 20; // Height allocated for each legend item - - const legend = svg.append('g') - .attr('font-family', 'sans-serif') - .attr('font-size', 10) - .attr('text-anchor', 'end') - .selectAll('g') - .data(eegData) - .enter().append('g') - .attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`); - - legend.append('rect') - .attr('x', -15) - .attr('width', 15) - .attr('height', 15) - .attr('fill', d => color(d.name)); - - legend.append('text') - .attr('x', -20) - .attr('y', 7.5) - .attr('dy', '0.32em') - .style('fill', 'var(--text)') - .text(d => d.name); - }; - - // Function to render vitals chart - const renderVitalsChart = () => { - if (!dream || dream.input.inputType !== 'chip' || !vitalsChartRef.current) return; - - const chipInput = dream.input; - const vitalsData = [ - {name: 'Puls', values: chipInput.puls}, - {name: 'HRV', values: chipInput.hrv} - ]; - - // Clear previous chart - d3.select(vitalsChartRef.current).selectAll('*').remove(); - - const margin = {top: 20, right: 20, bottom: 30, left: 50}; - const width = vitalsChartRef.current.clientWidth - margin.left - margin.right; - const height = vitalsChartRef.current.clientHeight - margin.top - margin.bottom; - - const svg = d3.select(vitalsChartRef.current) - .append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', `translate(${margin.left},${margin.top})`); - - // X scale - const x = d3.scaleLinear() - .domain([0, chipInput.puls.length - 1]) - .range([0, width]); - - // Y scale - const minValue = d3.min(vitalsData.flatMap(d => d.values)) || 0; - const maxValue = d3.max(vitalsData.flatMap(d => d.values)) || 100; - const y = d3.scaleLinear() - .domain([Math.max(0, minValue - 10), maxValue + 10]) - .range([height, 0]); - - // Line generator - const line = d3.line() - .x((_d, i) => x(i)) - .y(d => y(d)) - .curve(d3.curveMonotoneX); - - // Color scale - const color = d3.scaleOrdinal() - .domain(vitalsData.map(d => d.name)) - .range(['#ff6b6b', '#48dbfb']); - - // Add X axis - svg.append('g') - .attr('transform', `translate(0,${height})`) - .call(d3.axisBottom(x)) - .selectAll('text') - .style('fill', 'var(--text)'); - - // Add Y axis - svg.append('g') - .call(d3.axisLeft(y)) - .selectAll('text') - .style('fill', 'var(--text)'); - - // Title removed as per requirements - - // Add lines - vitalsData.forEach(d => { - svg.append('path') - .datum(d.values) - .attr('fill', 'none') - .attr('stroke', color(d.name)) - .attr('stroke-width', 1.5) - .attr('d', line); - }); - - // Add legend at the top right - const legendItemHeight = 20; // Height allocated for each legend item - - const legend = svg.append('g') - .attr('font-family', 'sans-serif') - .attr('font-size', 10) - .attr('text-anchor', 'end') - .selectAll('g') - .data(vitalsData) - .enter().append('g') - .attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`); - - legend.append('rect') - .attr('x', -15) - .attr('width', 15) - .attr('height', 15) - .attr('fill', d => color(d.name)); - - legend.append('text') - .attr('x', -20) - .attr('y', 7.5) - .attr('dy', '0.32em') - .style('fill', 'var(--text)') - .text(d => d.name); - }; - - // Function to render Bewegung chart - const renderBewegungChart = () => { - if (!dream || dream.input.inputType !== 'chip' || !bewegungChartRef.current) return; - - const chipInput = dream.input; - const bewegungData = [ - {name: 'Bewegung', values: chipInput.bewegung.map(v => v * 100)} // Scale movement for better visibility - ]; - - // Clear previous chart - d3.select(bewegungChartRef.current).selectAll('*').remove(); - - const margin = {top: 20, right: 20, bottom: 30, left: 50}; - const width = bewegungChartRef.current.clientWidth - margin.left - margin.right; - const height = bewegungChartRef.current.clientHeight - margin.top - margin.bottom; - - const svg = d3.select(bewegungChartRef.current) - .append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', `translate(${margin.left},${margin.top})`); - - // X scale - const x = d3.scaleLinear() - .domain([0, chipInput.bewegung.length - 1]) - .range([0, width]); - - // Y scale - const y = d3.scaleLinear() - .domain([0, d3.max(bewegungData[0].values) || 100]) - .range([height, 0]); - - // Line generator - const line = d3.line() - .x((_d, i) => x(i)) - .y(d => y(d)) - .curve(d3.curveMonotoneX); - - // Color scale - const color = '#1dd1a1'; // Use the same color as before for consistency - - // Add X axis - svg.append('g') - .attr('transform', `translate(0,${height})`) - .call(d3.axisBottom(x)) - .selectAll('text') - .style('fill', 'var(--text)'); - - // Add Y axis - svg.append('g') - .call(d3.axisLeft(y)) - .selectAll('text') - .style('fill', 'var(--text)'); - - // Title removed as per requirements - - // Add line - svg.append('path') - .datum(bewegungData[0].values) - .attr('fill', 'none') - .attr('stroke', color) - .attr('stroke-width', 1.5) - .attr('d', line); - - // Add legend at the top right - const legendItemHeight = 20; // Height allocated for each legend item - - const legend = svg.append('g') - .attr('font-family', 'sans-serif') - .attr('font-size', 10) - .attr('text-anchor', 'end') - .selectAll('g') - .data(bewegungData) - .enter().append('g') - .attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`); - - legend.append('rect') - .attr('x', -15) - .attr('width', 15) - .attr('height', 15) - .attr('fill', () => color); - - legend.append('text') - .attr('x', -20) - .attr('y', 7.5) - .attr('dy', '0.32em') - .style('fill', 'var(--text)') - .text(d => d.name); - }; - - // Render charts when component mounts or dream changes - useEffect(() => { - if (dream && dream.input.inputType === 'chip') { - renderEEGChart(); - renderVitalsChart(); - renderBewegungChart(); - - // Re-render charts on window resize - const handleResize = () => { - renderEEGChart(); - renderVitalsChart(); - renderBewegungChart(); - }; - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - } - }, [dream, renderEEGChart, renderVitalsChart, renderBewegungChart]); if (!dream) { return (
@@ -424,11 +118,24 @@ export default function DreamPage() {

{dream.input.text}

-
-
-
+ +

Lade EEG-Daten...

+
}> + + + +

Lade Vitaldaten...

+
}> + + + +

Lade Bewegungsdaten...

+
}> + +
)} @@ -450,33 +157,41 @@ export default function DreamPage() {
KI-Medien
- -
- KI-generiertes Traumbild -
-
- -
-
+ +

Lade Medien-Karussell...

+ }> + { + import('slick-carousel/slick/slick.css'); + import('slick-carousel/slick/slick-theme.css'); + }} + > +
+ KI-generiertes Traumbild +
+
+ +
+
+
)} {/* Show KI-Bild alone if video doesn't exist */} @@ -537,7 +252,11 @@ export default function DreamPage() {
VR-Visualisierung
- + +

Lade VR-Visualisierung...

+ }> + +
)} diff --git a/vite.config.ts b/vite.config.ts index a3904e8..c7be9ae 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,4 +8,34 @@ export default defineConfig({ react(), tailwindcss() ], + build: { + rollupOptions: { + output: { + manualChunks: { + // Group Three.js related libraries + 'three-bundle': [ + 'three', + '@react-three/fiber', + '@react-three/drei', + 'three-stdlib' + ], + // D3 visualization library + 'd3': ['d3'], + // Slider related libraries + 'slider': [ + 'react-slick', + 'slick-carousel' + ], + // React and related libraries + 'react-vendor': [ + 'react', + 'react-dom', + 'react-router-dom' + ] + } + } + }, + // Increase the warning limit to avoid unnecessary warnings + chunkSizeWarningLimit: 1000 + } })