Skip to content

Commit fcc67a0

Browse files
ismoilovdevmlclaude
andcommitted
feat: Add smooth page animations and improve Top Active Projects algorithm
- Add page-level staggered animations to PipelinesTab - Header animation (100ms delay) - Sidebar slide from left (200ms delay) - Content slide from right (300ms delay) - Statistics cards scale animation (400ms delay) - Chart scale animation (500ms delay) - Filters fade in (600ms delay) - Pipeline cards fade in (700ms delay) - Enhance project list in PipelinesTab - Add project avatars to list items - Add gradient background for selected state - Add pulsing orange indicator for active project - Improve hover effects with scale transform - Optimize Top Active Projects algorithm - Increase pipeline data collection from 10 to ALL projects - Increase pipelines per project from 5 to 10 - Increase total pipelines tracked from 15 to 50 - Add two-tier sorting: by count (primary), then by last activity (secondary) - Add last activity timestamp tracking for each project - More accurate ranking of most active projects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent eef0061 commit fcc67a0

File tree

5 files changed

+321
-111
lines changed

5 files changed

+321
-111
lines changed

src/app/login/page.tsx

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState, useEffect } from 'react';
44
import { useRouter } from 'next/navigation';
5-
import { LogIn, Eye, EyeOff, AlertCircle } from 'lucide-react';
5+
import { LogIn, Eye, EyeOff, AlertCircle, GitBranch, Activity, Zap } from 'lucide-react';
66
import axios from 'axios';
77

