Skip to content

Commit 078fc6c

Browse files
committed
feat(frontend): Loading skeletons + animations polish (#36)
- Add Skeleton components (RepoCardSkeleton, StatCardSkeleton, etc.) - Add loading state to RepoList with skeleton grid - Add reposLoading state in DashboardHome - Add CSS animations (fadeIn, slideIn, scaleIn) - Add staggered animations for repo cards - Add focus-ring and hover utilities - Smooth page transitions
1 parent 3bdd2d0 commit 078fc6c

4 files changed

Lines changed: 147 additions & 3 deletions

File tree

frontend/src/components/RepoList.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { Repository } from '../types'
2+
import { RepoGridSkeleton } from './ui/Skeleton'
23

34
interface RepoListProps {
45
repos: Repository[]
56
selectedRepo: string | null
67
onSelect: (repoId: string) => void
8+
loading?: boolean
79
}
810

911
// Status indicator with glow effect
@@ -40,7 +42,11 @@ const StatusIndicator = ({ status }: { status: string }) => {
4042
)
4143
}
4244

43-
export function RepoList({ repos, selectedRepo, onSelect }: RepoListProps) {
45+
export function RepoList({ repos, selectedRepo, onSelect, loading }: RepoListProps) {
46+
if (loading) {
47+
return <RepoGridSkeleton count={3} />
48+
}
49+
4450
if (repos.length === 0) {
4551
return (
4652
<div className="bg-[#111113] border border-white/5 rounded-2xl p-16 text-center">
@@ -57,19 +63,20 @@ export function RepoList({ repos, selectedRepo, onSelect }: RepoListProps) {
5763

5864
return (
5965
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
60-
{repos.map((repo) => {
66+
{repos.map((repo, index) => {
6167
const isSelected = selectedRepo === repo.id
6268

6369
return (
6470
<button
6571
key={repo.id}
6672
onClick={() => onSelect(repo.id)}
6773
className={`group relative text-left rounded-2xl p-5 transition-all duration-300
68-
bg-[#111113] border overflow-hidden
74+
bg-[#111113] border overflow-hidden opacity-0 animate-fade-in focus-ring
6975
${isSelected
7076
? 'border-blue-500/50 shadow-lg shadow-blue-500/10'
7177
: 'border-white/5 hover:border-white/10 hover:bg-[#151518]'
7278
}`}
79+
style={{ animationDelay: `${index * 0.05}s` }}
7380
>
7481
{/* Subtle gradient overlay on hover */}
7582
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 via-transparent to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />

frontend/src/components/dashboard/DashboardHome.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function DashboardHome() {
2020
const [selectedRepo, setSelectedRepo] = useState<string | null>(null)
2121
const [activeTab, setActiveTab] = useState<RepoTab>('overview')
2222
const [loading, setLoading] = useState(false)
23+
const [reposLoading, setReposLoading] = useState(true)
2324
const [showPerformance, setShowPerformance] = useState(false)
2425

2526
const fetchRepos = async () => {
@@ -33,6 +34,8 @@ export function DashboardHome() {
3334
setRepos(data.repositories || [])
3435
} catch (error) {
3536
console.error('Error fetching repos:', error)
37+
} finally {
38+
setReposLoading(false)
3639
}
3740
}
3841

@@ -150,6 +153,7 @@ export function DashboardHome() {
150153
<RepoList
151154
repos={repos}
152155
selectedRepo={selectedRepo}
156+
loading={reposLoading}
153157
onSelect={(id) => {
154158
setSelectedRepo(id)
155159
setActiveTab('overview')
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { cn } from '@/lib/utils'
2+
3+
interface SkeletonProps {
4+
className?: string
5+
}
6+
7+
export function Skeleton({ className }: SkeletonProps) {
8+
return (
9+
<div
10+
className={cn(
11+
'animate-pulse rounded-lg bg-white/5',
12+
className
13+
)}
14+
/>
15+
)
16+
}
17+
18+
// Preset skeleton components
19+
export function RepoCardSkeleton() {
20+
return (
21+
<div className="bg-[#111113] border border-white/5 rounded-2xl p-5 space-y-4">
22+
<div className="flex items-center gap-3">
23+
<Skeleton className="w-10 h-10 rounded-xl" />
24+
<div className="flex-1 space-y-2">
25+
<Skeleton className="h-4 w-24" />
26+
<Skeleton className="h-3 w-16" />
27+
</div>
28+
<Skeleton className="h-6 w-20 rounded-full" />
29+
</div>
30+
<div className="space-y-2">
31+
<div className="flex justify-between">
32+
<Skeleton className="h-3 w-28" />
33+
<Skeleton className="h-3 w-12" />
34+
</div>
35+
</div>
36+
</div>
37+
)
38+
}
39+
40+
export function RepoGridSkeleton({ count = 3 }: { count?: number }) {
41+
return (
42+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
43+
{Array.from({ length: count }).map((_, i) => (
44+
<RepoCardSkeleton key={i} />
45+
))}
46+
</div>
47+
)
48+
}
49+
50+
export function StatCardSkeleton() {
51+
return (
52+
<div className="bg-[#0a0a0c] border border-white/5 rounded-xl p-5 space-y-2">
53+
<Skeleton className="h-3 w-20" />
54+
<Skeleton className="h-8 w-16" />
55+
</div>
56+
)
57+
}
58+
59+
export function SearchResultSkeleton() {
60+
return (
61+
<div className="bg-[#0a0a0c] border border-white/5 rounded-xl p-5 space-y-4">
62+
<div className="flex items-start justify-between">
63+
<div className="space-y-2">
64+
<Skeleton className="h-4 w-32" />
65+
<Skeleton className="h-3 w-48" />
66+
</div>
67+
<Skeleton className="h-6 w-12" />
68+
</div>
69+
<Skeleton className="h-32 w-full rounded-lg" />
70+
<Skeleton className="h-3 w-40" />
71+
</div>
72+
)
73+
}
74+
75+
export function TableSkeleton({ rows = 5 }: { rows?: number }) {
76+
return (
77+
<div className="space-y-2">
78+
{Array.from({ length: rows }).map((_, i) => (
79+
<div key={i} className="flex items-center gap-4 p-3 bg-white/5 rounded-lg">
80+
<Skeleton className="h-4 flex-1" />
81+
<Skeleton className="h-4 w-16" />
82+
</div>
83+
))}
84+
</div>
85+
)
86+
}

frontend/src/index.css

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,54 @@
130130
background: rgba(59, 130, 246, 0.2);
131131
}
132132

133+
/* Page transitions and animations */
134+
@keyframes fadeIn {
135+
from { opacity: 0; transform: translateY(8px); }
136+
to { opacity: 1; transform: translateY(0); }
137+
}
138+
139+
@keyframes slideIn {
140+
from { opacity: 0; transform: translateX(-8px); }
141+
to { opacity: 1; transform: translateX(0); }
142+
}
143+
144+
@keyframes scaleIn {
145+
from { opacity: 0; transform: scale(0.95); }
146+
to { opacity: 1; transform: scale(1); }
147+
}
148+
149+
.animate-fade-in {
150+
animation: fadeIn 0.3s ease-out forwards;
151+
}
152+
153+
.animate-slide-in {
154+
animation: slideIn 0.3s ease-out forwards;
155+
}
133156

157+
.animate-scale-in {
158+
animation: scaleIn 0.2s ease-out forwards;
159+
}
160+
161+
/* Staggered animations for lists */
162+
.stagger-1 { animation-delay: 0.05s; }
163+
.stagger-2 { animation-delay: 0.1s; }
164+
.stagger-3 { animation-delay: 0.15s; }
165+
.stagger-4 { animation-delay: 0.2s; }
166+
.stagger-5 { animation-delay: 0.25s; }
167+
168+
/* Focus visible states */
169+
.focus-ring {
170+
@apply focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-[#09090b];
171+
}
172+
173+
/* Smooth hover transitions */
174+
.hover-lift {
175+
@apply transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg;
176+
}
177+
178+
.hover-glow {
179+
@apply transition-all duration-200 hover:shadow-[0_0_20px_rgba(59,130,246,0.3)];
180+
}
134181

135182
@layer base {
136183
* {

0 commit comments

Comments
 (0)