Skip to content

Commit d0c3825

Browse files
committed
feat: add AppLoader and SplineWithLoader components with loading animations
1 parent f55d37c commit d0c3825

File tree

5 files changed

+240
-13
lines changed

5 files changed

+240
-13
lines changed

frontend/app/layout.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import "@/styles/globals.css";
33
import "react-toastify/dist/ReactToastify.css";
44
import ThemeProvider from "../components/ThemeProvider";
55
import ThemeSwitcher from "../components/ThemeSwitcher";
6-
import Spline from "@splinetool/react-spline";
6+
import SplineWithLoader from "../components/SplineWithLoader";
7+
import AppLoader from "../components/AppLoader";
78
import ApiHealthProvider from "../components/ApiHealthProvider";
89
import UpdateProvider from "../components/UpdateProvider";
910
import { useEffect, useState } from "react";
@@ -15,6 +16,7 @@ export default function RootLayout({
1516
children: React.ReactNode;
1617
}>) {
1718
const [performanceMode, setPerformanceMode] = useState(true);
19+
const [isAppLoading, setIsAppLoading] = useState(true);
1820

1921
useEffect(() => {
2022
// Load performance mode preference from localStorage
@@ -43,6 +45,9 @@ export default function RootLayout({
4345
/>
4446
</head>
4547
<body className="min-h-screen bg-mesh" suppressHydrationWarning>
48+
{/* Global app loader */}
49+
{isAppLoading && <AppLoader onLoadComplete={() => setIsAppLoading(false)} />}
50+
4651
<ThemeProvider>
4752
{/* Background Elements */}
4853
<div className="fixed inset-0 w-full h-full overflow-hidden">
@@ -83,7 +88,7 @@ export default function RootLayout({
8388
{/* Spline 3D object (when not in performance mode) */}
8489
{!performanceMode && (
8590
<div className="absolute inset-0">
86-
<Spline
91+
<SplineWithLoader
8792
scene="https://prod.spline.design/0zfiWcHYJLJfg6nt/scene.splinecode"
8893
className="absolute"
8994
/>

frontend/components/AppLoader.tsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"use client";
2+
import { useState, useEffect } from "react";
3+
4+
interface AppLoaderProps {
5+
onLoadComplete?: () => void;
6+
}
7+
8+
export default function AppLoader({ onLoadComplete }: AppLoaderProps) {
9+
const [isVisible, setIsVisible] = useState(true);
10+
const [isFading, setIsFading] = useState(false);
11+
const [progress, setProgress] = useState(0);
12+
13+
useEffect(() => {
14+
// Animate progress bar
15+
const progressInterval = setInterval(() => {
16+
setProgress((prev) => {
17+
if (prev >= 100) {
18+
clearInterval(progressInterval);
19+
return 100;
20+
}
21+
return prev + 2;
22+
});
23+
}, 30);
24+
25+
// Start fade out animation
26+
const fadeTimer = setTimeout(() => {
27+
setIsFading(true);
28+
}, 1800);
29+
30+
// Hide loader after fade out
31+
const hideTimer = setTimeout(() => {
32+
setIsVisible(false);
33+
onLoadComplete?.();
34+
}, 2300);
35+
36+
return () => {
37+
clearInterval(progressInterval);
38+
clearTimeout(fadeTimer);
39+
clearTimeout(hideTimer);
40+
};
41+
}, [onLoadComplete]);
42+
43+
if (!isVisible) return null;
44+
45+
return (
46+
<div className={`fixed inset-0 z-[9999] flex items-center justify-center overflow-hidden transition-opacity duration-500 ${isFading ? 'opacity-0' : 'opacity-100'}`}>
47+
{/* Animated background gradient */}
48+
<div className="absolute inset-0 bg-gradient-to-br from-background via-background to-background">
49+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(34,197,94,0.1),transparent_50%)]"></div>
50+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_80%_20%,rgba(59,130,246,0.1),transparent_50%)]"></div>
51+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(168,85,247,0.1),transparent_50%)]"></div>
52+
</div>
53+
54+
{/* Animated particles */}
55+
<div className="absolute inset-0 overflow-hidden">
56+
{[...Array(20)].map((_, i) => (
57+
<div
58+
key={i}
59+
className="absolute w-1 h-1 bg-green-400/30 rounded-full animate-float"
60+
style={{
61+
left: `${Math.random() * 100}%`,
62+
top: `${Math.random() * 100}%`,
63+
animationDelay: `${Math.random() * 3}s`,
64+
animationDuration: `${3 + Math.random() * 4}s`,
65+
}}
66+
/>
67+
))}
68+
</div>
69+
70+
{/* Main content */}
71+
<div className="relative z-10 flex flex-col items-center gap-8 px-4">
72+
{/* Logo with glow effect */}
73+
<div className="relative">
74+
<div className="absolute inset-0 blur-3xl bg-gradient-to-r from-green-400 to-blue-500 opacity-30 animate-pulse"></div>
75+
<div className="relative">
76+
<h1 className="text-5xl md:text-7xl font-black tracking-tight">
77+
<span className="bg-gradient-to-r from-green-400 via-emerald-400 to-blue-500 bg-clip-text text-transparent animate-gradient-x">
78+
KickViewerBOT
79+
</span>
80+
</h1>
81+
</div>
82+
</div>
83+
84+
{/* Futuristic loader ring */}
85+
<div className="relative w-32 h-32">
86+
{/* Outer ring */}
87+
<div className="absolute inset-0 rounded-full border-2 border-green-400/10"></div>
88+
89+
{/* Spinning rings */}
90+
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-green-400 border-r-green-400 animate-spin"></div>
91+
<div
92+
className="absolute inset-2 rounded-full border-2 border-transparent border-t-blue-400 border-l-blue-400 animate-spin"
93+
style={{ animationDirection: "reverse", animationDuration: "1.5s" }}
94+
></div>
95+
<div
96+
className="absolute inset-4 rounded-full border-2 border-transparent border-b-purple-400 border-r-purple-400 animate-spin"
97+
style={{ animationDuration: "2s" }}
98+
></div>
99+
100+
{/* Center glow */}
101+
<div className="absolute inset-8 rounded-full bg-gradient-to-br from-green-400/20 to-blue-500/20 blur-xl animate-pulse"></div>
102+
</div>
103+
104+
{/* Progress bar */}
105+
<div className="w-64 md:w-80">
106+
<div className="relative h-2 bg-foreground/5 rounded-full overflow-hidden backdrop-blur-sm">
107+
<div
108+
className="absolute inset-y-0 left-0 bg-gradient-to-r from-green-400 via-emerald-400 to-blue-500 rounded-full transition-all duration-300 ease-out"
109+
style={{ width: `${progress}%` }}
110+
>
111+
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer"></div>
112+
</div>
113+
</div>
114+
115+
{/* Progress percentage */}
116+
<div className="flex justify-between items-center mt-3">
117+
<span className="text-xs text-foreground/50 font-medium">Loading...</span>
118+
<span className="text-xs text-foreground/70 font-bold tabular-nums">{progress}%</span>
119+
</div>
120+
</div>
121+
122+
{/* Animated status text */}
123+
<div className="flex items-center gap-3">
124+
<div className="flex gap-1">
125+
{[0, 1, 2].map((i) => (
126+
<div
127+
key={i}
128+
className="w-1.5 h-1.5 bg-green-400 rounded-full animate-bounce"
129+
style={{ animationDelay: `${i * 0.15}s` }}
130+
/>
131+
))}
132+
</div>
133+
<span className="text-sm text-foreground/60 font-medium">
134+
Initializing application
135+
</span>
136+
</div>
137+
</div>
138+
</div>
139+
);
140+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client";
2+
import { useState } from "react";
3+
import Spline from "@splinetool/react-spline";
4+
5+
interface SplineWithLoaderProps {
6+
scene: string;
7+
className?: string;
8+
}
9+
10+
export default function SplineWithLoader({
11+
scene,
12+
className,
13+
}: SplineWithLoaderProps) {
14+
const [isLoading, setIsLoading] = useState(true);
15+
16+
return (
17+
<>
18+
{/* Loader - visible until Spline is loaded */}
19+
{isLoading && (
20+
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm z-[100]">
21+
<div className="flex flex-col items-center gap-4">
22+
{/* Spinner */}
23+
<div className="relative w-16 h-16">
24+
<div className="absolute inset-0 border-4 border-green-400/20 rounded-full"></div>
25+
<div className="absolute inset-0 border-4 border-transparent border-t-green-400 rounded-full animate-spin"></div>
26+
</div>
27+
28+
{/* Loading text */}
29+
<div className="text-foreground/50 text-xs font-medium animate-pulse">
30+
Loading 3D...
31+
</div>
32+
</div>
33+
</div>
34+
)}
35+
36+
{/* Spline component */}
37+
<Spline
38+
scene={scene}
39+
onLoad={() => setIsLoading(false)}
40+
className={className}
41+
/>
42+
</>
43+
);
44+
}

frontend/components/UpdateProvider.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,8 @@ export default function UpdateProvider() {
1414
const { theme } = useTheme();
1515

1616
useEffect(() => {
17-
console.log("UpdateProvider effect triggered:", {
18-
showToast,
19-
hasLatestVersion: !!latestVersion,
20-
latestVersion: latestVersion?.tag_name,
21-
});
2217

2318
if (showToast && latestVersion) {
24-
console.log(
25-
"Displaying update toast for version:",
26-
latestVersion.tag_name
27-
);
2819

2920
const isDark = theme === "dark";
3021

@@ -33,7 +24,6 @@ export default function UpdateProvider() {
3324
<UpdateToastContent
3425
version={latestVersion.tag_name}
3526
onViewDetails={() => {
36-
console.log("View details clicked");
3727
setIsModalOpen(true);
3828
}}
3929
/>,
@@ -57,7 +47,6 @@ export default function UpdateProvider() {
5747
WebkitBackdropFilter: "blur(12px)",
5848
},
5949
onClose: () => {
60-
console.log("Toast closed, dismissing update");
6150
dismissUpdate();
6251
},
6352
}

frontend/styles/globals.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,52 @@
2828
height: 18px;
2929
width: 18px;
3030
}
31+
32+
/* Custom animations for AppLoader */
33+
@keyframes gradient-x {
34+
0%, 100% {
35+
background-position: 0% 50%;
36+
}
37+
50% {
38+
background-position: 100% 50%;
39+
}
40+
}
41+
42+
@keyframes shimmer {
43+
0% {
44+
transform: translateX(-100%);
45+
}
46+
100% {
47+
transform: translateX(100%);
48+
}
49+
}
50+
51+
@keyframes float {
52+
0%, 100% {
53+
transform: translateY(0) translateX(0);
54+
opacity: 0;
55+
}
56+
10% {
57+
opacity: 0.3;
58+
}
59+
50% {
60+
transform: translateY(-30px) translateX(10px);
61+
opacity: 0.5;
62+
}
63+
90% {
64+
opacity: 0.3;
65+
}
66+
}
67+
68+
.animate-gradient-x {
69+
background-size: 200% 200%;
70+
animation: gradient-x 3s ease infinite;
71+
}
72+
73+
.animate-shimmer {
74+
animation: shimmer 2s ease-in-out infinite;
75+
}
76+
77+
.animate-float {
78+
animation: float 6s ease-in-out infinite;
79+
}

0 commit comments

Comments
 (0)