Solar
🧩 Syntax:
"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<string, Material> = {
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 (
<div
style={{
position: "absolute",
left: `${position.x}px`,
top: `${position.y}px`,
width: `${size}px`,
height: `${size}px`,
transform: "translate(-50%, -50%)",
}}
>
<div
style={{
position: "absolute",
width: "100%",
height: "100%",
borderRadius: "50%",
background: `radial-gradient(circle at 30% 30%,
#FFFBEB 0%,
#FEF3C7 20%,
#FDE047 40%,
#F59E0B 70%,
#EA580C 90%,
#3B82F6 100%)`,
boxShadow: `
0 0 ${size * 0.4}px rgba(251, 191, 36, ${0.8 * intensity}),
0 0 ${size * 0.8}px rgba(245, 158, 11, ${0.5 * intensity}),
inset -${size * 0.05}px -${size * 0.05}px ${
size * 0.15
}px rgba(0,0,0,0.2),
inset ${size * 0.05}px ${size * 0.05}px ${
size * 0.15
}px rgba(255,255,255,0.4)`,
animation:
"sunRotate 25s linear infinite, sunPulse 4s ease-in-out infinite alternate",
}}
/>
</div>
);
};
const Moon3D: React.FC<{
size: number;
position: { x: number; y: number };
}> = ({ size, position }) => {
return (
<div
style={{
position: "absolute",
left: `${position.x}px`,
top: `${position.y}px`,
width: `${size}px`,
height: `${size}px`,
transform: "translate(-50%, -50%)",
}}
>
<div
style={{
position: "absolute",
width: "100%",
height: "100%",
borderRadius: "50%",
background: `radial-gradient(circle at 35% 35%,
#F8FAFC 0%,
#E2E8F0 30%,
#CBD5E1 60%,
#94A3B8 100%)`,
boxShadow: `
0 0 ${size * 0.3}px rgba(248, 250, 252, 0.4),
inset -${size * 0.08}px -${size * 0.08}px ${
size * 0.2
}px rgba(0,0,0,0.15),
inset ${size * 0.08}px ${size * 0.08}px ${
size * 0.2
}px rgba(255,255,255,0.5)`,
animation: "moonGlow 6s ease-in-out infinite alternate",
}}
/>
</div>
);
};
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 <Moon3D size={moonSize} position={{ x: moonX, y: moonY }} />;
}
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 (
<Sun3D
size={sunSize}
position={{ x: sunX, y: sunY }}
sunAltitude={sunAltitude}
/>
);
};
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(
<div
key={i}
style={{
position: "absolute",
width: `${size}px`,
height: `${size}px`,
backgroundColor: "#F8FAFC",
borderRadius: "50%",
top: `${top}%`,
left: `${left}%`,
opacity,
animation: `twinkle 3s infinite alternate ${animationDelay}s`,
boxShadow: `0 0 ${size * 3}px rgba(248, 250, 252, 0.6)`,
}}
/>
);
}
return stars;
}
return null;
};
return (
<div
style={{
width: "100%",
height: "100%",
background: getSkyGradient(),
position: "relative",
overflow: "hidden",
transition: "background 1.5s cubic-bezier(0.4, 0, 0.2, 1)",
}}
>
{renderStars()}
</div>
);
};
const SolarPanelView: React.FC<{
panelAngle: number;
materialColor: string;
}> = ({ panelAngle, materialColor }) => {
return (
<div
style={{
width: "min(75%, 220px)",
height: "clamp(24px, 5vw, 32px)",
backgroundColor: materialColor,
border: "none",
borderRadius: "4px",
margin: "24px auto 0",
transformOrigin: "center center",
transform: `perspective(500px) rotateX(${panelAngle}deg)`,
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
boxShadow: `
inset -6px -6px 12px rgba(0, 0, 0, 0.15),
inset 6px 6px 12px rgba(255, 255, 255, 0.15),
0 12px 24px rgba(0, 0, 0, 0.2),
0 4px 8px rgba(0, 0, 0, 0.1)`,
position: "relative",
overflow: "hidden",
}}
aria-label="Solar panel"
>
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundImage:
"linear-gradient(rgba(255, 255, 255, 0.15) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.15) 1px, transparent 1px)",
backgroundSize: "8px 8px",
}}
/>
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
width: "60%",
height: "60%",
transform: "translate(-50%, -50%)",
background:
"linear-gradient(45deg, rgba(255,255,255,0.1) 0%, transparent 50%, rgba(0,0,0,0.1) 100%)",
borderRadius: "6px",
}}
/>
</div>
);
};
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<number | null>(null);
const containerRef = useRef<HTMLDivElement>(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 (
<div ref={containerRef} style={{ width: "100%", position: "relative" }}>
{/* Metric Selection */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "8px",
marginBottom: "20px",
}}
>
{(["heat", "efficiency", "power", "cumulative"] as const).map(
(metric) => (
<button
key={metric}
onClick={() => setActiveMetric(metric)}
style={{
padding: "12px 8px",
fontSize: "0.8rem",
border: "none",
borderRadius: "12px",
backgroundColor:
activeMetric === metric ? getMetricColor() : "#F8FAFC",
color: activeMetric === metric ? "#FFFFFF" : "#475569",
cursor: "pointer",
fontWeight: "700",
textTransform: "capitalize",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
boxShadow:
activeMetric === metric
? `0 8px 25px -3px ${getMetricColor()}40`
: "0 2px 4px rgba(0,0,0,0.1)",
transform:
activeMetric === metric
? "translateY(-2px)"
: "translateY(0)",
}}
>
{metric}
</button>
)
)}
</div>
{/* Stats Toggle */}
<div
style={{
display: "flex",
justifyContent: "center",
marginBottom: "16px",
}}
>
<button
onClick={() => setShowStats(!showStats)}
style={{
padding: "8px 16px",
fontSize: "0.8rem",
border: "none",
borderRadius: "8px",
backgroundColor: showStats ? "#3B82F6" : "#F8FAFC",
color: showStats ? "#FFFFFF" : "#475569",
cursor: "pointer",
fontWeight: "600",
transition: "all 0.3s ease",
boxShadow: showStats
? "0 4px 12px #05966940"
: "0 2px 4px rgba(0,0,0,0.1)",
}}
>
{showStats ? "Hide Stats" : "Show Stats"}
</button>
</div>
{/* Stats Cards */}
{showStats && (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "12px",
marginBottom: "20px",
}}
>
<div
style={{
padding: "16px",
backgroundColor: "#FAFAFA",
borderRadius: "12px",
textAlign: "center",
border: "1px solid #E5E7EB",
}}
>
<div
style={{
fontSize: "0.7rem",
color: "#6B7280",
fontWeight: "600",
marginBottom: "4px",
}}
>
Peak Value
</div>
<div
style={{
fontSize: "1.1rem",
color: getMetricColor(),
fontWeight: "700",
}}
>
{stats.max.toFixed(1)} {getMetricUnit()}
</div>
</div>
<div
style={{
padding: "16px",
backgroundColor: "#FAFAFA",
borderRadius: "12px",
textAlign: "center",
border: "1px solid #E5E7EB",
}}
>
<div
style={{
fontSize: "0.7rem",
color: "#6B7280",
fontWeight: "600",
marginBottom: "4px",
}}
>
Average
</div>
<div
style={{
fontSize: "1.1rem",
color: getMetricColor(),
fontWeight: "700",
}}
>
{stats.avg.toFixed(1)} {getMetricUnit()}
</div>
</div>
<div
style={{
padding: "16px",
backgroundColor: "#FAFAFA",
borderRadius: "12px",
textAlign: "center",
border: "1px solid #E5E7EB",
}}
>
<div
style={{
fontSize: "0.7rem",
color: "#6B7280",
fontWeight: "600",
marginBottom: "4px",
}}
>
Peak Time
</div>
<div
style={{
fontSize: "1.1rem",
color: getMetricColor(),
fontWeight: "700",
}}
>
{stats.peak
? `${Math.floor(stats.peak.time)}:${String(
Math.round((stats.peak.time % 1) * 60)
).padStart(2, "0")}`
: "N/A"}
</div>
</div>
<div
style={{
padding: "16px",
backgroundColor: "#FAFAFA",
borderRadius: "12px",
textAlign: "center",
border: "1px solid #E5E7EB",
}}
>
<div
style={{
fontSize: "0.7rem",
color: "#6B7280",
fontWeight: "600",
marginBottom: "4px",
}}
>
Total Energy
</div>
<div
style={{
fontSize: "1.1rem",
color: getMetricColor(),
fontWeight: "700",
}}
>
{stats.totalEnergy.toFixed(0)} Wh
</div>
</div>
</div>
)}
{/* Modern Bar Chart */}
<div
style={{
backgroundColor: "#FFFFFF",
borderRadius: "16px",
padding: "20px",
boxShadow:
"0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
border: "1px solid #F3F4F6",
position: "relative",
}}
>
{/* Chart Title */}
<div
style={{
textAlign: "center",
marginBottom: "16px",
fontSize: "0.9rem",
fontWeight: "700",
color: "#374151",
}}
>
{activeMetric.charAt(0).toUpperCase() + activeMetric.slice(1)} Over
Time
</div>
{/* Bar Chart Container */}
<div
style={{
display: "flex",
alignItems: "end",
justifyContent: "space-between",
height: `${dimensions.height}px`,
padding: "0 8px",
position: "relative",
}}
>
{/* Y-axis labels */}
<div
style={{
position: "absolute",
left: "-40px",
top: "0",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
fontSize: "0.7rem",
color: "#6B7280",
fontWeight: "600",
}}
>
<span>{maxValue.toFixed(0)}</span>
<span>{((maxValue + minValue) / 2).toFixed(0)}</span>
<span>{minValue.toFixed(0)}</span>
</div>
{/* 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 (
<div
key={actualIndex}
style={{
width: "100%",
maxWidth: "20px",
height: `${barHeight}px`,
backgroundColor: isCurrentTime
? "#3B82F6"
: getMetricColor(),
borderRadius: "4px 4px 0 0",
cursor: "pointer",
transition: "all 0.3s ease",
opacity: isHovered ? 1 : isCurrentTime ? 0.9 : 0.7,
transform: isHovered ? "scaleY(1.05)" : "scaleY(1)",
boxShadow: isCurrentTime
? "0 4px 12px rgba(220, 38, 38, 0.4)"
: isHovered
? `0 4px 12px ${getMetricColor()}40`
: "none",
position: "relative",
}}
onMouseEnter={() => setHoveredIndex(actualIndex)}
onMouseLeave={() => setHoveredIndex(null)}
onTouchStart={() => setHoveredIndex(actualIndex)}
>
{/* Value label on hover */}
{isHovered && (
<div
style={{
position: "absolute",
bottom: "100%",
left: "50%",
transform: "translateX(-50%)",
backgroundColor: "#374151",
color: "#FFFFFF",
padding: "4px 8px",
borderRadius: "6px",
fontSize: "0.7rem",
fontWeight: "600",
whiteSpace: "nowrap",
marginBottom: "4px",
zIndex: 10,
}}
>
{value.toFixed(1)} {getMetricUnit()}
<div
style={{
position: "absolute",
top: "100%",
left: "50%",
transform: "translateX(-50%)",
width: 0,
height: 0,
borderLeft: "4px solid transparent",
borderRight: "4px solid transparent",
borderTop: "4px solid #374151",
}}
/>
</div>
)}
</div>
);
})}
</div>
{/* X-axis labels */}
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: "12px",
paddingLeft: "8px",
paddingRight: "8px",
fontSize: "0.7rem",
color: "#6B7280",
fontWeight: "600",
}}
>
<span>0h</span>
<span>6h</span>
<span>12h</span>
<span>18h</span>
<span>24h</span>
</div>
{/* Current time indicator */}
<div
style={{
position: "absolute",
top: "60px",
right: "16px",
backgroundColor: "#3B82F6",
color: "#FFFFFF",
padding: "6px 12px",
borderRadius: "8px",
fontSize: "0.75rem",
fontWeight: "700",
}}
>
Current: {Math.floor(currentTimeOfDay)}:
{String(Math.round((currentTimeOfDay % 1) * 60)).padStart(2, "0")}
</div>
</div>
{/* Detailed info for hovered point */}
{hoveredPoint && (
<div
style={{
marginTop: "16px",
padding: "16px",
backgroundColor: "#F8FAFC",
borderRadius: "12px",
border: "1px solid #E5E7EB",
}}
>
<div
style={{
fontSize: "0.8rem",
fontWeight: "700",
color: getMetricColor(),
marginBottom: "8px",
}}
>
Time: {Math.floor(hoveredPoint.time)}:
{String(Math.round((hoveredPoint.time % 1) * 60)).padStart(2, "0")}
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "8px",
fontSize: "0.75rem",
color: "#374151",
}}
>
<div>Heat: {hoveredPoint.heat.toFixed(1)} units</div>
<div>Power: {hoveredPoint.power.toFixed(1)}W</div>
<div>Efficiency: {hoveredPoint.efficiency.toFixed(1)}%</div>
<div>Energy: {hoveredPoint.cumulative.toFixed(1)}Wh</div>
</div>
</div>
)}
</div>
);
};
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 (
<div
style={{
position: "relative",
width: "100%",
marginBottom: "clamp(24px, 6vw, 32px)",
marginTop: "20px",
opacity: disabled ? 0.6 : 1,
}}
>
<div
style={{
position: "relative",
height: "16px",
background: "linear-gradient(135deg, #F1F5F9 0%, #E2E8F0 100%)",
borderRadius: "8px",
boxShadow: "inset 0 2px 4px rgba(0, 0, 0, 0.1)",
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
top: "0",
left: "0",
height: "100%",
width: `${percentage}%`,
background: "linear-gradient(135deg, #3B82F6 0%, #1D4ED8 100%)",
borderRadius: "8px",
transition: isDragging
? "none"
: "width 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
boxShadow: "0 2px 8px rgba(59, 130, 246, 0.4)",
}}
/>
</div>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => !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}
/>
<div
style={{
position: "absolute",
top: "50%",
left: `${percentage}%`,
width: "28px",
height: "28px",
background: "linear-gradient(135deg, #FFFFFF 0%, #F8FAFC 100%)",
borderRadius: "50%",
transform: "translate(-50%, -50%)",
cursor: disabled ? "not-allowed" : "pointer",
boxShadow: isDragging
? "0 8px 25px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1)"
: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
transition: isDragging
? "none"
: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
zIndex: 6,
border: `3px solid ${isDragging ? "#3B82F6" : "#E5E7EB"}`,
scale: isDragging ? "1.1" : "1",
}}
/>
</div>
);
};
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 (
<div
style={{
marginBottom: "clamp(24px, 6vw, 32px)",
opacity: disabled ? 0.6 : 1,
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "16px",
}}
>
<label
style={{
fontSize: "clamp(1rem, 2.8vw, 1.25rem)",
color: "#111827",
fontWeight: "800",
letterSpacing: "0.025em",
}}
>
{label}
</label>
</div>
{/* Preset Angle Buttons */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "8px",
marginBottom: "20px",
}}
>
{presetAngles.map((preset) => (
<button
key={preset.label}
onClick={() => !disabled && onChange(preset.value)}
disabled={disabled}
style={{
padding: "10px 8px",
fontSize: "0.75rem",
border: "none",
borderRadius: "10px",
backgroundColor:
Math.abs(value - preset.value) < 3 ? "#3B82F6" : "#F8FAFC",
color: Math.abs(value - preset.value) < 3 ? "#FFFFFF" : "#475569",
cursor: disabled ? "not-allowed" : "pointer",
fontWeight: "600",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
boxShadow:
Math.abs(value - preset.value) < 3
? "0 4px 12px rgba(59, 130, 246, 0.4)"
: "0 2px 4px rgba(0,0,0,0.1)",
transform:
Math.abs(value - preset.value) < 3
? "translateY(-1px)"
: "translateY(0)",
}}
>
{preset.label}
</button>
))}
</div>
{/* Modern Stepper Controls */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "12px",
background: "linear-gradient(135deg, #F8FAFC 0%, #F1F5F9 100%)",
padding: "16px",
borderRadius: "20px",
border: "1px solid #E2E8F0",
boxShadow:
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
}}
>
{/* Large Decrease */}
<button
onClick={() => adjustValue(-10)}
disabled={disabled || value <= 0}
style={{
width: "45px",
height: "45px",
border: "none",
borderRadius: "50%",
background: "white",
color: "#374151",
fontSize: "1.2rem",
fontWeight: "700",
cursor: disabled || value <= 0 ? "not-allowed" : "pointer",
opacity: disabled || value <= 0 ? 0.5 : 1,
boxShadow: "0 4px 12px rgba(55, 65, 81,0.3)",
transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
transform: isAdjusting ? "scale(0.95)" : "scale(1)",
}}
>
--
</button>
{/* Small Decrease */}
<button
onClick={() => adjustValue(-1)}
disabled={disabled || value <= 0}
style={{
width: "40px",
height: "40px",
border: "none",
borderRadius: "50%",
background: "white",
color: "#374151",
fontSize: "1rem",
fontWeight: "700",
cursor: disabled || value <= 0 ? "not-allowed" : "pointer",
opacity: disabled || value <= 0 ? 0.5 : 1,
boxShadow: "0 4px 12px rgba(55, 65, 81,0.3)",
transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
transform: isAdjusting ? "scale(0.95)" : "scale(1)",
}}
>
-
</button>
{/* Visual Angle Indicator */}
<div
style={{
width: "80px",
height: "80px",
position: "relative",
background: "linear-gradient(135deg, #FFFFFF 0%, #F9FAFB 100%)",
borderRadius: "50%",
border: "3px solid #E5E7EB",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "inset 0 2px 4px rgba(0, 0, 0, 0.1)",
}}
>
<div
style={{
width: "4px",
height: "30px",
background: "linear-gradient(135deg, #3B82F6 0%, #3B82F6 100%)",
borderRadius: "2px",
transformOrigin: "bottom center",
transform: `rotate(${value}deg)`,
transition: "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
boxShadow: "0 2px 8px rgba(59, 130, 246, 0.4)",
}}
/>
<div
style={{
position: "absolute",
bottom: "50%",
left: "50%",
width: "8px",
height: "8px",
backgroundColor: "#3B82F6",
borderRadius: "50%",
transform: "translate(-50%, 50%)",
boxShadow: "0 0 8px rgba(5, 150, 105, 0.6)",
}}
/>
</div>
{/* Small Increase */}
<button
onClick={() => adjustValue(1)}
disabled={disabled || value >= 90}
style={{
width: "40px",
height: "40px",
border: "none",
borderRadius: "50%",
background: "white",
color: "#374151",
fontSize: "1rem",
fontWeight: "700",
cursor: disabled || value >= 90 ? "not-allowed" : "pointer",
opacity: disabled || value >= 90 ? 0.5 : 1,
boxShadow: "0 4px 12px rgba(55, 65, 81,0.3)",
transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
transform: isAdjusting ? "scale(0.95)" : "scale(1)",
}}
>
+
</button>
{/* Large Increase */}
<button
onClick={() => adjustValue(10)}
disabled={disabled || value >= 90}
style={{
width: "45px",
height: "45px",
border: "none",
borderRadius: "50%",
background: "white",
color: "#374151",
fontSize: "1.2rem",
fontWeight: "700",
cursor: disabled || value >= 90 ? "not-allowed" : "pointer",
opacity: disabled || value >= 90 ? 0.5 : 1,
boxShadow: "0 4px 12px rgba(55, 65, 81,0.3)",
transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
transform: isAdjusting ? "scale(0.95)" : "scale(1)",
}}
>
++
</button>
</div>
{/* Angle Range Indicator */}
<div
style={{
marginTop: "16px",
height: "8px",
background: "linear-gradient(135deg, #F1F5F9 0%, #E2E8F0 100%)",
borderRadius: "4px",
position: "relative",
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
top: "0",
left: "0",
height: "100%",
width: `${(value / 90) * 100}%`,
background: "linear-gradient(90deg, #3B82F6, #3B82F6)",
borderRadius: "4px",
transition: "width 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
<div
style={{
position: "absolute",
top: "50%",
left: `${(value / 90) * 100}%`,
width: "16px",
height: "16px",
backgroundColor: "#3B82F6",
borderRadius: "50%",
transform: "translate(-50%, -50%)",
boxShadow: "0 0 12px rgba(5, 150, 105, 0.6)",
border: "2px solid #FFFFFF",
}}
/>
</div>
</div>
);
};
const SolarSimulator: React.FC = () => {
const [timeOfDay, setTimeOfDay] = useState<number>(12);
const [selectedMaterialKey, setSelectedMaterialKey] = useState<string>(
MATERIAL_KEYS[0]
);
const [panelAngle, setPanelAngle] = useState<number>(30);
const [manualPanelAngle, setManualPanelAngle] = useState<number>(30);
const [deviceOrientationPermission, setDeviceOrientationPermission] =
useState<"prompt" | "granted" | "denied" | null>(null);
const [orientationMode, setOrientationMode] = useState<"manual" | "device">(
"manual"
);
const [isCalibrated, setIsCalibrated] = useState<boolean>(false);
const [calibrationOffset, setCalibrationOffset] = useState<number>(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 (
<div
style={{
fontFamily:
"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
display: "grid",
gridTemplateColumns: "1fr",
gridTemplateRows: "auto",
gap: "clamp(20px, 5vw, 40px)",
width: "100%",
maxWidth: "min(100vw, 1400px)",
minHeight: "100vh",
margin: "0 auto",
padding: "clamp(16px, 4vw, 32px)",
background: "linear-gradient(135deg, #F8FAFC 0%, #E2E8F0 100%)",
color: "#1F2937",
position: "relative",
}}
>
<style>{globalStyles}</style>
<header
style={{
textAlign: "center",
marginBottom: "clamp(20px, 5vw, 32px)",
}}
>
<h1
style={{
color: "#111827",
fontSize: "clamp(1.75rem, 6vw, 3rem)",
margin: "0",
fontWeight: "900",
letterSpacing: "-0.025em",
background:
"linear-gradient(135deg, #3B82F6 0%, #1D4ED8 50%, #7C3AED 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
lineHeight: "1.1",
}}
>
Solar Performance Studio
</h1>
<p
style={{
fontSize: "clamp(0.9rem, 2.8vw, 1.125rem)",
color: "#6B7280",
margin: "12px 0 0 0",
fontWeight: "500",
maxWidth: "600px",
marginLeft: "auto",
marginRight: "auto",
lineHeight: "1.6",
}}
>
Advanced solar panel simulation with real-time analytics and device
integration
</p>
</header>
<section
style={{
width: "100%",
height: "clamp(240px, 40vw, 480px)",
borderRadius: "clamp(20px, 5vw, 40px)",
overflow: "hidden",
boxShadow: `
0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.1)`,
position: "relative",
border: "1px solid rgba(255, 255, 255, 0.2)",
}}
ref={skyViewRef}
>
<SkyBackground timeOfDay={timeOfDay} />
{skyViewDims.width > 0 && (
<SunView
timeOfDay={timeOfDay}
skyWidth={skyViewDims.width}
skyHeight={skyViewDims.height}
/>
)}
</section>
<section
style={{
width: "100%",
height: "clamp(100px, 18vw, 140px)",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "clamp(20px, 5vw, 32px)",
background: "linear-gradient(135deg, #F1F5F9 0%, #E2E8F0 100%)",
boxShadow: `
0 10px 25px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.1)`,
border: "1px solid rgba(255, 255, 255, 0.2)",
}}
>
<SolarPanelView
panelAngle={panelAngle}
materialColor={currentMaterial.color}
/>
</section>
<section
style={{
width: "100%",
padding: "clamp(24px, 6vw, 40px)",
background: "linear-gradient(135deg, #FFFFFF 0%, #F9FAFB 100%)",
borderRadius: "clamp(20px, 5vw, 40px)",
boxShadow: `
0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.1)`,
border: "1px solid rgba(255, 255, 255, 0.2)",
}}
>
<div style={{ marginBottom: "clamp(32px, 7vw, 48px)" }}>
<label
style={{
display: "block",
fontSize: "clamp(1rem, 2.8vw, 1.25rem)",
color: "#111827",
fontWeight: "800",
letterSpacing: "0.025em",
}}
>
Time: {formatTimeValue(timeOfDay)} | Sun Altitude:{" "}
{calculateSunAltitude(timeOfDay).toFixed(1)}°
</label>
<ResponsiveSlider
label="Time of day"
value={timeOfDay}
min={0}
max={24}
step={0.1}
onChange={setTimeOfDay}
formatValue={formatTimeValue}
/>
</div>
<div style={{ marginBottom: "clamp(32px, 7vw, 48px)" }}>
<ModernAngleStepper
label={`Panel Angle: ${panelAngle.toFixed(
1
)}° | Heat Output: ${currentHeat.toFixed(1)} units`}
value={orientationMode === "manual" ? panelAngle : manualPanelAngle}
onChange={
orientationMode === "manual" ? setPanelAngle : setManualPanelAngle
}
disabled={orientationMode === "device"}
/>
</div>
<div style={{ marginBottom: "clamp(24px, 5vw, 32px)" }}>
<label
style={{
display: "block",
fontSize: "clamp(1rem, 2.8vw, 1.25rem)",
color: "#111827",
marginBottom: "clamp(16px, 4vw, 20px)",
fontWeight: "800",
letterSpacing: "0.025em",
}}
>
Control Mode:
</label>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "clamp(12px, 3vw, 16px)",
alignItems: "center",
}}
>
<button
onClick={toggleOrientationMode}
style={{
padding: "clamp(12px, 3vw, 16px) clamp(20px, 5vw, 24px)",
fontSize: "clamp(0.875rem, 2.4vw, 1rem)",
background:
orientationMode === "manual"
? "linear-gradient(135deg, #3B82F6 0%, #3B82F6 100%)"
: "linear-gradient(135deg, #FFFFFF 0%, #F9FAFB 100%)",
color: orientationMode === "manual" ? "#FFFFFF" : "#374151",
cursor: "pointer",
borderRadius: "clamp(10px, 2.5vw, 12px)",
fontWeight: "700",
boxShadow:
orientationMode === "manual"
? "0 10px 25px -3px rgba(59, 130, 246, 0.4), 0 4px 6px -2px rgba(5, 150, 105, 0.2)"
: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
transform:
orientationMode === "manual"
? "translateY(-1px)"
: "translateY(0)",
border:
orientationMode === "manual" ? "none" : "1px solid #E5E7EB",
}}
>
Manual
</button>
<button
onClick={toggleOrientationMode}
style={{
padding: "clamp(12px, 3vw, 16px) clamp(20px, 5vw, 24px)",
fontSize: "clamp(0.875rem, 2.4vw, 1rem)",
background:
orientationMode === "device"
? "linear-gradient(135deg, #3B82F6 0%, #3B82F6 100%)"
: "linear-gradient(135deg, #FFFFFF 0%, #F9FAFB 100%)",
color: orientationMode === "device" ? "#FFFFFF" : "#374151",
cursor: "pointer",
borderRadius: "clamp(10px, 2.5vw, 12px)",
fontWeight: "700",
boxShadow:
orientationMode === "device"
? "0 10px 25px -3px rgba(59, 130, 246, 0.4), 0 4px 6px -2px rgba(5, 150, 105, 0.2)"
: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
transform:
orientationMode === "device"
? "translateY(-1px)"
: "translateY(0)",
opacity: deviceOrientationPermission !== "granted" ? 0.6 : 1,
border:
orientationMode === "device" ? "none" : "1px solid #E5E7EB",
}}
disabled={deviceOrientationPermission !== "granted"}
>
Device Tilt
</button>
{orientationMode === "device" &&
deviceOrientationPermission === "granted" && (
<button
onClick={calibrateDevice}
style={{
padding: "clamp(10px, 2.5vw, 14px) clamp(16px, 4vw, 20px)",
fontSize: "clamp(0.8rem, 2.2vw, 0.9rem)",
border: "none",
background:
"linear-gradient(135deg, #F59E0B 0%, #D97706 100%)",
color: "#FFFFFF",
cursor: "pointer",
borderRadius: "clamp(8px, 2vw, 10px)",
fontWeight: "700",
boxShadow:
"0 8px 25px -3px rgba(245, 158, 11, 0.4), 0 4px 6px -2px rgba(245, 158, 11, 0.2)",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
transform: "translateY(-1px)",
}}
>
Calibrate
</button>
)}
</div>
{deviceOrientationPermission === "granted" && (
<div
style={{
marginTop: "clamp(20px, 5vw, 24px)",
padding: "clamp(20px, 5vw, 24px)",
background: "linear-gradient(135deg, #F8FAFC 0%, #F1F5F9 100%)",
borderRadius: "clamp(16px, 4vw, 20px)",
border: "1px solid #E2E8F0",
boxShadow:
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
}}
>
<div
style={{
fontSize: "clamp(0.8rem, 2.2vw, 0.9rem)",
color: orientationMode === "device" ? "#3B82F6" : "#3B82F6",
fontWeight: "700",
marginBottom: "16px",
textAlign: "center",
}}
>
Tilt Sensor:{" "}
{orientationMode === "device" ? "Active" : "Inactive"} |{" "}
{isCalibrated ? "Calibrated" : "Uncalibrated"}
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: "12px",
fontSize: "clamp(0.75rem, 2vw, 0.8rem)",
color: "#6B7280",
fontWeight: "600",
textAlign: "center",
marginBottom: "16px",
}}
>
<span>Panel: {panelAngle.toFixed(1)}°</span>
<span>Beta: {rawOrientation.beta?.toFixed(1) ?? "N/A"}°</span>
<span>Gamma: {rawOrientation.gamma?.toFixed(1) ?? "N/A"}°</span>
</div>
<div
style={{
width: "100%",
height: "28px",
background:
"linear-gradient(135deg, #F1F5F9 0%, #E2E8F0 100%)",
borderRadius: "14px",
position: "relative",
overflow: "hidden",
boxShadow: "inset 0 2px 4px rgba(0, 0, 0, 0.1)",
}}
>
<div
style={{
position: "absolute",
top: "0",
left: "0",
height: "100%",
width: `${(panelAngle / 90) * 100}%`,
background: "linear-gradient(90deg, #3B82F6, #1D4ED8)",
borderRadius: "14px",
transition: "width 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
boxShadow: "0 2px 8px rgba(59, 130, 246, 0.4)",
}}
/>
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
width: "12px",
height: "12px",
backgroundColor: isCalibrated ? "#3B82F6" : "#D10000",
borderRadius: "50%",
transform: "translate(-50%, -50%)",
boxShadow: `0 0 12px ${
isCalibrated ? "#3B82F6" : "#D10000"
}`,
}}
/>
</div>
</div>
)}
{deviceOrientationPermission === "prompt" && (
<button
onClick={requestDeviceOrientationPermission}
style={{
padding: "clamp(16px, 4vw, 20px)",
fontSize: "clamp(0.9rem, 2.5vw, 1.125rem)",
border: "none",
background: "linear-gradient(135deg, #3B82F6 0%, #3B82F6 100%)",
color: "#FFFFFF",
cursor: "pointer",
width: "100%",
marginTop: "20px",
borderRadius: "clamp(12px, 3vw, 16px)",
fontWeight: "700",
letterSpacing: "0.025em",
boxShadow:
"0 10px 25px -3px rgba(59, 130, 246, 0.4), 0 4px 6px -2px rgba(5, 150, 105, 0.2)",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
transform: "translateY(0)",
}}
onMouseDown={(e) =>
(e.currentTarget.style.transform = "translateY(1px)")
}
onMouseUp={(e) =>
(e.currentTarget.style.transform = "translateY(0)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.transform = "translateY(0)")
}
>
Enable Device Tilt Control
</button>
)}
{deviceOrientationPermission === "denied" && (
<p
style={{
fontSize: "clamp(0.875rem, 2.4vw, 1rem)",
color: "#3B82F6",
textAlign: "center",
fontWeight: "600",
margin: "20px 0 0 0",
padding: "16px",
background: "rgba(220, 38, 38, 0.1)",
borderRadius: "12px",
border: "1px solid rgba(220, 38, 38, 0.2)",
}}
>
Device orientation denied. Check browser settings or use manual
mode.
</p>
)}
</div>
</section>
<section
style={{
width: "100%",
background: "linear-gradient(135deg, #FFFFFF 0%, #F9FAFB 100%)",
borderRadius: "clamp(20px, 5vw, 40px)",
boxShadow: `
0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.1)`,
padding: "clamp(24px, 6vw, 40px)",
border: "1px solid rgba(255, 255, 255, 0.2)",
}}
>
<h2
style={{
fontSize: "clamp(1.25rem, 3.5vw, 1.5rem)",
color: "#111827",
margin: "0 0 clamp(24px, 6vw, 32px) 0",
fontWeight: "800",
letterSpacing: "0.025em",
textAlign: "center",
}}
>
Advanced Performance Analytics
</h2>
<MobileOptimizedGraph
graphData={graphData}
currentTimeOfDay={timeOfDay}
material={currentMaterial}
panelAngle={panelAngle}
/>
</section>
<footer
style={{
textAlign: "center",
padding: "clamp(20px, 5vw, 32px)",
color: "#6B7280",
fontSize: "clamp(0.8rem, 2.2vw, 0.9rem)",
fontWeight: "500",
}}
>
<p style={{ margin: "0", lineHeight: "1.6" }}>
Interactive solar simulation with real-time device orientation support
and advanced analytics
</p>
</footer>
</div>
);
};
export default SolarSimulator;