
Что показывает список, почта или галерея, пока в них пусто.
npx astra-rn add empty-collection-v1pnpm dlx astra-rn add empty-collection-v1yarn dlx astra-rn add empty-collection-v1bunx --bun astra-rn add empty-collection-v1npm install expo-asset react-native-reanimated react-native-svgpnpm add expo-asset react-native-reanimated react-native-svgyarn add expo-asset react-native-reanimated react-native-svgbun add expo-asset react-native-reanimated react-native-svgimport React, { memo, useCallback, useEffect, useMemo, useState } from "react";
import {
Image,
Platform,
Pressable,
StyleSheet,
Text,
View,
type ImageSourcePropType,
type ViewStyle,
} from "react-native";
import Animated, {
Easing,
cancelAnimation,
interpolate,
useAnimatedStyle,
useSharedValue,
withDelay,
withRepeat,
withSpring,
withTiming,
} from "react-native-reanimated";
import Svg, { Path } from "react-native-svg";
import type { IEmptyCollectionState, IGlyph, IPhotoCard } from "./types";
import {
ACTION_HEIGHT,
ACTION_HORIZONTAL_PADDING,
CARD_BORDER_WIDTH,
CARD_HEIGHT,
CARD_LAYOUTS,
CARD_RADIUS,
CARD_WIDTH,
COLORS,
CONTENT_HORIZONTAL_PADDING,
DEFAULT_ACTION_LABEL,
DEFAULT_PHOTOS,
DEFAULT_TITLE,
ENTRANCE_SPRING,
ENTRANCE_STAGGER,
ENTRANCE_START_SCALE,
FLOAT_DISTANCE,
FLOAT_DURATION,
FLOAT_STAGGER,
STACK_HEIGHT,
STACK_WIDTH,
} from "./const";
const webLayer = (
Platform.OS === "web" ? { willChange: "transform" } : null
) as ViewStyle | null;
const ArrowRightGlyph: React.FC<IGlyph> = ({
size = 15,
color = COLORS.actionLabel,
}: IGlyph) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
d="M4 12h15M13 6l6 6-6 6"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);
const PhotoCard: React.FC<IPhotoCard> = memo(
({ source, layout, index, animated }: IPhotoCard) => {
const progress = useSharedValue<number>(animated ? 0 : 1);
const drift = useSharedValue<number>(0);
useEffect(() => {
if (!animated) {
cancelAnimation(progress);
cancelAnimation(drift);
progress.value = 1;
drift.value = 0;
return;
}
progress.value = withDelay(
index * ENTRANCE_STAGGER,
withSpring(1, ENTRANCE_SPRING),
);
drift.value = withDelay(
index * FLOAT_STAGGER,
withRepeat(
withTiming(1, {
duration: FLOAT_DURATION,
easing: Easing.inOut(Easing.sin),
}),
-1,
true,
),
);
return () => {
cancelAnimation(progress);
cancelAnimation(drift);
};
}, [animated, drift, index, progress]);
const cardStyle = useAnimatedStyle(() => ({
opacity: progress.value,
transform: [
{ translateX: progress.value * layout.translateX },
{
translateY:
progress.value * layout.translateY +
interpolate(drift.value, [0, 1], [0, -FLOAT_DISTANCE]),
},
{ rotate: `${progress.value * layout.rotate}deg` },
{
scale: interpolate(progress.value, [0, 1], [ENTRANCE_START_SCALE, 1]),
},
],
}));
return (
<Animated.View style={[styles.card, webLayer, cardStyle]}>
{source ? (
<Image
source={source}
style={styles.photo}
resizeMode="cover"
accessibilityIgnoresInvertColors
/>
) : (
<View style={[styles.photo, styles.placeholder]} />
)}
</Animated.View>
);
},
);
PhotoCard.displayName = "PhotoCard";
const EmptyCollectionState: React.FC<IEmptyCollectionState> = ({
title = DEFAULT_TITLE,
actionLabel = DEFAULT_ACTION_LABEL,
photos,
hideAction = false,
animated = true,
style,
onActionPress,
}: IEmptyCollectionState) => {
const [isActionPressed, setIsActionPressed] = useState<boolean>(false);
const handlePressIn = useCallback((): void => setIsActionPressed(true), []);
const handlePressOut = useCallback((): void => setIsActionPressed(false), []);
const cards = useMemo<(ImageSourcePropType | null)[]>(() => {
const sources: ImageSourcePropType[] =
photos ?? DEFAULT_PHOTOS.map((uri) => ({ uri }));
return CARD_LAYOUTS.map((_, index) => sources[index] ?? null);
}, [photos]);
return (
<View style={[styles.container, style]}>
<View style={styles.stack}>
{CARD_LAYOUTS.map((layout, index) => (
<PhotoCard
key={`${layout.rotate}-${layout.translateX}`}
source={cards[index] ?? null}
layout={layout}
index={index}
animated={animated}
/>
))}
</View>
<Text style={styles.title}>{title}</Text>
{hideAction ? null : (
<Pressable
accessibilityRole="button"
accessibilityLabel={actionLabel}
onPress={onActionPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={[styles.action, isActionPressed ? styles.actionPressed : null]}
>
<Text style={styles.actionLabel} numberOfLines={1}>
{actionLabel}
</Text>
<ArrowRightGlyph />
</Pressable>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
backgroundColor: COLORS.screen,
},
stack: {
width: STACK_WIDTH,
height: STACK_HEIGHT,
alignItems: "center",
justifyContent: "center",
},
card: {
position: "absolute",
width: CARD_WIDTH,
height: CARD_HEIGHT,
borderRadius: CARD_RADIUS,
borderWidth: CARD_BORDER_WIDTH,
borderColor: COLORS.card,
backgroundColor: COLORS.card,
shadowColor: "#000000",
shadowOpacity: 0.44,
shadowRadius: 18,
shadowOffset: { width: 0, height: 6 },
elevation: 6,
},
photo: {
flex: 1,
borderRadius: CARD_RADIUS - CARD_BORDER_WIDTH,
},
placeholder: {
backgroundColor: COLORS.placeholder,
},
title: {
maxWidth: 340,
marginTop: 30,
fontSize: 20,
lineHeight: 28,
fontWeight: "500",
letterSpacing: -0.3,
color: COLORS.title,
textAlign: "center",
},
action: {
flexDirection: "row",
alignItems: "center",
gap: 10,
marginTop: 24,
height: ACTION_HEIGHT,
paddingHorizontal: ACTION_HORIZONTAL_PADDING,
borderRadius: ACTION_HEIGHT / 2,
borderWidth: 1,
borderColor: COLORS.actionBorder,
backgroundColor: COLORS.action,
},
actionPressed: {
backgroundColor: COLORS.actionPressed,
},
actionLabel: {
fontSize: 14,
fontWeight: "500",
letterSpacing: -0.1,
color: COLORS.actionLabel,
},
});
export { EmptyCollectionState };
export type {
ICardLayout,
IEmptyCollectionState,
IGlyph,
IPhotoCard,
} from "./types";
export default memo(EmptyCollectionState);import type { ICardLayout } from "./types";
import { PHOTO_SOURCES } from "./photos";
export const DEFAULT_TITLE =
"We operate at the intersection of technology, finance, and wellness.";
export const DEFAULT_ACTION_LABEL = "Find your perfect choice";
export const DEFAULT_PHOTOS = [
PHOTO_SOURCES["empty-collection-v1-1"],
PHOTO_SOURCES["empty-collection-v1-2"],
PHOTO_SOURCES["empty-collection-v1-3"],
] as const;
export const CONTENT_HORIZONTAL_PADDING = 24;
export const STACK_WIDTH = 320;
export const STACK_HEIGHT = 236;
export const CARD_WIDTH = 124;
export const CARD_HEIGHT = 124;
export const CARD_RADIUS = 22;
export const CARD_BORDER_WIDTH = 5.5;
export const CARD_LAYOUTS: ICardLayout[] = [
{ rotate: -2, translateX: -2, translateY: -20 },
{ rotate: -13, translateX: -78, translateY: 12 },
{ rotate: 9, translateX: 76, translateY: 6 },
];
export const ENTRANCE_START_SCALE = 0.88;
export const ENTRANCE_STAGGER = 90;
export const ENTRANCE_SPRING = {
damping: 14,
stiffness: 120,
mass: 0.9,
} as const;
export const FLOAT_DISTANCE = 6;
export const FLOAT_DURATION = 2600;
export const FLOAT_STAGGER = 320;
export const ACTION_HEIGHT = 40;
export const ACTION_HORIZONTAL_PADDING = 18;
export const COLORS = {
screen: "#f7f7f6",
card: "#ffffff",
placeholder: "#e8e8e6",
title: "#1a1a1a",
action: "#1f1f1f",
actionPressed: "#0a0a0a",
actionBorder: "#b9b9b8",
actionLabel: "#f3f1f1",
} as const;import type { ImageSourcePropType, StyleProp, ViewStyle } from "react-native";
export interface IGlyph {
size?: number;
color?: string;
}
export interface ICardLayout {
rotate: number;
translateX: number;
translateY: number;
}
export interface IPhotoCard {
source: ImageSourcePropType | null;
layout: ICardLayout;
index: number;
animated: boolean;
}
export interface IEmptyCollectionState {
title?: string;
actionLabel?: string;
photos?: ImageSourcePropType[];
hideAction?: boolean;
animated?: boolean;
style?: StyleProp<ViewStyle>;
onActionPress?: () => void;
}import { Asset } from "expo-asset";
const uri = (id: number): string => Asset.fromModule(id).uri;
export const PHOTO_SOURCES = {
"empty-collection-v1-1": uri(require("./empty-collection-v1-1.webp")),
"empty-collection-v1-2": uri(require("./empty-collection-v1-2.webp")),
"empty-collection-v1-3": uri(require("./empty-collection-v1-3.webp")),
};Ещё 3 файла с картинками или шрифтами. astra-rn add скачивает их сам, вручную положите их по этим путям: