Skip to content

Commit 9014c7e

Browse files
committed
ui: skeleton loading, spring card animation, fix icons to vector, richer background
1 parent 02ce674 commit 9014c7e

2 files changed

Lines changed: 306 additions & 113 deletions

File tree

AndroidApp/App.tsx

Lines changed: 182 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import 'react-native-url-polyfill/auto';
2-
import React, { useEffect, useState } from 'react';
2+
import React, { useEffect, useState, useRef } from 'react';
33
import {
44
StyleSheet,
55
Text,
66
View,
77
FlatList,
88
StatusBar,
9-
ActivityIndicator,
109
RefreshControl,
1110
Dimensions,
11+
Animated,
12+
Easing,
1213
} from 'react-native';
1314
import { fetchApps, AppModel } from './src/config/supabase';
1415
import { AppCard } from './src/components/AppCard';
@@ -17,6 +18,67 @@ import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
1718

1819
const { width, height } = Dimensions.get('window');
1920

21+
// --- Skeleton Card Component ---
22+
const SkeletonCard = () => {
23+
const shimmer = useRef(new Animated.Value(0)).current;
24+
25+
useEffect(() => {
26+
const loop = Animated.loop(
27+
Animated.sequence([
28+
Animated.timing(shimmer, {
29+
toValue: 1,
30+
duration: 1200,
31+
easing: Easing.ease,
32+
useNativeDriver: true,
33+
}),
34+
Animated.timing(shimmer, {
35+
toValue: 0,
36+
duration: 1200,
37+
easing: Easing.ease,
38+
useNativeDriver: true,
39+
}),
40+
])
41+
);
42+
loop.start();
43+
return () => loop.stop();
44+
}, [shimmer]);
45+
46+
const opacity = shimmer.interpolate({
47+
inputRange: [0, 1],
48+
outputRange: [0.3, 0.7],
49+
});
50+
51+
return (
52+
<View style={skeletonStyles.card}>
53+
<Animated.View style={[skeletonStyles.iconBox, { opacity }]} />
54+
<View style={skeletonStyles.textArea}>
55+
<Animated.View style={[skeletonStyles.lineWide, { opacity }]} />
56+
<Animated.View style={[skeletonStyles.lineNarrow, { opacity }]} />
57+
<View style={{ flexDirection: 'row', gap: 6, marginTop: 6 }}>
58+
<Animated.View style={[skeletonStyles.pill, { opacity }]} />
59+
<Animated.View style={[skeletonStyles.pill, { opacity, width: 50 }]} />
60+
</View>
61+
</View>
62+
<Animated.View style={[skeletonStyles.btn, { opacity }]} />
63+
</View>
64+
);
65+
};
66+
67+
const SkeletonList = () => (
68+
<View style={{ paddingTop: StatusBar.currentHeight ? StatusBar.currentHeight + 16 : 56 }}>
69+
{/* Skeleton header */}
70+
<View style={skeletonStyles.header}>
71+
<View>
72+
<View style={[skeletonStyles.lineNarrow, { width: 100, marginBottom: 8 }]} />
73+
<View style={[skeletonStyles.lineWide, { width: 160, height: 26 }]} />
74+
</View>
75+
<View style={skeletonStyles.avatarSkel} />
76+
</View>
77+
<View style={[skeletonStyles.lineNarrow, { width: 80, marginLeft: 22, marginBottom: 16 }]} />
78+
{[0, 1, 2, 3].map(i => <SkeletonCard key={i} />)}
79+
</View>
80+
);
81+
2082
function App(): React.JSX.Element {
2183
const [apps, setApps] = useState<AppModel[]>([]);
2284
const [loading, setLoading] = useState(true);
@@ -52,24 +114,26 @@ function App(): React.JSX.Element {
52114

53115
const closePopup = () => {
54116
setPopupVisible(false);
55-
setTimeout(() => setSelectedApp(null), 300);
117+
setTimeout(() => setSelectedApp(null), 350);
56118
};
57119

58120
const renderHeader = () => (
59121
<View style={styles.header}>
60-
{/* Top bar */}
61122
<View style={styles.topBar}>
62123
<View>
63-
<Text style={styles.greeting}>Welcome back 👋</Text>
124+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
125+
<Text style={styles.greeting}>Welcome back</Text>
126+
<Icon name="hand-wave" size={16} color="#FBBF24" />
127+
</View>
64128
<Text style={styles.title}>Zeeshan Hub</Text>
65129
</View>
66130
<View style={styles.avatar}>
67131
<Text style={styles.avatarLetter}>Z</Text>
68132
</View>
69133
</View>
70134

71-
{/* Section title */}
72135
<View style={styles.sectionRow}>
136+
<Icon name="apps" size={18} color="#A78BFA" />
73137
<Text style={styles.sectionTitle}>Your Apps</Text>
74138
<View style={styles.countBadge}>
75139
<Text style={styles.countText}>{apps.length}</Text>
@@ -82,18 +146,15 @@ function App(): React.JSX.Element {
82146
<View style={styles.container}>
83147
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
84148

85-
{/* Background orbs */}
86-
<View style={styles.orb1} />
87-
<View style={styles.orb2} />
88-
<View style={styles.orb3} />
149+
{/* Layered background for depth */}
150+
<View style={styles.bgBase} />
151+
<View style={styles.bgOrb1} />
152+
<View style={styles.bgOrb2} />
153+
<View style={styles.bgOrb3} />
154+
<View style={styles.bgTopStrip} />
89155

90156
{loading && !refreshing ? (
91-
<View style={styles.loadingWrap}>
92-
<View style={styles.loadingRing}>
93-
<ActivityIndicator size="large" color="#A78BFA" />
94-
</View>
95-
<Text style={styles.loadingText}>Loading apps…</Text>
96-
</View>
157+
<SkeletonList />
97158
) : (
98159
<FlatList
99160
data={apps}
@@ -114,10 +175,10 @@ function App(): React.JSX.Element {
114175
ListEmptyComponent={
115176
<View style={styles.emptyWrap}>
116177
<View style={styles.emptyIcon}>
117-
<Icon name="package-variant" size={56} color="rgba(167, 139, 250, 0.3)" />
178+
<Icon name="package-variant" size={48} color="rgba(167, 139, 250, 0.4)" />
118179
</View>
119180
<Text style={styles.emptyTitle}>No apps yet</Text>
120-
<Text style={styles.emptyDesc}>Upload your first app via the{'\n'}Admin Portal to see it here.</Text>
181+
<Text style={styles.emptyDesc}>Upload your first app via the Admin Portal to see it here.</Text>
121182
</View>
122183
}
123184
/>
@@ -135,58 +196,47 @@ function App(): React.JSX.Element {
135196
const styles = StyleSheet.create({
136197
container: {
137198
flex: 1,
138-
backgroundColor: '#0A0E1A',
199+
backgroundColor: '#080B16',
139200
},
140-
// Background decorative orbs
141-
orb1: {
142-
position: 'absolute',
143-
top: -height * 0.08,
144-
left: -width * 0.25,
145-
width: width * 0.7,
146-
height: width * 0.7,
147-
borderRadius: width * 0.35,
148-
backgroundColor: 'rgba(124, 58, 237, 0.08)',
201+
// Multi-layer background
202+
bgBase: {
203+
...StyleSheet.absoluteFillObject,
204+
backgroundColor: '#080B16',
149205
},
150-
orb2: {
206+
bgOrb1: {
151207
position: 'absolute',
152-
top: height * 0.35,
153-
right: -width * 0.3,
154-
width: width * 0.8,
155-
height: width * 0.8,
156-
borderRadius: width * 0.4,
157-
backgroundColor: 'rgba(56, 189, 248, 0.05)',
208+
top: -height * 0.12,
209+
left: -width * 0.3,
210+
width: width * 0.9,
211+
height: width * 0.9,
212+
borderRadius: width * 0.45,
213+
backgroundColor: 'rgba(124, 58, 237, 0.06)',
158214
},
159-
orb3: {
215+
bgOrb2: {
160216
position: 'absolute',
161-
bottom: -height * 0.1,
162-
left: width * 0.1,
163-
width: width * 0.5,
164-
height: width * 0.5,
165-
borderRadius: width * 0.25,
166-
backgroundColor: 'rgba(167, 139, 250, 0.04)',
217+
top: height * 0.4,
218+
right: -width * 0.35,
219+
width: width,
220+
height: width,
221+
borderRadius: width * 0.5,
222+
backgroundColor: 'rgba(56, 189, 248, 0.035)',
167223
},
168-
// Loading
169-
loadingWrap: {
170-
flex: 1,
171-
justifyContent: 'center',
172-
alignItems: 'center',
173-
},
174-
loadingRing: {
175-
width: 80,
176-
height: 80,
177-
borderRadius: 40,
178-
backgroundColor: 'rgba(124, 58, 237, 0.08)',
179-
justifyContent: 'center',
180-
alignItems: 'center',
181-
marginBottom: 16,
182-
borderWidth: 1,
183-
borderColor: 'rgba(167, 139, 250, 0.15)',
224+
bgOrb3: {
225+
position: 'absolute',
226+
bottom: -height * 0.15,
227+
left: -width * 0.1,
228+
width: width * 0.7,
229+
height: width * 0.7,
230+
borderRadius: width * 0.35,
231+
backgroundColor: 'rgba(167, 139, 250, 0.03)',
184232
},
185-
loadingText: {
186-
color: '#A78BFA',
187-
fontSize: 15,
188-
fontWeight: '600',
189-
letterSpacing: 0.3,
233+
bgTopStrip: {
234+
position: 'absolute',
235+
top: 0,
236+
left: 0,
237+
right: 0,
238+
height: StatusBar.currentHeight ? StatusBar.currentHeight + 80 : 120,
239+
backgroundColor: 'rgba(124, 58, 237, 0.04)',
190240
},
191241
// List
192242
listContent: {
@@ -208,14 +258,14 @@ const styles = StyleSheet.create({
208258
fontSize: 14,
209259
color: '#64748B',
210260
fontWeight: '500',
211-
marginBottom: 4,
212261
letterSpacing: 0.3,
213262
},
214263
title: {
215-
fontSize: 30,
264+
fontSize: 28,
216265
fontWeight: '800',
217266
color: '#F8FAFC',
218267
letterSpacing: 0.3,
268+
marginTop: 4,
219269
},
220270
avatar: {
221271
width: 46,
@@ -242,7 +292,7 @@ const styles = StyleSheet.create({
242292
marginBottom: 4,
243293
},
244294
sectionTitle: {
245-
fontSize: 18,
295+
fontSize: 17,
246296
fontWeight: '700',
247297
color: '#CBD5E1',
248298
letterSpacing: 0.3,
@@ -268,13 +318,13 @@ const styles = StyleSheet.create({
268318
paddingHorizontal: 40,
269319
},
270320
emptyIcon: {
271-
width: 100,
272-
height: 100,
273-
borderRadius: 30,
321+
width: 90,
322+
height: 90,
323+
borderRadius: 28,
274324
backgroundColor: 'rgba(124, 58, 237, 0.06)',
275325
justifyContent: 'center',
276326
alignItems: 'center',
277-
marginBottom: 20,
327+
marginBottom: 18,
278328
borderWidth: 1,
279329
borderColor: 'rgba(167, 139, 250, 0.1)',
280330
},
@@ -292,4 +342,66 @@ const styles = StyleSheet.create({
292342
},
293343
});
294344

345+
const skeletonStyles = StyleSheet.create({
346+
card: {
347+
marginHorizontal: 20,
348+
marginBottom: 14,
349+
borderRadius: 20,
350+
backgroundColor: '#161B2E',
351+
padding: 16,
352+
flexDirection: 'row',
353+
alignItems: 'center',
354+
borderWidth: 1,
355+
borderColor: 'rgba(255, 255, 255, 0.03)',
356+
},
357+
iconBox: {
358+
width: 58,
359+
height: 58,
360+
borderRadius: 16,
361+
backgroundColor: '#1E2440',
362+
},
363+
textArea: {
364+
flex: 1,
365+
marginLeft: 14,
366+
},
367+
lineWide: {
368+
width: '75%',
369+
height: 14,
370+
borderRadius: 7,
371+
backgroundColor: '#1E2440',
372+
marginBottom: 8,
373+
},
374+
lineNarrow: {
375+
width: '45%',
376+
height: 10,
377+
borderRadius: 5,
378+
backgroundColor: '#1E2440',
379+
},
380+
pill: {
381+
width: 40,
382+
height: 18,
383+
borderRadius: 6,
384+
backgroundColor: '#1E2440',
385+
},
386+
btn: {
387+
width: 62,
388+
height: 38,
389+
borderRadius: 12,
390+
backgroundColor: '#1E2440',
391+
},
392+
header: {
393+
paddingHorizontal: 22,
394+
flexDirection: 'row',
395+
justifyContent: 'space-between',
396+
alignItems: 'center',
397+
marginBottom: 28,
398+
},
399+
avatarSkel: {
400+
width: 46,
401+
height: 46,
402+
borderRadius: 15,
403+
backgroundColor: '#1E2440',
404+
},
405+
});
406+
295407
export default App;

0 commit comments

Comments
 (0)