feat: add navigation with tab bar and screen layouts
Expo Router file-based navigation with glass tab bar, 3 tabs (Créations, Explorer, Profil), Phosphor icons, modal picker, and fade transitions.
This commit is contained in:
232
app/(tabs)/index.tsx
Normal file
232
app/(tabs)/index.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
Dimensions,
|
||||
FlatList,
|
||||
Image,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useRouter } from "expo-router";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
import { Plus, ImageSquare } from "phosphor-react-native";
|
||||
import { GlassCard, GlassButton } from "@/components/glass";
|
||||
import { useWallpaperStore } from "@/stores/wallpaper.store";
|
||||
import { colors, typography, spacing, layout } from "@/theme";
|
||||
import { ANIMATION_META, type WallpaperConfig } from "@/types/wallpaper";
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get("window");
|
||||
const CARD_SIZE = (SCREEN_WIDTH - layout.screenPadding * 2 - spacing.md) / 2;
|
||||
|
||||
export default function HomeScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
const savedWallpapers = useWallpaperStore((s) => s.savedWallpapers);
|
||||
|
||||
const handleNewWallpaper = useCallback(() => {
|
||||
router.push("/picker");
|
||||
}, [router]);
|
||||
|
||||
const handleOpenWallpaper = useCallback(
|
||||
(config: WallpaperConfig) => {
|
||||
const store = useWallpaperStore.getState();
|
||||
store.setSourceImage(config.sourceUri);
|
||||
store.setAnimation(config.animation);
|
||||
Object.entries(config.uniforms).forEach(([key, value]) => {
|
||||
store.setUniform(key, value);
|
||||
});
|
||||
router.push(`/editor/${config.id}`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top + spacing.md }]}>
|
||||
{/* Header */}
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(500)}
|
||||
style={styles.header}
|
||||
>
|
||||
<GlassCard variant="subtle" radius="lg" padding={16}>
|
||||
<View style={styles.headerRow}>
|
||||
<View>
|
||||
<Text style={typography.title}>Lively</Text>
|
||||
<Text style={[typography.caption, { marginTop: 2 }]}>
|
||||
Vos fonds d'écran animés
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</GlassCard>
|
||||
</Animated.View>
|
||||
|
||||
{/* Content */}
|
||||
{savedWallpapers.length === 0 ? (
|
||||
<EmptyState onPress={handleNewWallpaper} />
|
||||
) : (
|
||||
<FlatList
|
||||
data={savedWallpapers}
|
||||
numColumns={2}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.grid}
|
||||
columnWrapperStyle={styles.row}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListHeaderComponent={
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={typography.label}>Récentes</Text>
|
||||
<Text style={[typography.caption, { color: colors.text.secondary }]}>
|
||||
{savedWallpapers.length} création{savedWallpapers.length > 1 ? "s" : ""}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
renderItem={({ item, index }) => (
|
||||
<WallpaperCard
|
||||
config={item}
|
||||
index={index}
|
||||
onPress={() => handleOpenWallpaper(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Floating CTA */}
|
||||
{savedWallpapers.length > 0 && (
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(300).duration(400)}
|
||||
style={[styles.fabContainer, { bottom: layout.tabBarHeight + insets.bottom + 16 }]}
|
||||
>
|
||||
<GlassButton
|
||||
label="Nouveau"
|
||||
onPress={handleNewWallpaper}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
pulse
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState({ onPress }: { onPress: () => void }) {
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(200).duration(600)}
|
||||
style={styles.emptyContainer}
|
||||
>
|
||||
<GlassCard variant="medium" radius="xl" padding={32}>
|
||||
<View style={styles.emptyContent}>
|
||||
<Text style={[typography.heading, { textAlign: "center" }]}>
|
||||
Donnez vie à vos photos
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
typography.bodySecondary,
|
||||
{ textAlign: "center", marginTop: 8, marginBottom: 24 },
|
||||
]}
|
||||
>
|
||||
Choisissez une image et appliquez{"\n"}une animation en quelques taps
|
||||
</Text>
|
||||
<GlassButton
|
||||
label="Choisir une photo"
|
||||
onPress={onPress}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
pulse
|
||||
/>
|
||||
</View>
|
||||
</GlassCard>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
function WallpaperCard({
|
||||
config,
|
||||
index,
|
||||
onPress,
|
||||
}: {
|
||||
config: WallpaperConfig;
|
||||
index: number;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const meta = ANIMATION_META[config.animation];
|
||||
|
||||
return (
|
||||
<Animated.View entering={FadeInDown.delay(index * 80).duration(400)}>
|
||||
<Pressable onPress={onPress}>
|
||||
<GlassCard variant="subtle" radius="lg" noPadding>
|
||||
<Image
|
||||
source={{ uri: config.sourceUri }}
|
||||
style={styles.cardImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<View style={styles.cardOverlay}>
|
||||
<Text style={styles.cardLabel}>{meta.label}</Text>
|
||||
</View>
|
||||
</GlassCard>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
paddingHorizontal: layout.screenPadding,
|
||||
},
|
||||
header: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
grid: {
|
||||
paddingBottom: 160,
|
||||
},
|
||||
row: {
|
||||
gap: spacing.md,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
emptyContent: {
|
||||
alignItems: "center",
|
||||
},
|
||||
cardImage: {
|
||||
width: CARD_SIZE,
|
||||
height: CARD_SIZE * 1.4,
|
||||
borderRadius: 24,
|
||||
},
|
||||
cardOverlay: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: 12,
|
||||
borderBottomLeftRadius: 24,
|
||||
borderBottomRightRadius: 24,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||
},
|
||||
cardLabel: {
|
||||
...typography.caption,
|
||||
color: colors.text.primary,
|
||||
fontWeight: "600",
|
||||
},
|
||||
fabContainer: {
|
||||
position: "absolute",
|
||||
left: layout.screenPadding,
|
||||
right: layout.screenPadding,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user