"use client"; import type React from "react"; import { useState, useEffect, useCallback, useMemo, useRef } from "react"; const DEG_TO_RAD = Math.PI / 180; interface Material { name: string; absorptionFactor: number; lossFactor: number; color: string; efficiency: number; } const MATERIALS: Record = { metal: { name: "Metal", absorptionFactor: 0.85, lossFactor: 8, color: "#64748B", efficiency: 0.75, }, plastic: { name: "Plastic", absorptionFactor: 0.6, lossFactor: 3, color: "#A855F7", efficiency: 0.45, }, darkPaint: { name: "Dark Coated", absorptionFactor: 0.95, lossFactor: 5, color: "#1E293B", efficiency: 0.88, }, silicon: { name: "Silicon", absorptionFactor: 0.92, lossFactor: 4, color: "#0F172A", efficiency: 0.92, }, }; const MATERIAL_KEYS = Object.keys(MATERIALS); const MAX_SUN_INTENSITY = 100; const GRAPH_TIME_POINTS = 97; interface DataPoint { time: number; heat: number; efficiency: number; power: number; cumulative: number; } const calculateSunAltitude = (timeOfDay: number): number => { if (timeOfDay < 5 || timeOfDay > 19) { return 0; } const daylightHours = 14; const sunriseTime = 5; const normalizedTime = (timeOfDay - sunriseTime) / daylightHours; if (normalizedTime < 0 || normalizedTime > 1) return 0; const sunAngleRadians = normalizedTime * Math.PI; return 90 * Math.sin(sunAngleRadians); }; const calculateHeatValue = ( timeOfDay: number, material: Material, panelAngleDegrees: number ): number => { const sunAltitude = calculateSunAltitude(timeOfDay); if (sunAltitude <= 0) { return -material.lossFactor; } const currentSunIntensity = MAX_SUN_INTENSITY * (sunAltitude / 90); const incidenceAngleDegrees = Math.abs(90 - sunAltitude - panelAngleDegrees); let angleMultiplier = Math.cos(incidenceAngleDegrees * DEG_TO_RAD); angleMultiplier = Math.max(0, angleMultiplier); return currentSunIntensity * material.absorptionFactor * angleMultiplier; }; const Sun3D: React.FC<{ size: number; position: { x: number; y: number }; sunAltitude: number; }> = ({ size, position, sunAltitude }) => { const intensity = Math.max(0.3, sunAltitude / 90); return (
); }; const Moon3D: React.FC<{ size: number; position: { x: number; y: number }; }> = ({ size, position }) => { return (
); }; const SunView: React.FC<{ timeOfDay: number; skyWidth: number; skyHeight: number; }> = ({ timeOfDay, skyWidth, skyHeight }) => { const sunAltitude = calculateSunAltitude(timeOfDay); if (sunAltitude <= 0 && (timeOfDay < 5 || timeOfDay > 19)) { const moonX = skyWidth * 0.75; const moonY = skyHeight * 0.3; const moonSize = Math.min(skyWidth * 0.08, 35); return ; } const sunPathRadius = Math.min(skyWidth * 0.45, skyHeight * 0.7); const sunPathCenterY = skyHeight * 0.85; const daylightHours = 14; const sunriseTime = 5; const normalizedDayTime = Math.max( 0, Math.min(1, (timeOfDay - sunriseTime) / daylightHours) ); const sunAngleOnArc = normalizedDayTime * Math.PI; const sunX = skyWidth / 2 - sunPathRadius * Math.cos(sunAngleOnArc); const sunY = sunPathCenterY - sunPathRadius * Math.sin(sunAngleOnArc); const minSunSize = Math.min(skyWidth * 0.07, 25); const maxSunSize = Math.min(skyWidth * 0.12, 50); const sunSize = minSunSize + (maxSunSize - minSunSize) * (sunAltitude / 90); return ( ); }; const SkyBackground: React.FC<{ timeOfDay: number }> = ({ timeOfDay }) => { const getSkyGradient = () => { if (timeOfDay >= 4 && timeOfDay < 5.5) { return "linear-gradient(to bottom, #0F172A 0%, #1E293B 100%)"; } else if (timeOfDay >= 5.5 && timeOfDay < 6.5) { return "linear-gradient(to bottom, #FB7185 0%, #F97316 50%, #FBBF24 100%)"; } else if (timeOfDay >= 6.5 && timeOfDay < 8) { return "linear-gradient(to bottom, #FDE047 0%, #38BDF8 100%)"; } else if (timeOfDay >= 8 && timeOfDay < 10) { return "linear-gradient(to bottom, #0EA5E9 0%, #7DD3FC 100%)"; } else if (timeOfDay >= 10 && timeOfDay < 16) { return "linear-gradient(to bottom, #0284C7 0%, #38BDF8 100%)"; } else if (timeOfDay >= 16 && timeOfDay < 17.5) { return "linear-gradient(to bottom, #38BDF8 0%, #FBBF24 50%, #F97316 100%)"; } else if (timeOfDay >= 17.5 && timeOfDay < 18.5) { return "linear-gradient(to bottom, #F97316 0%, #3B82F6 30%, #EC4899 70%, #8B5CF6 100%)"; } else if (timeOfDay >= 18.5 && timeOfDay < 19.5) { return "linear-gradient(to bottom, #8B5CF6 0%, #6366F1 50%, #1E293B 100%)"; } else if (timeOfDay >= 19.5 && timeOfDay < 21) { return "linear-gradient(to bottom, #1E293B 0%, #0F172A 100%)"; } else { return "linear-gradient(to bottom, #020617 0%, #0F172A 50%, #1E293B 100%)"; } }; const renderStars = () => { if (timeOfDay >= 19 || timeOfDay < 5) { const starCount = 40; const stars = []; for (let i = 0; i < starCount; i++) { const size = Math.random() * 2.5 + 1; const top = Math.random() * 60; const left = Math.random() * 100; const opacity = Math.random() * 0.7 + 0.3; const animationDelay = Math.random() * 4; stars.push(
); } return stars; } return null; }; return (
{renderStars()}
); }; const SolarPanelView: React.FC<{ panelAngle: number; materialColor: string; }> = ({ panelAngle, materialColor }) => { return (
); }; const MobileOptimizedGraph: React.FC<{ graphData: DataPoint[]; currentTimeOfDay: number; material: Material; panelAngle: number; }> = ({ graphData, currentTimeOfDay, material, panelAngle }) => { const [activeMetric, setActiveMetric] = useState< "heat" | "efficiency" | "power" | "cumulative" >("heat"); const [showStats, setShowStats] = useState(true); const [hoveredIndex, setHoveredIndex] = useState(null); const containerRef = useRef(null); const [dimensions, setDimensions] = useState({ width: 350, height: 250 }); useEffect(() => { const updateDimensions = () => { if (containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); setDimensions({ width: Math.max(300, rect.width - 32), height: Math.max(200, Math.min(300, rect.width * 0.6)), }); } }; updateDimensions(); window.addEventListener("resize", updateDimensions); return () => window.removeEventListener("resize", updateDimensions); }, []); const getMetricValue = (point: DataPoint) => { switch (activeMetric) { case "heat": return point.heat; case "efficiency": return point.efficiency; case "power": return point.power; case "cumulative": return point.cumulative; default: return point.heat; } }; const metricValues = graphData.map(getMetricValue); const maxValue = Math.max(1, ...metricValues); const minValue = Math.min(-1, ...metricValues); const valueRange = maxValue - minValue; const getMetricColor = () => { switch (activeMetric) { case "heat": return "#EF4444"; case "efficiency": return "#10B981"; case "power": return "#F97316"; case "cumulative": return "#8B5CF6"; default: return "#EF4444"; } }; const getMetricUnit = () => { switch (activeMetric) { case "heat": return "units"; case "efficiency": return "%"; case "power": return "W"; case "cumulative": return "Wh"; default: return "units"; } }; const stats = useMemo(() => { const values = metricValues; const avg = values.reduce((a, b) => a + b, 0) / values.length; const max = Math.max(...values); const min = Math.min(...values); const peak = graphData.find((p) => getMetricValue(p) === max); const totalEnergy = graphData.reduce( (sum, p) => sum + Math.max(0, p.power), 0 ); return { avg, max, min, peak, totalEnergy }; }, [metricValues, graphData, activeMetric]); const currentPointIndex = Math.floor( (currentTimeOfDay / 24) * (graphData.length - 1) ); const hoveredPoint = hoveredIndex !== null ? graphData[hoveredIndex] : null; return (
{/* Metric Selection */}
{(["heat", "efficiency", "power", "cumulative"] as const).map( (metric) => ( ) )}
{/* Stats Toggle */}
{/* Stats Cards */} {showStats && (
Peak Value
{stats.max.toFixed(1)} {getMetricUnit()}
Average
{stats.avg.toFixed(1)} {getMetricUnit()}
Peak Time
{stats.peak ? `${Math.floor(stats.peak.time)}:${String( Math.round((stats.peak.time % 1) * 60) ).padStart(2, "0")}` : "N/A"}
Total Energy
{stats.totalEnergy.toFixed(0)} Wh
)} {/* Modern Bar Chart */}
{/* Chart Title */}
{activeMetric.charAt(0).toUpperCase() + activeMetric.slice(1)} Over Time
{/* Bar Chart Container */}
{/* Y-axis labels */}
{maxValue.toFixed(0)} {((maxValue + minValue) / 2).toFixed(0)} {minValue.toFixed(0)}
{/* Bars */} {graphData .filter((_, i) => i % 4 === 0) .map((point, index) => { const actualIndex = index * 4; const value = getMetricValue(point); const normalizedValue = valueRange === 0 ? 0 : (value - minValue) / valueRange; const barHeight = Math.max( 4, normalizedValue * (dimensions.height - 40) ); const isCurrentTime = Math.abs(actualIndex - currentPointIndex) <= 2; const isHovered = hoveredIndex === actualIndex; return (
setHoveredIndex(actualIndex)} onMouseLeave={() => setHoveredIndex(null)} onTouchStart={() => setHoveredIndex(actualIndex)} > {/* Value label on hover */} {isHovered && (
{value.toFixed(1)} {getMetricUnit()}
)}
); })}
{/* X-axis labels */}
0h 6h 12h 18h 24h
{/* Current time indicator */}
Current: {Math.floor(currentTimeOfDay)}: {String(Math.round((currentTimeOfDay % 1) * 60)).padStart(2, "0")}
{/* Detailed info for hovered point */} {hoveredPoint && (
Time: {Math.floor(hoveredPoint.time)}: {String(Math.round((hoveredPoint.time % 1) * 60)).padStart(2, "0")}
Heat: {hoveredPoint.heat.toFixed(1)} units
Power: {hoveredPoint.power.toFixed(1)}W
Efficiency: {hoveredPoint.efficiency.toFixed(1)}%
Energy: {hoveredPoint.cumulative.toFixed(1)}Wh
)}
); }; const ResponsiveSlider: React.FC<{ label: string; value: number; min: number; max: number; step: number; onChange: (value: number) => void; unit?: string; formatValue?: (value: number) => string; disabled?: boolean; }> = ({ label, value, min, max, step, onChange, unit = "", formatValue, disabled = false, }) => { const [isDragging, setIsDragging] = useState(false); const percentage = ((value - min) / (max - min)) * 100; const displayValue = formatValue ? formatValue(value) : `${value.toFixed(1)}${unit}`; return (
!disabled && onChange(Number(e.target.value))} onMouseDown={() => !disabled && setIsDragging(true)} onMouseUp={() => setIsDragging(false)} onTouchStart={() => !disabled && setIsDragging(true)} onTouchEnd={() => setIsDragging(false)} disabled={disabled} style={{ position: "absolute", top: "0", left: "0", width: "100%", height: "100%", opacity: 0, cursor: disabled ? "not-allowed" : "pointer", zIndex: 5, }} aria-label={label} />
); }; const ModernAngleStepper: React.FC<{ label: string; value: number; onChange: (value: number) => void; disabled?: boolean; }> = ({ label, value, onChange, disabled = false }) => { const [isAdjusting, setIsAdjusting] = useState(false); const adjustValue = (delta: number) => { if (disabled) return; const newValue = Math.max(0, Math.min(90, value + delta)); onChange(newValue); setIsAdjusting(true); setTimeout(() => setIsAdjusting(false), 200); }; const presetAngles = [ { label: "Flat", value: 0 }, { label: "Low", value: 15 }, { label: "Optimal", value: 30 }, { label: "High", value: 45 }, { label: "Steep", value: 60 }, { label: "Vertical", value: 90 }, ]; return (
{/* Preset Angle Buttons */}
{presetAngles.map((preset) => ( ))}
{/* Modern Stepper Controls */}
{/* Large Decrease */} {/* Small Decrease */} {/* Visual Angle Indicator */}
{/* Small Increase */} {/* Large Increase */}
{/* Angle Range Indicator */}
); }; const SolarSimulator: React.FC = () => { const [timeOfDay, setTimeOfDay] = useState(12); const [selectedMaterialKey, setSelectedMaterialKey] = useState( MATERIAL_KEYS[0] ); const [panelAngle, setPanelAngle] = useState(30); const [manualPanelAngle, setManualPanelAngle] = useState(30); const [deviceOrientationPermission, setDeviceOrientationPermission] = useState<"prompt" | "granted" | "denied" | null>(null); const [orientationMode, setOrientationMode] = useState<"manual" | "device">( "manual" ); const [isCalibrated, setIsCalibrated] = useState(false); const [calibrationOffset, setCalibrationOffset] = useState(0); const [rawOrientation, setRawOrientation] = useState<{ alpha: number | null; beta: number | null; gamma: number | null; }>({ alpha: null, beta: null, gamma: null }); const [skyViewDims, setSkyViewDims] = useState({ width: 300, height: 200 }); const skyViewRef = useCallback((node: HTMLDivElement | null) => { if (node) { const updateDimensions = () => { setSkyViewDims({ width: node.offsetWidth, height: node.offsetHeight }); }; updateDimensions(); const resizeObserver = new ResizeObserver(updateDimensions); resizeObserver.observe(node); return () => resizeObserver.disconnect(); } }, []); useEffect(() => { const handleOrientation = (event: DeviceOrientationEvent) => { const { alpha, beta, gamma } = event; setRawOrientation({ alpha, beta, gamma }); if (orientationMode === "device" && beta !== null) { let adjustedBeta = beta - calibrationOffset; if (window.screen && window.screen.orientation) { const orientation = window.screen.orientation.angle; if (orientation === 90 || orientation === -90) { adjustedBeta = gamma !== null ? gamma - calibrationOffset : adjustedBeta; } } let newAngle = Math.abs(adjustedBeta); newAngle = Math.max(0, Math.min(90, newAngle)); setPanelAngle(newAngle); } }; if (deviceOrientationPermission === "granted") { window.addEventListener("deviceorientation", handleOrientation, true); return () => window.removeEventListener( "deviceorientation", handleOrientation, true ); } }, [deviceOrientationPermission, orientationMode, calibrationOffset]); useEffect(() => { if ( typeof (DeviceOrientationEvent as any).requestPermission === "function" ) { setDeviceOrientationPermission("prompt"); } else if (window.DeviceOrientationEvent) { setDeviceOrientationPermission("granted"); } else { setDeviceOrientationPermission("denied"); } }, []); const requestDeviceOrientationPermission = async () => { try { const permissionState = await ( DeviceOrientationEvent as any ).requestPermission(); if (permissionState === "granted") { setDeviceOrientationPermission("granted"); setOrientationMode("device"); } else { setDeviceOrientationPermission("denied"); } } catch (error) { console.error("Error requesting device orientation permission:", error); setDeviceOrientationPermission("denied"); } }; const calibrateDevice = () => { if (rawOrientation.beta !== null) { setCalibrationOffset(rawOrientation.beta); setIsCalibrated(true); } }; const toggleOrientationMode = () => { if (orientationMode === "manual") { if (deviceOrientationPermission === "granted") { setOrientationMode("device"); } } else { setOrientationMode("manual"); setPanelAngle(manualPanelAngle); } }; useEffect(() => { if (orientationMode === "manual") { setManualPanelAngle(panelAngle); } }, [panelAngle, orientationMode]); const currentMaterial = MATERIALS[selectedMaterialKey]; const graphData = useMemo(() => { const dataPoints: DataPoint[] = []; let cumulativeEnergy = 0; for (let i = 0; i < GRAPH_TIME_POINTS; i++) { const time = (i / (GRAPH_TIME_POINTS - 1)) * 24; const heat = calculateHeatValue(time, currentMaterial, panelAngle); const efficiency = currentMaterial.efficiency * Math.max(0, Math.min(1, heat / 50)); const power = Math.max(0, heat * efficiency * 0.1); cumulativeEnergy += power * (24 / GRAPH_TIME_POINTS); dataPoints.push({ time, heat, efficiency: efficiency * 100, power, cumulative: cumulativeEnergy, }); } return dataPoints; }, [currentMaterial, panelAngle]); const formatTimeValue = (value: number) => { const hours = Math.floor(value); const minutes = Math.round((value % 1) * 60); return `${hours}:${String(minutes).padStart(2, "0")}`; }; const currentHeat = calculateHeatValue( timeOfDay, currentMaterial, panelAngle ); const globalStyles = ` @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;900&display=swap'); @keyframes sunRotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes sunPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } } @keyframes moonGlow { 0%, 100% { opacity: 0.85; } 50% { opacity: 1; } } @keyframes twinkle { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } } * { box-sizing: border-box; } body { margin: 0; padding: 0; font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(135deg, #F8FAFC 0%, #E2E8F0 100%); min-height: 100vh; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @media (max-width: 320px) { html { font-size: 13px; } } @media (min-width: 321px) and (max-width: 480px) { html { font-size: 14px; } } @media (min-width: 481px) and (max-width: 768px) { html { font-size: 15px; } } @media (min-width: 769px) and (max-width: 1024px) { html { font-size: 16px; } } @media (min-width: 1025px) and (max-width: 1440px) { html { font-size: 17px; } } @media (min-width: 1441px) { html { font-size: 18px; } } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } } `; return (

Solar Performance Studio

Advanced solar panel simulation with real-time analytics and device integration

{skyViewDims.width > 0 && ( )}
{orientationMode === "device" && deviceOrientationPermission === "granted" && ( )}
{deviceOrientationPermission === "granted" && (
Tilt Sensor:{" "} {orientationMode === "device" ? "Active" : "Inactive"} |{" "} {isCalibrated ? "Calibrated" : "Uncalibrated"}
Panel: {panelAngle.toFixed(1)}° Beta: {rawOrientation.beta?.toFixed(1) ?? "N/A"}° Gamma: {rawOrientation.gamma?.toFixed(1) ?? "N/A"}°
)} {deviceOrientationPermission === "prompt" && ( )} {deviceOrientationPermission === "denied" && (

Device orientation denied. Check browser settings or use manual mode.

)}

Advanced Performance Analytics

); }; export default SolarSimulator;