import React from 'react';
import { View, Text, Image, Actionable, Flexbox, Icon } from '@evlop/native-components';
import { useProduct, PriceDisplay } from '@evlop/shopify';
import { Animated, Easing, StyleSheet, Pressable } from 'react-native';
interface ProductImage {
id: string;
originalSrc: string;
altText: string;
}
interface Product {
id: string;
title: string;
handle: string;
images: ProductImage[];
price?: string;
}
interface Annotation {
id: string;
x: number;
y: number;
width: number;
height: number;
product: Product;
}
interface ImageData {
url: string;
aspectRatio: number;
annotations: Annotation[];
}
interface AppBlockProps {
data: {
images: ImageData[];
};
}
const HOTSPOT_SIZE = 20;
const IMAGE_SIZE = 60;
const RING_COUNT = 3;
const ACTIVE_COLOR = '#4CD964';
const NAV_BUTTON_SIZE = 44;
const IMAGE_TRANSITION_MS = 400;
// ---------------------------------------------------------
// Animated pulse ring
// ---------------------------------------------------------
interface PulseRingProps {
delay: number;
isActive: boolean;
}
const PulseRing: React.FC<PulseRingProps> = ({ delay, isActive }) => {
const progress = React.useRef(new Animated.Value(0)).current;
const loopRef = React.useRef<Animated.CompositeAnimation | null>(null);
const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
React.useEffect(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (loopRef.current) {
loopRef.current.stop();
loopRef.current = null;
}
if (isActive) {
timerRef.current = setTimeout(() => {
progress.setValue(0);
loopRef.current = Animated.loop(
Animated.sequence([
Animated.timing(progress, {
toValue: 1,
duration: 2000,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(progress, {
toValue: 0,
duration: 2000,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
]),
);
loopRef.current.start();
}, delay);
} else {
Animated.timing(progress, {
toValue: 0,
duration: 300,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}).start();
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (loopRef.current) {
loopRef.current.stop();
loopRef.current = null;
}
};
}, [delay, isActive, progress]);
const scale = progress.interpolate({
inputRange: [0, 1],
outputRange: [1, 3.5],
});
const opacity = progress.interpolate({
inputRange: [0, 0.3, 0.7, 1],
outputRange: [0.7, 0.5, 0.2, 0],
});
return (
<Animated.View
style={[
styles.pulseRing,
{
opacity,
transform: [{ scale }],
},
]}
/>
);
};
// ---------------------------------------------------------
// Hotspot + product card
// ---------------------------------------------------------
interface HotspotWithCardProps {
annotation: Annotation;
index: number;
imageWidth: number;
imageHeight: number;
isActive: boolean;
onHotspotPress: (index: number) => void;
}
const HotspotWithCard: React.FC<HotspotWithCardProps> = ({
annotation,
index,
imageWidth,
imageHeight,
isActive,
onHotspotPress,
}) => {
const [showProduct, setShowProduct] = React.useState(false);
const product = useProduct(
annotation.product.handle
? { productHandle: annotation.product.handle }
: { productId: annotation.product.id },
);
const contentX = (annotation.x + annotation.width / 2) * imageWidth;
const contentY = (annotation.y + annotation.height / 2) * imageHeight;
const annotationWidth = annotation.width * imageWidth;
const leftSpace = contentX;
const rightSpace = imageWidth - contentX;
const isLeft = leftSpace > rightSpace;
const gap = 8;
const halfAnnotationWidth = annotationWidth / 2;
const productX = isLeft
? -(halfAnnotationWidth + gap + IMAGE_SIZE)
: halfAnnotationWidth + gap;
const productY = -IMAGE_SIZE / 2;
const lineWidth = halfAnnotationWidth + gap;
// Idle pulse
const idlePulse = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(idlePulse, {
toValue: 1,
duration: 1500,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(idlePulse, {
toValue: 0,
duration: 1500,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
]),
);
animation.start();
return () => animation.stop();
}, [idlePulse]);
// Active state
const activeScale = React.useRef(new Animated.Value(1)).current;
const activeGlow = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
Animated.parallel([
Animated.timing(activeScale, {
toValue: isActive ? 1.2 : 1,
duration: isActive ? 300 : 250,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(activeGlow, {
toValue: isActive ? 1 : 0,
duration: isActive ? 400 : 300,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
]).start();
}, [activeGlow, activeScale, isActive]);
const lineProgress = React.useRef(new Animated.Value(0)).current;
const productProgress = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
let lineTimer: ReturnType<typeof setTimeout> | null = null;
let productTimer: ReturnType<typeof setTimeout> | null = null;
let hideTimer: ReturnType<typeof setTimeout> | null = null;
if (isActive) {
setShowProduct(true);
lineProgress.stopAnimation();
productProgress.stopAnimation();
lineProgress.setValue(0);
productProgress.setValue(0);
lineTimer = setTimeout(() => {
Animated.timing(lineProgress, {
toValue: 1,
duration: 250,
easing: Easing.out(Easing.quad),
useNativeDriver: false,
}).start();
}, 300);
productTimer = setTimeout(() => {
Animated.timing(productProgress, {
toValue: 1,
duration: 400,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}).start();
}, 550);
} else {
Animated.timing(productProgress, {
toValue: 0,
duration: 200,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}).start();
lineTimer = setTimeout(() => {
Animated.timing(lineProgress, {
toValue: 0,
duration: 180,
easing: Easing.inOut(Easing.quad),
useNativeDriver: false,
}).start();
}, 100);
hideTimer = setTimeout(() => {
setShowProduct(false);
}, 300);
}
return () => {
if (lineTimer) clearTimeout(lineTimer);
if (productTimer) clearTimeout(productTimer);
if (hideTimer) clearTimeout(hideTimer);
};
}, [isActive, lineProgress, productProgress]);
const idleScale = idlePulse.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.08],
});
const combinedScale = Animated.multiply(idleScale, activeScale);
const glowScale = activeGlow.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.1],
});
const glowOpacity = activeGlow.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.4],
});
const glowBackdropScale = activeGlow.interpolate({
inputRange: [0, 1],
outputRange: [0.8, 1],
});
const productScale = productProgress.interpolate({
inputRange: [0, 1],
outputRange: [0.3, 1],
});
const productOpacity = productProgress.interpolate({
inputRange: [0, 0.3, 1],
outputRange: [0, 0.4, 1],
});
// Animate width instead of scaleX.
// This avoids needing transformOrigin and draws from the hotspot outward.
const animatedLineWidth = lineProgress.interpolate({
inputRange: [0, 1],
outputRange: [0, lineWidth],
});
const lineOpacity = lineProgress.interpolate({
inputRange: [0, 0.2, 1],
outputRange: [0, 1, 1],
});
return (
<View
style={[
styles.overlayContainer,
{
left: contentX,
top: contentY,
},
]}
pointerEvents="box-none"
>
<Pressable
onPress={() => onHotspotPress(index)}
style={styles.hotspotPressable}
>
<View style={styles.hotspotWrapper}>
<View style={styles.ringsContainer}>
{[...Array(RING_COUNT)].map((_, i) => (
<PulseRing
key={i}
delay={i * 400}
isActive={isActive}
/>
))}
</View>
<Animated.View
style={[
styles.hotspotGlow,
{
opacity: glowOpacity,
transform: [{ scale: glowBackdropScale }],
},
]}
/>
<Animated.View
style={[
styles.hotspotOuter,
{
transform: [{ scale: combinedScale }],
},
isActive && styles.hotspotOuterActive,
]}
>
<Animated.View
style={[
styles.hotspotInner,
{
transform: [{ scale: glowScale }],
},
isActive && styles.hotspotInnerActive,
]}
/>
</Animated.View>
</View>
</Pressable>
{showProduct && (
<Animated.View
style={[
styles.connectionLine,
{
width: animatedLineWidth,
opacity: lineOpacity,
top: -1,
...(isLeft ? styles.connectionLineLeft : styles.connectionLineRight),
},
]}
/>
)}
{showProduct && (
<Animated.View
style={[
styles.productContainer,
{
left: productX,
top: productY,
opacity: productOpacity,
transform: [{ scale: productScale }],
},
]}
>
<Actionable
action={product?.actions?.openDetailsPage}
hapticFeedback="impactLight"
pressEffect="pop"
>
<Flexbox flexDirection="column" alignItems="center" gap={5}>
<Image
src={product?.images?.[0]}
style={styles.productImage}
resizeMode="cover"
/>
{product?.priceRange?.minVariantPrice && (
<View
bg="gray-1100/600"
px="4xs"
py="6xs"
borderRadius={10}
>
<PriceDisplay
adjustsFontSizeToFit
numberOfLines={1}
color="gray-0"
fontSize="2xs"
price={product.priceRange.minVariantPrice}
/>
</View>
)}
</Flexbox>
</Actionable>
</Animated.View>
)}
</View>
);
};
// ---------------------------------------------------------
// Navigation button
// ---------------------------------------------------------
interface NavButtonProps {
direction: 'prev' | 'next';
onPress: () => void;
disabled?: boolean;
}
const NavButton: React.FC<NavButtonProps> = ({
direction,
onPress,
disabled,
}) => {
const scale = React.useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Animated.timing(scale, {
toValue: 0.9,
duration: 100,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
Animated.timing(scale, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}).start();
};
return (
<Pressable
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
disabled={disabled}
style={[styles.navButton, disabled && styles.navButtonDisabled]}
>
<Animated.View
style={[
styles.navButtonInner,
{
transform: [{ scale }],
},
]}
>
<Icon
color="white"
icon={
direction === 'prev'
? 'evilicons:chevron-left'
: 'evilicons:chevron-right'
}
/>
</Animated.View>
</Pressable>
);
};
// ---------------------------------------------------------
// Image
// ---------------------------------------------------------
interface LookImageProps {
image: ImageData;
imageIndex: number;
layout: { width: number; height: number };
activeHotspot: number;
showCard: boolean;
onHotspotPress: (index: number) => void;
opacity: Animated.Value;
isVisible: boolean;
isOverlay?: boolean;
}
const LookImage: React.FC<LookImageProps> = ({
image,
imageIndex,
layout,
activeHotspot,
showCard,
onHotspotPress,
opacity,
isVisible,
isOverlay = false,
}) => {
if (!isVisible) return null;
return (
<Animated.View
style={[
isOverlay ? styles.lookImageOverlay : styles.lookImageBase,
{ opacity },
]}
>
<Image
source={{ uri: image.url }}
style={{ width: '100%', aspectRatio: image.aspectRatio }}
resizeMode="cover"
/>
<View style={styles.vignetteOverlay} />
{layout.width > 0 &&
image.annotations?.map((annotation, index) => (
<HotspotWithCard
key={`${imageIndex}-${annotation.id}`}
annotation={annotation}
index={index}
imageWidth={layout.width}
imageHeight={layout.height}
isActive={index === activeHotspot && showCard}
onHotspotPress={onHotspotPress}
/>
))}
</Animated.View>
);
};
// ---------------------------------------------------------
// App block
// ---------------------------------------------------------
const AppBlock: React.FC<AppBlockProps> = ({ data }) => {
const { images } = data || {};
if (!images || images.length === 0) {
return (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>No images available</Text>
</View>
);
}
const [currentImageIndex, setCurrentImageIndex] = React.useState(0);
const [nextImageIndex, setNextImageIndex] = React.useState<number | null>(null);
const [containerWidth, setContainerWidth] = React.useState(0);
const [activeHotspot, setActiveHotspot] = React.useState(0);
const [showCard, setShowCard] = React.useState(true);
const [isTransitioning, setIsTransitioning] = React.useState(false);
const currentOpacity = React.useRef(new Animated.Value(1)).current;
const nextOpacity = React.useRef(new Animated.Value(0)).current;
const animatedHeight = React.useRef(new Animated.Value(0)).current;
const currentImage = images[currentImageIndex];
const nextImage =
nextImageIndex !== null ? images[nextImageIndex] : null;
const totalImages = images.length;
const totalHotspots = currentImage?.annotations?.length || 0;
const getHeightForImage = React.useCallback(
(image: ImageData, width: number) => {
if (!width || !image?.aspectRatio) return 0;
return width / image.aspectRatio;
},
[],
);
React.useEffect(() => {
if (containerWidth > 0 && currentImage) {
const initialHeight = getHeightForImage(
currentImage,
containerWidth,
);
animatedHeight.setValue(initialHeight);
}
}, [
animatedHeight,
containerWidth,
currentImage,
currentImage?.aspectRatio,
getHeightForImage,
]);
const layout = React.useMemo(
() => ({
width: containerWidth,
height:
containerWidth > 0 && currentImage
? getHeightForImage(currentImage, containerWidth)
: 0,
}),
[containerWidth, currentImage, getHeightForImage],
);
const DISPLAY_MS = 3500;
const TRANSITION_MS = 300;
const cycleRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const transitionRef =
React.useRef<ReturnType<typeof setTimeout> | null>(null);
const clearTimers = React.useCallback(() => {
if (cycleRef.current) {
clearTimeout(cycleRef.current);
cycleRef.current = null;
}
if (transitionRef.current) {
clearTimeout(transitionRef.current);
transitionRef.current = null;
}
}, []);
const startCycle = React.useCallback(() => {
clearTimers();
cycleRef.current = setTimeout(() => {
setShowCard(false);
transitionRef.current = setTimeout(() => {
setActiveHotspot((prev) =>
totalHotspots > 0 ? (prev + 1) % totalHotspots : 0,
);
setShowCard(true);
startCycle();
}, TRANSITION_MS);
}, DISPLAY_MS);
}, [clearTimers, totalHotspots]);
React.useEffect(() => {
if (!totalHotspots || isTransitioning) return;
startCycle();
return () => clearTimers();
}, [
clearTimers,
currentImageIndex,
isTransitioning,
startCycle,
totalHotspots,
]);
const handleHotspotPress = React.useCallback(
(index: number) => {
if (index === activeHotspot || isTransitioning) return;
clearTimers();
setShowCard(false);
transitionRef.current = setTimeout(() => {
setActiveHotspot(index);
setShowCard(true);
startCycle();
}, TRANSITION_MS);
},
[
activeHotspot,
clearTimers,
isTransitioning,
startCycle,
],
);
const transitionToImage = React.useCallback(
(targetIndex: number) => {
if (isTransitioning || targetIndex === currentImageIndex) return;
if (targetIndex < 0 || targetIndex >= totalImages) return;
clearTimers();
setShowCard(false);
setIsTransitioning(true);
setNextImageIndex(targetIndex);
const targetImage = images[targetIndex];
const targetHeight = getHeightForImage(
targetImage,
containerWidth,
);
currentOpacity.setValue(1);
nextOpacity.setValue(0);
Animated.parallel([
Animated.timing(nextOpacity, {
toValue: 1,
duration: IMAGE_TRANSITION_MS,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(animatedHeight, {
toValue: targetHeight,
duration: IMAGE_TRANSITION_MS,
easing: Easing.inOut(Easing.quad),
useNativeDriver: false,
}),
]).start();
const completeTimer = setTimeout(() => {
setCurrentImageIndex(targetIndex);
setActiveHotspot(0);
setShowCard(true);
setIsTransitioning(false);
}, IMAGE_TRANSITION_MS);
const clearOverlayTimer = setTimeout(() => {
setNextImageIndex(null);
nextOpacity.setValue(0);
}, IMAGE_TRANSITION_MS + 100);
return () => {
clearTimeout(completeTimer);
clearTimeout(clearOverlayTimer);
};
},
[
animatedHeight,
clearTimers,
containerWidth,
currentImageIndex,
currentOpacity,
getHeightForImage,
images,
isTransitioning,
nextOpacity,
totalImages,
],
);
const handlePrevImage = React.useCallback(() => {
const prevIndex =
currentImageIndex === 0
? totalImages - 1
: currentImageIndex - 1;
transitionToImage(prevIndex);
}, [currentImageIndex, totalImages, transitionToImage]);
const handleNextImage = React.useCallback(() => {
const nextIndex =
currentImageIndex === totalImages - 1
? 0
: currentImageIndex + 1;
transitionToImage(nextIndex);
}, [currentImageIndex, totalImages, transitionToImage]);
const hasInitializedHeight = React.useRef(false);
const handleLayout = React.useCallback(
(e: any) => {
const { width } = e.nativeEvent.layout;
if (width !== containerWidth) {
setContainerWidth(width);
if (currentImage && !hasInitializedHeight.current) {
hasInitializedHeight.current = true;
animatedHeight.setValue(
getHeightForImage(currentImage, width),
);
}
}
},
[
animatedHeight,
containerWidth,
currentImage,
getHeightForImage,
],
);
return (
<View style={styles.container}>
<Animated.View
style={[
styles.imageWrapper,
{
height: animatedHeight,
},
]}
onLayout={handleLayout}
>
<LookImage
image={currentImage}
imageIndex={currentImageIndex}
layout={layout}
activeHotspot={activeHotspot}
showCard={showCard && !isTransitioning}
onHotspotPress={handleHotspotPress}
opacity={currentOpacity}
isVisible
isOverlay={false}
/>
{nextImage && (
<LookImage
image={nextImage}
imageIndex={nextImageIndex!}
layout={{
width: containerWidth,
height: getHeightForImage(
nextImage,
containerWidth,
),
}}
activeHotspot={0}
showCard={false}
onHotspotPress={() => {}}
opacity={nextOpacity}
isVisible
isOverlay
/>
)}
{totalImages > 1 && (
<>
<View style={styles.navButtonLeft}>
<NavButton
direction="prev"
onPress={handlePrevImage}
disabled={isTransitioning}
/>
</View>
<View style={styles.navButtonRight}>
<NavButton
direction="next"
onPress={handleNextImage}
disabled={isTransitioning}
/>
</View>
</>
)}
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'transparent',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 24,
backgroundColor: 'transparent',
},
emptyText: {
fontSize: 14,
color: '#666',
textAlign: 'center',
},
imageWrapper: {
width: '100%',
position: 'relative',
zIndex: 1,
overflow: 'hidden',
},
lookImageBase: {
position: 'relative',
width: '100%',
},
lookImageOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
width: '100%',
},
vignetteOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'transparent',
borderWidth: 0,
},
overlayContainer: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
zIndex: 10,
overflow: 'visible',
},
ringsContainer: {
position: 'absolute',
width: HOTSPOT_SIZE,
height: HOTSPOT_SIZE,
justifyContent: 'center',
alignItems: 'center',
zIndex: 5,
},
pulseRing: {
position: 'absolute',
width: HOTSPOT_SIZE,
height: HOTSPOT_SIZE,
borderRadius: HOTSPOT_SIZE / 2,
borderWidth: 2,
borderColor: ACTIVE_COLOR,
backgroundColor: 'transparent',
},
hotspotPressable: {
width: HOTSPOT_SIZE * 2,
height: HOTSPOT_SIZE * 2,
marginLeft: -HOTSPOT_SIZE,
marginTop: -HOTSPOT_SIZE,
justifyContent: 'center',
alignItems: 'center',
zIndex: 15,
},
hotspotWrapper: {
width: HOTSPOT_SIZE,
height: HOTSPOT_SIZE,
justifyContent: 'center',
alignItems: 'center',
},
hotspotGlow: {
position: 'absolute',
width: HOTSPOT_SIZE * 2,
height: HOTSPOT_SIZE * 2,
borderRadius: HOTSPOT_SIZE,
backgroundColor: ACTIVE_COLOR,
},
hotspotOuter: {
width: HOTSPOT_SIZE,
height: HOTSPOT_SIZE,
borderRadius: HOTSPOT_SIZE / 2,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1.5,
borderColor: 'rgba(255, 255, 255, 0.6)',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
elevation: 4,
},
hotspotInner: {
width: HOTSPOT_SIZE * 0.45,
height: HOTSPOT_SIZE * 0.45,
borderRadius: HOTSPOT_SIZE * 0.225,
backgroundColor: '#fff',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 2,
elevation: 2,
},
hotspotOuterActive: {
borderColor: ACTIVE_COLOR,
backgroundColor: 'rgba(76, 217, 100, 0.15)',
},
hotspotInnerActive: {
backgroundColor: ACTIVE_COLOR,
},
connectionLine: {
position: 'absolute',
height: 2,
backgroundColor: ACTIVE_COLOR,
borderRadius: 1,
zIndex: 12,
},
// The hotspot press target is 2x HOTSPOT_SIZE and is centered using
// negative margins. Offset the line by HOTSPOT_SIZE so its inner edge
// aligns with the actual hotspot center rather than the pressable edge.
connectionLineLeft: {
right: HOTSPOT_SIZE,
},
connectionLineRight: {
left: HOTSPOT_SIZE,
},
productContainer: {
position: 'absolute',
width: IMAGE_SIZE,
alignItems: 'center',
zIndex: 20,
},
productImage: {
width: IMAGE_SIZE,
height: IMAGE_SIZE,
borderRadius: IMAGE_SIZE / 2,
borderWidth: 2,
borderColor: '#fff',
backgroundColor: '#f0f0f0',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 6,
elevation: 8,
},
navButtonLeft: {
position: 'absolute',
left: 12,
top: '50%',
marginTop: -NAV_BUTTON_SIZE / 2,
zIndex: 30,
},
navButtonRight: {
position: 'absolute',
right: 12,
top: '50%',
marginTop: -NAV_BUTTON_SIZE / 2,
zIndex: 30,
},
navButton: {
width: NAV_BUTTON_SIZE,
height: NAV_BUTTON_SIZE,
borderRadius: NAV_BUTTON_SIZE / 2,
justifyContent: 'center',
alignItems: 'center',
},
navButtonDisabled: {
opacity: 0.3,
},
navButtonInner: {
width: NAV_BUTTON_SIZE,
height: NAV_BUTTON_SIZE,
borderRadius: NAV_BUTTON_SIZE / 2,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.3)',
},
});
export default AppBlock;