analog-clock-ios-style
Version:
An analog iOS style clock react component with tailwind css
1 lines • 19.2 kB
Source Map (JSON)
{"version":3,"sources":["/Users/montazha/Documents/projects/analag-clock/analag-clock-ios-style/dist/index.mjs","../src/components/AnalogClock.tsx"],"names":["React","useEffect","useRef","useState","AnalogClock","city","offset","currentTime","showToday","bgColorClassName","textColorClassName","bgColorHex","textColorHex","handColorHex","handColorClassName","accentColorHex","accentColorClassName","size","Date","currentTimeState","setCurrentTimeState","animationFrameId","initialTimeRef","initialTimestampRef","performance","now","isClient","setIsClient","current","updateClock","elapsedMilliseconds","newDisplayTime","getTime","requestAnimationFrame","cancelAnimationFrame","getAdjustedTime","adjustedTime","setHours","getHours","hours","minutes","getMinutes","seconds","getSeconds","milliseconds","getMilliseconds","secondDegrees","minuteDegrees","hourDegrees","propHours","isDayTime","bgColor","textColor","effectiveHandColorHex","effectiveAccentColorHex","createElement","className","style","width","height","Array","map","_","i","num","angle","radian","Math","PI","x","cos","y","sin","key","left","top","transform","fontSize","bottom","backgroundColor","transition"],"mappings":"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","sourcesContent":["// src/components/AnalogClock.tsx\nimport React, { useEffect, useRef, useState } from \"react\";\nvar AnalogClock = ({\n city,\n offset = 0,\n currentTime,\n showToday = true,\n bgColorClassName,\n textColorClassName,\n bgColorHex,\n textColorHex,\n handColorHex,\n handColorClassName,\n accentColorHex,\n accentColorClassName,\n size = 128\n // Default size of 128px (32 * 4)\n}) => {\n const [currentTimeState, setCurrentTimeState] = useState(\n new Date(currentTime)\n );\n const animationFrameId = useRef(null);\n const initialTimeRef = useRef(new Date(currentTime));\n const initialTimestampRef = useRef(performance.now());\n const [isClient, setIsClient] = useState(false);\n useEffect(() => {\n setIsClient(true);\n }, []);\n useEffect(() => {\n initialTimeRef.current = new Date(currentTime);\n initialTimestampRef.current = performance.now();\n setCurrentTimeState(new Date(currentTime));\n const updateClock = () => {\n const elapsedMilliseconds = performance.now() - initialTimestampRef.current;\n const newDisplayTime = new Date(\n initialTimeRef.current.getTime() + elapsedMilliseconds\n );\n setCurrentTimeState(newDisplayTime);\n animationFrameId.current = requestAnimationFrame(updateClock);\n };\n animationFrameId.current = requestAnimationFrame(updateClock);\n return () => {\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n }\n };\n }, [currentTime]);\n const getAdjustedTime = () => {\n const adjustedTime2 = new Date(currentTimeState);\n if (offset !== 0) {\n adjustedTime2.setHours(currentTimeState.getHours() + offset);\n }\n return adjustedTime2;\n };\n const adjustedTime = getAdjustedTime();\n const hours = adjustedTime.getHours();\n const minutes = adjustedTime.getMinutes();\n const seconds = adjustedTime.getSeconds();\n const milliseconds = adjustedTime.getMilliseconds();\n const secondDegrees = (seconds + milliseconds / 1e3) / 60 * 360;\n const minuteDegrees = (minutes + (seconds + milliseconds / 1e3) / 60) / 60 * 360;\n const hourDegrees = (hours % 12 + (minutes + (seconds + milliseconds / 1e3) / 60) / 60) / 12 * 360;\n const propHours = currentTime.getHours();\n const isDayTime = propHours >= 6 && propHours < 18;\n const bgColor = bgColorClassName ?? (isDayTime ? \"bg-white\" : \"bg-gray-800\");\n const textColor = textColorClassName ?? (isDayTime ? \"text-black\" : \"text-white\");\n const effectiveHandColorHex = handColorHex ?? (isDayTime ? \"#000\" : \"#fff\");\n const effectiveAccentColorHex = accentColorHex ?? \"#FF9500\";\n if (!isClient) {\n return \"\";\n }\n return /* @__PURE__ */ React.createElement(\n \"div\",\n {\n className: \"flex flex-col items-center\",\n style: { width: `${size * 1.5}px` }\n },\n /* @__PURE__ */ React.createElement(\n \"div\",\n {\n \"data-testid\": \"clock-face\",\n className: `relative ${bgColor} rounded-full flex items-center justify-center shadow-lg mb-2`,\n style: { width: `${size}px`, height: `${size}px` }\n },\n [...Array(12)].map((_, i) => {\n const num = i + 1;\n const angle = num * 30 - 90;\n const radian = angle * Math.PI / 180;\n const x = 50 + 42 * Math.cos(radian);\n const y = 50 + 42 * Math.sin(radian);\n return /* @__PURE__ */ React.createElement(\n \"div\",\n {\n key: num,\n className: `absolute font-semibold ${textColor}`,\n style: {\n left: `${x}%`,\n top: `${y}%`,\n transform: \"translate(-50%, -50%)\",\n fontSize: `${size * 0.1}px`\n }\n },\n num\n );\n }),\n /* @__PURE__ */ React.createElement(\n \"div\",\n {\n \"data-testid\": \"clock-hand\",\n className: `absolute rounded-full origin-bottom ${handColorClassName ?? \"\"}`,\n style: {\n width: `${size * 0.01}px`,\n height: `${size * 0.25}px`,\n bottom: \"50%\",\n transform: `rotate(${hourDegrees}deg)`,\n ...handColorClassName ? {} : { backgroundColor: effectiveHandColorHex }\n }\n }\n ),\n /* @__PURE__ */ React.createElement(\n \"div\",\n {\n \"data-testid\": \"clock-hand\",\n className: `absolute rounded-full origin-bottom ${handColorClassName ?? \"\"}`,\n style: {\n width: `${size * 0.01}px`,\n height: `${size * 0.38}px`,\n bottom: \"50%\",\n transform: `rotate(${minuteDegrees}deg)`,\n ...handColorClassName ? {} : { backgroundColor: effectiveHandColorHex }\n }\n }\n ),\n /* @__PURE__ */ React.createElement(\n \"div\",\n {\n \"data-testid\": \"clock-hand\",\n className: `absolute rounded-full origin-bottom ${accentColorClassName ?? \"\"}`,\n style: {\n width: `${size * 5e-3}px`,\n height: `${size * 0.4}px`,\n bottom: \"50%\",\n transform: `rotate(${secondDegrees}deg)`,\n ...accentColorClassName ? {} : { backgroundColor: effectiveAccentColorHex },\n transition: \"none\"\n }\n }\n ),\n /* @__PURE__ */ React.createElement(\n \"div\",\n {\n \"data-testid\": \"center-dot\",\n className: `absolute rounded-full ${accentColorClassName ?? \"\"}`,\n style: {\n width: `${size * 0.02}px`,\n height: `${size * 0.02}px`,\n ...accentColorClassName ? {} : { backgroundColor: effectiveAccentColorHex }\n }\n }\n )\n ),\n /* @__PURE__ */ React.createElement(\"div\", { className: \"text-center\" }, city && /* @__PURE__ */ React.createElement(\n \"h3\",\n {\n className: \"text-white font-medium\",\n style: { fontSize: `${size * 0.16}px` }\n },\n city\n ), showToday && /* @__PURE__ */ React.createElement(\"p\", { className: \"text-gray-400\", style: { fontSize: `${size * 0.1}px` } }, \"Today\"), offset !== 0 && /* @__PURE__ */ React.createElement(\"p\", { className: \"text-gray-400\", style: { fontSize: `${size * 0.1}px` } }, offset >= 0 ? \"+\" : \"\", offset, \"HRS\"))\n );\n};\nexport {\n AnalogClock\n};\n","import React, { useEffect, useRef, useState } from 'react';\n\nexport interface AnalogClockProps {\n city?: string;\n offset?: number;\n currentTime: Date;\n showToday?: boolean;\n bgColorClassName?: string;\n textColorClassName?: string;\n bgColorHex?: string;\n textColorHex?: string;\n handColorHex?: string;\n handColorClassName?: string;\n accentColorHex?: string;\n accentColorClassName?: string;\n size?: number; // Size in pixels for the clock face\n}\n\nexport const AnalogClock = ({\n city,\n offset = 0,\n currentTime,\n showToday = true,\n bgColorClassName,\n textColorClassName,\n bgColorHex,\n textColorHex,\n handColorHex,\n handColorClassName,\n accentColorHex,\n accentColorClassName,\n size = 128, // Default size of 128px (32 * 4)\n}: AnalogClockProps) => {\n const [currentTimeState, setCurrentTimeState] = useState(\n new Date(currentTime),\n );\n const animationFrameId = useRef<number | null>(null);\n const initialTimeRef = useRef(new Date(currentTime));\n const initialTimestampRef = useRef(performance.now());\n const [isClient, setIsClient] = useState(false);\n\n useEffect(() => {\n setIsClient(true);\n }, []);\n\n // Update time with smooth animation using requestAnimationFrame\n useEffect(() => {\n // Reset refs when currentTime prop changes\n initialTimeRef.current = new Date(currentTime);\n initialTimestampRef.current = performance.now();\n setCurrentTimeState(new Date(currentTime)); // Ensure immediate update to new prop time\n\n const updateClock = () => {\n const elapsedMilliseconds =\n performance.now() - initialTimestampRef.current;\n const newDisplayTime = new Date(\n initialTimeRef.current.getTime() + elapsedMilliseconds,\n );\n setCurrentTimeState(newDisplayTime);\n animationFrameId.current = requestAnimationFrame(updateClock);\n };\n\n animationFrameId.current = requestAnimationFrame(updateClock);\n\n // Clear animation frame on unmount\n return () => {\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n }\n };\n }, [currentTime]); // Rerun effect if currentTime prop changes\n\n const getAdjustedTime = () => {\n const adjustedTime = new Date(currentTimeState);\n if (offset !== 0) {\n adjustedTime.setHours(currentTimeState.getHours() + offset);\n }\n return adjustedTime;\n };\n\n const adjustedTime = getAdjustedTime();\n const hours = adjustedTime.getHours();\n const minutes = adjustedTime.getMinutes();\n const seconds = adjustedTime.getSeconds();\n\n // Calculate angles for clock hands with millisecond precision for smooth movement\n const milliseconds = adjustedTime.getMilliseconds();\n const secondDegrees = ((seconds + milliseconds / 1000) / 60) * 360;\n const minuteDegrees =\n ((minutes + (seconds + milliseconds / 1000) / 60) / 60) * 360;\n const hourDegrees =\n (((hours % 12) + (minutes + (seconds + milliseconds / 1000) / 60) / 60) /\n 12) *\n 360;\n\n // Determine if it's day or night for theming\n const propHours = currentTime.getHours();\n const isDayTime = propHours >= 6 && propHours < 18;\n const bgColor = bgColorClassName ?? (isDayTime ? 'bg-white' : 'bg-gray-800');\n const textColor =\n textColorClassName ?? (isDayTime ? 'text-black' : 'text-white');\n const effectiveHandColorHex = handColorHex ?? (isDayTime ? '#000' : '#fff');\n const effectiveAccentColorHex = accentColorHex ?? '#FF9500'; // iOS orange accent color\n\n if (!isClient) {\n return '';\n }\n\n return (\n <div\n className=\"flex flex-col items-center\"\n style={{ width: `${size * 1.5}px` }}\n >\n {/* Clock face */}\n <div\n data-testid=\"clock-face\"\n className={`relative ${bgColor} rounded-full flex items-center justify-center shadow-lg mb-2`}\n style={{ width: `${size}px`, height: `${size}px` }}\n >\n {/* Clock numbers */}\n {[...Array(12)].map((_, i) => {\n const num = i + 1;\n const angle = num * 30 - 90; // 30 degrees per hour, starting from -90 (12 o'clock)\n const radian = (angle * Math.PI) / 180;\n const x = 50 + 42 * Math.cos(radian);\n const y = 50 + 42 * Math.sin(radian);\n\n return (\n <div\n key={num}\n className={`absolute font-semibold ${textColor}`}\n style={{\n left: `${x}%`,\n top: `${y}%`,\n transform: 'translate(-50%, -50%)',\n fontSize: `${size * 0.1}px`,\n }}\n >\n {num}\n </div>\n );\n })}\n\n {/* Hour hand */}\n <div\n data-testid=\"clock-hand\"\n className={`absolute rounded-full origin-bottom ${handColorClassName ?? ''}`}\n style={{\n width: `${size * 0.01}px`,\n height: `${size * 0.25}px`,\n bottom: '50%',\n transform: `rotate(${hourDegrees}deg)`,\n ...(handColorClassName\n ? {}\n : { backgroundColor: effectiveHandColorHex }),\n }}\n />\n\n {/* Minute hand */}\n <div\n data-testid=\"clock-hand\"\n className={`absolute rounded-full origin-bottom ${handColorClassName ?? ''}`}\n style={{\n width: `${size * 0.01}px`,\n height: `${size * 0.38}px`,\n bottom: '50%',\n transform: `rotate(${minuteDegrees}deg)`,\n ...(handColorClassName\n ? {}\n : { backgroundColor: effectiveHandColorHex }),\n }}\n />\n\n {/* Second hand with smooth transition */}\n <div\n data-testid=\"clock-hand\"\n className={`absolute rounded-full origin-bottom ${accentColorClassName ?? ''}`}\n style={{\n width: `${size * 0.005}px`,\n height: `${size * 0.4}px`,\n bottom: '50%',\n transform: `rotate(${secondDegrees}deg)`,\n ...(accentColorClassName\n ? {}\n : { backgroundColor: effectiveAccentColorHex }),\n transition: 'none',\n }}\n />\n\n {/* Center dot */}\n <div\n data-testid=\"center-dot\"\n className={`absolute rounded-full ${accentColorClassName ?? ''}`}\n style={{\n width: `${size * 0.02}px`,\n height: `${size * 0.02}px`,\n ...(accentColorClassName\n ? {}\n : { backgroundColor: effectiveAccentColorHex }),\n }}\n />\n </div>\n\n {/* City name and timezone */}\n <div className=\"text-center\">\n {city && (\n <h3\n className=\"text-white font-medium\"\n style={{ fontSize: `${size * 0.16}px` }}\n >\n {city}\n </h3>\n )}\n {showToday && (\n <p className=\"text-gray-400\" style={{ fontSize: `${size * 0.1}px` }}>\n Today\n </p>\n )}\n {offset !== 0 && (\n <p className=\"text-gray-400\" style={{ fontSize: `${size * 0.1}px` }}>\n {offset >= 0 ? '+' : ''}\n {offset}HRS\n </p>\n )}\n </div>\n </div>\n );\n};\n"]}