aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile/components/sharing
diff options
context:
space:
mode:
Diffstat (limited to 'apps/mobile/components/sharing')
-rw-r--r--apps/mobile/components/sharing/ErrorAnimation.tsx41
-rw-r--r--apps/mobile/components/sharing/LoadingAnimation.tsx120
-rw-r--r--apps/mobile/components/sharing/SuccessAnimation.tsx140
3 files changed, 301 insertions, 0 deletions
diff --git a/apps/mobile/components/sharing/ErrorAnimation.tsx b/apps/mobile/components/sharing/ErrorAnimation.tsx
new file mode 100644
index 00000000..c5cc743a
--- /dev/null
+++ b/apps/mobile/components/sharing/ErrorAnimation.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from "react";
+import { View } from "react-native";
+import Animated, {
+ useAnimatedStyle,
+ useSharedValue,
+ withSequence,
+ withSpring,
+ withTiming,
+} from "react-native-reanimated";
+import * as Haptics from "expo-haptics";
+import { AlertCircle } from "lucide-react-native";
+
+export default function ErrorAnimation() {
+ const scale = useSharedValue(0);
+ const shake = useSharedValue(0);
+
+ useEffect(() => {
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+
+ scale.value = withSpring(1, { damping: 12, stiffness: 200 });
+ shake.value = withSequence(
+ withTiming(-10, { duration: 50 }),
+ withTiming(10, { duration: 100 }),
+ withTiming(-10, { duration: 100 }),
+ withTiming(10, { duration: 100 }),
+ withTiming(0, { duration: 50 }),
+ );
+ }, []);
+
+ const style = useAnimatedStyle(() => ({
+ transform: [{ scale: scale.value }, { translateX: shake.value }],
+ }));
+
+ return (
+ <Animated.View style={style} className="items-center gap-4">
+ <View className="h-24 w-24 items-center justify-center rounded-full bg-destructive">
+ <AlertCircle size={48} color="white" strokeWidth={2} />
+ </View>
+ </Animated.View>
+ );
+}
diff --git a/apps/mobile/components/sharing/LoadingAnimation.tsx b/apps/mobile/components/sharing/LoadingAnimation.tsx
new file mode 100644
index 00000000..a8838915
--- /dev/null
+++ b/apps/mobile/components/sharing/LoadingAnimation.tsx
@@ -0,0 +1,120 @@
+import { useEffect } from "react";
+import { View } from "react-native";
+import Animated, {
+ Easing,
+ FadeIn,
+ useAnimatedStyle,
+ useSharedValue,
+ withDelay,
+ withRepeat,
+ withSequence,
+ withTiming,
+} from "react-native-reanimated";
+import { Text } from "@/components/ui/Text";
+import { Archive } from "lucide-react-native";
+
+export default function LoadingAnimation() {
+ const scale = useSharedValue(1);
+ const rotation = useSharedValue(0);
+ const opacity = useSharedValue(0.6);
+ const dotOpacity1 = useSharedValue(0);
+ const dotOpacity2 = useSharedValue(0);
+ const dotOpacity3 = useSharedValue(0);
+
+ useEffect(() => {
+ scale.value = withRepeat(
+ withSequence(
+ withTiming(1.1, { duration: 800, easing: Easing.inOut(Easing.ease) }),
+ withTiming(1, { duration: 800, easing: Easing.inOut(Easing.ease) }),
+ ),
+ -1,
+ false,
+ );
+
+ rotation.value = withRepeat(
+ withSequence(
+ withTiming(-5, { duration: 400, easing: Easing.inOut(Easing.ease) }),
+ withTiming(5, { duration: 800, easing: Easing.inOut(Easing.ease) }),
+ withTiming(0, { duration: 400, easing: Easing.inOut(Easing.ease) }),
+ ),
+ -1,
+ false,
+ );
+
+ opacity.value = withRepeat(
+ withSequence(
+ withTiming(1, { duration: 800 }),
+ withTiming(0.6, { duration: 800 }),
+ ),
+ -1,
+ false,
+ );
+
+ dotOpacity1.value = withRepeat(
+ withSequence(
+ withTiming(1, { duration: 300 }),
+ withDelay(900, withTiming(0, { duration: 0 })),
+ ),
+ -1,
+ );
+ dotOpacity2.value = withDelay(
+ 300,
+ withRepeat(
+ withSequence(
+ withTiming(1, { duration: 300 }),
+ withDelay(600, withTiming(0, { duration: 0 })),
+ ),
+ -1,
+ ),
+ );
+ dotOpacity3.value = withDelay(
+ 600,
+ withRepeat(
+ withSequence(
+ withTiming(1, { duration: 300 }),
+ withDelay(300, withTiming(0, { duration: 0 })),
+ ),
+ -1,
+ ),
+ );
+ }, []);
+
+ const iconStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: scale.value }, { rotate: `${rotation.value}deg` }],
+ opacity: opacity.value,
+ }));
+
+ const dot1Style = useAnimatedStyle(() => ({ opacity: dotOpacity1.value }));
+ const dot2Style = useAnimatedStyle(() => ({ opacity: dotOpacity2.value }));
+ const dot3Style = useAnimatedStyle(() => ({ opacity: dotOpacity3.value }));
+
+ return (
+ <Animated.View
+ entering={FadeIn.duration(300)}
+ className="items-center gap-6"
+ >
+ <Animated.View
+ style={iconStyle}
+ className="h-24 w-24 items-center justify-center rounded-full bg-primary/10"
+ >
+ <Archive size={48} className="text-primary" strokeWidth={1.5} />
+ </Animated.View>
+ <View className="flex-row items-baseline">
+ <Text variant="title1" className="font-semibold text-foreground">
+ Hoarding
+ </Text>
+ <View className="w-8 flex-row">
+ <Animated.Text style={dot1Style} className="text-xl text-foreground">
+ .
+ </Animated.Text>
+ <Animated.Text style={dot2Style} className="text-xl text-foreground">
+ .
+ </Animated.Text>
+ <Animated.Text style={dot3Style} className="text-xl text-foreground">
+ .
+ </Animated.Text>
+ </View>
+ </View>
+ </Animated.View>
+ );
+}
diff --git a/apps/mobile/components/sharing/SuccessAnimation.tsx b/apps/mobile/components/sharing/SuccessAnimation.tsx
new file mode 100644
index 00000000..fa0aaf3a
--- /dev/null
+++ b/apps/mobile/components/sharing/SuccessAnimation.tsx
@@ -0,0 +1,140 @@
+import { useEffect } from "react";
+import { View } from "react-native";
+import Animated, {
+ Easing,
+ interpolate,
+ useAnimatedStyle,
+ useSharedValue,
+ withDelay,
+ withSequence,
+ withSpring,
+ withTiming,
+} from "react-native-reanimated";
+import * as Haptics from "expo-haptics";
+import { Check } from "lucide-react-native";
+
+interface ParticleProps {
+ angle: number;
+ delay: number;
+ color: string;
+}
+
+function Particle({ angle, delay, color }: ParticleProps) {
+ const progress = useSharedValue(0);
+
+ useEffect(() => {
+ progress.value = withDelay(
+ 200 + delay,
+ withSequence(
+ withTiming(1, { duration: 400, easing: Easing.out(Easing.ease) }),
+ withTiming(0, { duration: 300 }),
+ ),
+ );
+ }, []);
+
+ const particleStyle = useAnimatedStyle(() => {
+ const distance = interpolate(progress.value, [0, 1], [0, 60]);
+ const opacity = interpolate(progress.value, [0, 0.5, 1], [0, 1, 0]);
+ const scale = interpolate(progress.value, [0, 0.5, 1], [0, 1, 0]);
+ const angleRad = (angle * Math.PI) / 180;
+
+ return {
+ position: "absolute" as const,
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: color,
+ opacity,
+ transform: [
+ { translateX: Math.cos(angleRad) * distance },
+ { translateY: Math.sin(angleRad) * distance },
+ { scale },
+ ],
+ };
+ });
+
+ return <Animated.View style={particleStyle} />;
+}
+
+interface SuccessAnimationProps {
+ isAlreadyExists: boolean;
+}
+
+export default function SuccessAnimation({
+ isAlreadyExists,
+}: SuccessAnimationProps) {
+ const checkScale = useSharedValue(0);
+ const checkOpacity = useSharedValue(0);
+ const ringScale = useSharedValue(0.8);
+ const ringOpacity = useSharedValue(0);
+
+ const particleColor = isAlreadyExists
+ ? "rgb(255, 180, 0)"
+ : "rgb(0, 200, 100)";
+
+ useEffect(() => {
+ Haptics.notificationAsync(
+ isAlreadyExists
+ ? Haptics.NotificationFeedbackType.Warning
+ : Haptics.NotificationFeedbackType.Success,
+ );
+
+ ringScale.value = withSequence(
+ withTiming(1.2, { duration: 400, easing: Easing.out(Easing.ease) }),
+ withTiming(1, { duration: 200 }),
+ );
+ ringOpacity.value = withSequence(
+ withTiming(1, { duration: 200 }),
+ withDelay(300, withTiming(0.3, { duration: 300 })),
+ );
+
+ checkScale.value = withDelay(
+ 150,
+ withSpring(1, {
+ damping: 12,
+ stiffness: 200,
+ mass: 0.8,
+ }),
+ );
+ checkOpacity.value = withDelay(150, withTiming(1, { duration: 200 }));
+ }, [isAlreadyExists]);
+
+ const ringStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: ringScale.value }],
+ opacity: ringOpacity.value,
+ }));
+
+ const checkStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: checkScale.value }],
+ opacity: checkOpacity.value,
+ }));
+
+ return (
+ <View className="items-center justify-center">
+ {Array.from({ length: 8 }, (_, i) => (
+ <Particle
+ key={i}
+ angle={(i * 360) / 8}
+ delay={i * 50}
+ color={particleColor}
+ />
+ ))}
+
+ <Animated.View
+ style={ringStyle}
+ className={`absolute h-28 w-28 rounded-full ${
+ isAlreadyExists ? "bg-yellow-500/20" : "bg-green-500/20"
+ }`}
+ />
+
+ <Animated.View
+ style={checkStyle}
+ className={`h-24 w-24 items-center justify-center rounded-full ${
+ isAlreadyExists ? "bg-yellow-500" : "bg-green-500"
+ }`}
+ >
+ <Check size={48} color="white" strokeWidth={3} />
+ </Animated.View>
+ </View>
+ );
+}