|
| 1 | +"use client" |
| 2 | + |
1 | 3 | import { cn } from "@/lib/utils" |
| 4 | +import Lottie from "lottie-react" |
| 5 | +import { useEffect, useState, useRef } from "react" |
| 6 | + |
| 7 | +// Animation cache to prevent multiple fetches of the same file |
| 8 | +const animationCache = new Map<string, any>() |
| 9 | + |
| 10 | +// Preload the default animation |
| 11 | +if (typeof window !== "undefined") { |
| 12 | + fetch("/loading.json") |
| 13 | + .then((response) => response.json()) |
| 14 | + .then((data) => { |
| 15 | + animationCache.set("/loading.json", data) |
| 16 | + }) |
| 17 | + .catch((error) => console.error("Error preloading animation:", error)) |
| 18 | +} |
2 | 19 |
|
3 | 20 | interface LoadingSpinnerProps { |
4 | | - text?: string |
5 | 21 | size?: "sm" | "md" | "lg" |
6 | | - showText?: boolean |
7 | 22 | className?: string |
| 23 | + animationFile?: string |
| 24 | + logoFill?: string |
8 | 25 | } |
9 | 26 |
|
10 | 27 | export const LoadingSpinner = ({ |
11 | | - text = "Loading...", |
12 | | - size = "md", |
13 | | - showText = true, |
| 28 | + size = "sm", |
14 | 29 | className, |
| 30 | + animationFile = "/loading.json", |
| 31 | + logoFill = "currentColor", |
15 | 32 | }: LoadingSpinnerProps) => { |
16 | | - const sizeClasses = { |
17 | | - sm: "w-3 h-3", |
18 | | - md: "w-4 h-4", |
19 | | - lg: "w-6 h-6", |
| 33 | + const [animationData, setAnimationData] = useState<any>( |
| 34 | + animationCache.get(animationFile) || null, |
| 35 | + ) |
| 36 | + const [lottieReady, setLottieReady] = useState(false) |
| 37 | + const hasFetchedRef = useRef(false) |
| 38 | + |
| 39 | + // Унифицированные размеры для SVG и Lottie |
| 40 | + const sizeMap = { |
| 41 | + sm: { className: "w-8 h-8", style: { width: "2rem", height: "2rem" } }, |
| 42 | + md: { className: "w-12 h-12", style: { width: "3rem", height: "3rem" } }, |
| 43 | + lg: { className: "w-16 h-16", style: { width: "4rem", height: "4rem" } }, |
20 | 44 | } |
21 | 45 |
|
| 46 | + useEffect(() => { |
| 47 | + // Skip fetch if we already have the animation in cache |
| 48 | + if (animationCache.has(animationFile)) { |
| 49 | + setAnimationData(animationCache.get(animationFile)) |
| 50 | + return |
| 51 | + } |
| 52 | + |
| 53 | + // Skip duplicate fetches |
| 54 | + if (hasFetchedRef.current) return |
| 55 | + hasFetchedRef.current = true |
| 56 | + |
| 57 | + // We'll use the JSON file as default |
| 58 | + if (animationFile && animationFile.endsWith(".json")) { |
| 59 | + fetch(animationFile) |
| 60 | + .then((response) => response.json()) |
| 61 | + .then((data) => { |
| 62 | + // Cache the animation data |
| 63 | + animationCache.set(animationFile, data) |
| 64 | + setAnimationData(data) |
| 65 | + }) |
| 66 | + .catch((error) => |
| 67 | + console.error("Error loading Lottie animation:", error), |
| 68 | + ) |
| 69 | + } |
| 70 | + }, [animationFile]) |
| 71 | + |
| 72 | + // Set lottieReady to true after a small delay when animation data is available |
| 73 | + useEffect(() => { |
| 74 | + if (animationData) { |
| 75 | + const timer = setTimeout(() => { |
| 76 | + setLottieReady(true) |
| 77 | + }, 300) // Small delay to ensure Lottie is rendered before fading in |
| 78 | + return () => clearTimeout(timer) |
| 79 | + } |
| 80 | + }, [animationData]) |
| 81 | + |
22 | 82 | return ( |
23 | 83 | <div |
24 | 84 | className={cn( |
25 | 85 | "w-full h-full flex items-center justify-center bg-background", |
26 | 86 | className, |
27 | 87 | )} |
28 | 88 | > |
29 | | - <div className={cn("relative", sizeClasses[size])}> |
30 | | - <div className="absolute inset-0 bg-foreground/30 rounded-full animate-pulse-slow"></div> |
31 | | - <div className="absolute inset-0 bg-foreground rounded-full animate-pulse-fast"></div> |
| 89 | + <div |
| 90 | + className={cn( |
| 91 | + sizeMap[size].className, |
| 92 | + "flex items-center justify-center relative", |
| 93 | + )} |
| 94 | + > |
| 95 | + {/* Always render both with absolute positioning for smooth transition */} |
| 96 | + <div |
| 97 | + className={cn( |
| 98 | + "w-full h-full animate-spin-slow absolute inset-0 transition-opacity duration-300", |
| 99 | + lottieReady ? "opacity-0" : "opacity-100", |
| 100 | + )} |
| 101 | + > |
| 102 | + <svg |
| 103 | + width="100%" |
| 104 | + height="100%" |
| 105 | + viewBox="0 0 60 60" |
| 106 | + fill="none" |
| 107 | + xmlns="http://www.w3.org/2000/svg" |
| 108 | + aria-label="21st logo loading" |
| 109 | + > |
| 110 | + <path |
| 111 | + d="M0 20C0 8.95431 8.95431 0 20 0C31.0457 0 40 8.95431 40 20C40 31.0457 31.5 35.5 20 40H40C40 51.0457 31.0457 60 20 60C8.95431 60 0 51.0457 0 40C0 28.9543 9.5 22 20 20H0Z" |
| 112 | + fill={logoFill} |
| 113 | + /> |
| 114 | + <path |
| 115 | + d="M40 60C51.7324 55.0977 60 43.5117 60 30C60 16.4883 51.7324 4.90234 40 0V60Z" |
| 116 | + fill={logoFill} |
| 117 | + /> |
| 118 | + </svg> |
| 119 | + </div> |
| 120 | + |
| 121 | + {animationData && ( |
| 122 | + <div |
| 123 | + className={cn( |
| 124 | + "w-full h-full absolute inset-0 transition-opacity duration-300", |
| 125 | + lottieReady ? "opacity-100" : "opacity-0", |
| 126 | + )} |
| 127 | + > |
| 128 | + <Lottie |
| 129 | + animationData={animationData} |
| 130 | + loop={true} |
| 131 | + style={{ width: "100%", height: "100%" }} |
| 132 | + rendererSettings={{ |
| 133 | + preserveAspectRatio: "xMidYMid slice", |
| 134 | + progressiveLoad: true, |
| 135 | + }} |
| 136 | + /> |
| 137 | + </div> |
| 138 | + )} |
32 | 139 | </div> |
33 | | - {showText && <span className="ml-3 text-foreground">{text}</span>} |
34 | 140 | </div> |
35 | 141 | ) |
36 | 142 | } |
37 | 143 |
|
38 | 144 | export const LoadingSpinnerPage = ({ |
39 | | - text, |
40 | 145 | size, |
41 | | - showText, |
42 | 146 | className, |
| 147 | + animationFile, |
| 148 | + logoFill, |
43 | 149 | }: LoadingSpinnerProps) => ( |
44 | 150 | <div |
45 | 151 | className={cn( |
46 | 152 | "w-full h-screen flex items-center justify-center bg-background", |
47 | 153 | className, |
48 | 154 | )} |
49 | 155 | > |
50 | | - <LoadingSpinner text={text} size={size} showText={showText} /> |
| 156 | + <LoadingSpinner |
| 157 | + size={size} |
| 158 | + animationFile={animationFile} |
| 159 | + logoFill={logoFill} |
| 160 | + /> |
51 | 161 | </div> |
52 | 162 | ) |
0 commit comments