88
export default function LoginPage() {
@@ -12,6 +12,12 @@ export default function LoginPage() {
1212
const [showPassword, setShowPassword] = useState(false);
1313
const [error, setError] = useState('');
1414
const [loading, setLoading] = useState(false);
15+
const [mounted, setMounted] = useState(false);
16+
17+
// Mounting animation
18+
useEffect(() => {
19+
setMounted(true);
20+
}, []);
1521

1622
// Check if already logged in
1723
useEffect(() => {
@@ -55,31 +61,49 @@ export default function LoginPage() {
5561
};
5662

5763
return (
58-
<div className="min-h-screen bg-gradient-to-br from-zinc-950 via-zinc-900 to-orange-950 flex items-center justify-center p-4">
59-
<div className="w-full max-w-md">
64+
<div className="min-h-screen bg-gradient-to-br from-zinc-950 via-zinc-900 to-orange-950 flex items-center justify-center p-4 relative overflow-hidden">
65+
{/* Animated Background Elements */}
66+
<div className="absolute inset-0 overflow-hidden pointer-events-none">
67+
<div className="absolute top-1/4 -left-20 w-72 h-72 bg-orange-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDuration: '4s' }} />
68+
<div className="absolute bottom-1/4 -right-20 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDuration: '6s', animationDelay: '1s' }} />
69+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-purple-500/5 rounded-full blur-3xl animate-pulse" style={{ animationDuration: '8s', animationDelay: '2s' }} />
70+
</div>
71+
72+
{/* Floating Icons */}
73+
<div className="absolute inset-0 overflow-hidden pointer-events-none opacity-20">
74+
<GitBranch className="absolute top-20 left-20 w-8 h-8 text-orange-400 animate-float" style={{ animationDelay: '0s', animationDuration: '6s' }} />
75+
<Activity className="absolute top-40 right-32 w-10 h-10 text-blue-400 animate-float" style={{ animationDelay: '1s', animationDuration: '7s' }} />
76+
<Zap className="absolute bottom-32 left-32 w-6 h-6 text-purple-400 animate-float" style={{ animationDelay: '2s', animationDuration: '5s' }} />
77+
<GitBranch className="absolute bottom-20 right-20 w-7 h-7 text-orange-400 animate-float" style={{ animationDelay: '3s', animationDuration: '8s' }} />
78+
</div>
79+
80+
<div className={`w-full max-w-md relative z-10 transition-all duration-1000 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
6081
{/* Logo & Title */}
61-
<div className="text-center mb-8">
62-
<div className="inline-flex items-center justify-center w-16 h-16 bg-orange-500 rounded-2xl mb-4 shadow-lg shadow-orange-500/20">
63-
<LogIn className="w-8 h-8 text-white" />
82+
<div className={`text-center mb-8 transition-all duration-700 delay-100 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-4'}`}>
83+
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-orange-500 to-orange-600 rounded-3xl mb-6 shadow-2xl shadow-orange-500/30 relative group hover:scale-110 transition-transform duration-300">
84+
<div className="absolute inset-0 bg-gradient-to-br from-orange-400 to-orange-600 rounded-3xl blur-xl opacity-50 group-hover:opacity-75 transition-opacity" />
85+
<LogIn className="w-10 h-10 text-white relative z-10 group-hover:rotate-12 transition-transform duration-300" />
6486
</div>
65-
<h1 className="text-3xl font-bold text-white mb-2">GitLab CI/CD Dashboard</h1>
66-
<p className="text-zinc-400">Sign in to your account</p>
87+
<h1 className="text-4xl font-bold text-white mb-3 bg-gradient-to-r from-white to-zinc-300 bg-clip-text text-transparent">
88+
GitLab CI/CD Dashboard
89+
</h1>
90+
<p className="text-zinc-400 text-lg">Sign in to your account</p>
6791
</div>
6892

6993
{/* Login Form */}
70-
<div className="bg-zinc-900 rounded-2xl shadow-xl border border-zinc-800 p-8">
94+
<div className={`bg-zinc-900/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-zinc-800/50 p-8 transition-all duration-700 delay-200 hover:border-zinc-700/50 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
7195
<form onSubmit={handleLogin} className="space-y-6">
7296
{/* Error Message */}
7397
{error && (
74-
<div className="bg-red-950 border border-red-900 rounded-lg p-4 flex items-center gap-3">
75-
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
98+
<div className="bg-red-950/50 backdrop-blur-sm border border-red-900/50 rounded-xl p-4 flex items-center gap-3 animate-shake">
99+
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 animate-pulse" />
76100
<p className="text-sm text-red-400">{error}</p>
77101
</div>
78102
)}
79103

80104
{/* Username */}
81-
<div>
82-
<label htmlFor="username" className="block text-sm font-medium text-zinc-300 mb-2">
105+
<div className="group">
106+
<label htmlFor="username" className="block text-sm font-medium text-zinc-300 mb-2 group-focus-within:text-orange-400 transition-colors">
83107
Username
84108
</label>
85109
<input
@@ -89,13 +113,13 @@ export default function LoginPage() {
89113
onChange={(e) => setUsername(e.target.value)}
90114
placeholder="Enter your username"
91115
required
92-
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all"
116+
className="w-full px-4 py-3.5 bg-zinc-800/50 backdrop-blur-sm border border-zinc-700/50 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent focus:bg-zinc-800 transition-all duration-300 hover:border-zinc-600"
93117
/>
94118
</div>
95119

96120
{/* Password */}
97-
<div>
98-
<label htmlFor="password" className="block text-sm font-medium text-zinc-300 mb-2">
121+
<div className="group">
122+
<label htmlFor="password" className="block text-sm font-medium text-zinc-300 mb-2 group-focus-within:text-orange-400 transition-colors">
99123
Password
100124
</label>
101125
<div className="relative">
@@ -106,12 +130,12 @@ export default function LoginPage() {
106130
onChange={(e) => setPassword(e.target.value)}
107131
placeholder="Enter your password"
108132
required
109-
className="w-full px-4 py-3 pr-12 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all"
133+
className="w-full px-4 py-3.5 pr-12 bg-zinc-800/50 backdrop-blur-sm border border-zinc-700/50 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent focus:bg-zinc-800 transition-all duration-300 hover:border-zinc-600"
110134
/>
111135
<button
112136
type="button"
113137
onClick={() => setShowPassword(!showPassword)}
114-
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-white transition-colors"
138+
className="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-orange-400 transition-all duration-200 hover:scale-110"
115139
>
116140
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
117141
</button>
@@ -122,27 +146,77 @@ export default function LoginPage() {
122146
<button
123147
type="submit"
124148
disabled={loading || !username || !password}
125-
className="w-full bg-orange-500 hover:bg-orange-600 text-white font-semibold py-3 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-orange-500/20 hover:shadow-orange-500/40"
149+
className="group relative w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white font-semibold py-3.5 rounded-xl transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-orange-500/30 hover:shadow-orange-500/50 hover:scale-[1.02] active:scale-[0.98] overflow-hidden"
126150
>
127-
{loading ? 'Signing in...' : 'Sign In'}
151+
<div className="absolute inset-0 bg-gradient-to-r from-orange-400 to-orange-500 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
152+
<div className="relative flex items-center justify-center gap-2">
153+
{loading ? (
154+
<>
155+
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
156+
<span>Signing in...</span>
157+
</>
158+
) : (
159+
<>
160+
<LogIn className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
161+
<span>Sign In</span>
162+
</>
163+
)}
164+
</div>
128165
</button>
129166
</form>
130167

131168
{/* Info */}
132-
<div className="mt-6 pt-6 border-t border-zinc-800">
169+
<div className="mt-6 pt-6 border-t border-zinc-800/50">
133170
<p className="text-sm text-zinc-500 text-center">
134171
Default credentials are set in your environment variables
135172
</p>
136173
</div>
137174
</div>
138175

139176
{/* Footer */}
140-
<div className="mt-8 text-center">
177+
<div className={`mt-8 text-center transition-all duration-700 delay-300 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
141178
<p className="text-sm text-zinc-600">
142179
Built with ❤️ using Next.js & TypeScript
143180
</p>
144181
</div>
145182
</div>
183+
184+
<style jsx>{`
185+
@keyframes float {
186+
0%, 100% {
187+
transform: translateY(0px) translateX(0px);
188+
}
189+
25% {
190+
transform: translateY(-20px) translateX(10px);
191+
}
192+
50% {
193+
transform: translateY(-10px) translateX(-10px);
194+
}
195+
75% {
196+
transform: translateY(-30px) translateX(5px);
197+
}
198+
}
199+
200+
@keyframes shake {
201+
0%, 100% {
202+
transform: translateX(0);
203+
}
204+
10%, 30%, 50%, 70%, 90% {
205+
transform: translateX(-4px);
206+
}
207+
20%, 40%, 60%, 80% {
208+
transform: translateX(4px);
209+
}
210+
}
211+
212+
.animate-float {
213+
animation: float 6s ease-in-out infinite;
214+
}
215+
216+
.animate-shake {
217+
animation: shake 0.5s ease-in-out;
218+
}
219+
`}</style>
146220
</div>
147221
);
148222
}

src/components/Overview.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ export default function Overview() {
5353
setActivePipelines(pipelines);
5454
setStats(pipelineStats);
5555

56-
// Load recent pipelines and active jobs
57-
const recentPromises = projects.slice(0, 10).map(project =>
58-
api.getPipelines(project.id, 1, 5).catch(() => [])
56+
// Load recent pipelines from ALL projects for accurate Top Active Projects
57+
// Increase to get better pipeline count statistics
58+
const recentPromises = projects.map(project =>
59+
api.getPipelines(project.id, 1, 10).catch(() => [])
5960
);
6061
const allRecent = await Promise.all(recentPromises);
6162

@@ -65,7 +66,7 @@ export default function Overview() {
6566
const recent = allRecent
6667
.flat()
6768
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
68-
.slice(0, 15);
69+
.slice(0, 50); // Keep more pipelines for accurate counting
6970
setRecentPipelines(recent);
7071

7172
// Load active jobs from running pipelines
@@ -132,28 +133,46 @@ export default function Overview() {
132133
// eslint-disable-next-line react-hooks/exhaustive-deps
133134
}, [autoRefresh, refreshInterval]);
134135

135-
// Top projects by pipeline count
136+
// Top projects by total pipeline run count (most active)
136137
const topProjects = useMemo(() => {
137138
interface ProjectCount {
138139
project: typeof projects[0];
139140
count: number;
141+
lastActivity: string;
140142
}
141143
const projectPipelineCounts = new Map<number, ProjectCount>();
142144

145+
// Count all recent pipelines per project
143146
recentPipelines.forEach(pipeline => {
144147
const project = projects.find(p => p.id === pipeline.project_id);
145148
if (project) {
146149
const existing = projectPipelineCounts.get(project.id);
147150
if (existing) {
148151
existing.count++;
152+
// Keep track of latest activity
153+
if (new Date(pipeline.updated_at) > new Date(existing.lastActivity)) {
154+
existing.lastActivity = pipeline.updated_at;
155+
}
149156
} else {
150-
projectPipelineCounts.set(project.id, { project, count: 1 });
157+
projectPipelineCounts.set(project.id, {
158+
project,
159+
count: 1,
160+
lastActivity: pipeline.updated_at
161+
});
151162
}
152163
}
153164
});
154165

166+
// Sort by pipeline count (descending), then by last activity (most recent first)
155167
return Array.from(projectPipelineCounts.values())
156-
.sort((a, b) => b.count - a.count)
168+
.sort((a, b) => {
169+
// Primary sort: by count (higher first)
170+
if (b.count !== a.count) {
171+
return b.count - a.count;
172+
}
173+
// Secondary sort: by last activity (more recent first)
174+
return new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime();
175+
})
157176
.slice(0, 5);
158177
}, [recentPipelines, projects]);
159178

0 commit comments

Comments
 (0)