From edb366854825f4c24ab5d77cf88759465d067f00 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Wed, 6 May 2026 19:42:52 -0500 Subject: [PATCH] refactor(scanner): added toasts in to make it look better --- lstMobile/src/app/login.tsx | 12 +- lstMobile/src/app/setup.tsx | 21 ++- lstMobile/src/components/LSTScanner.tsx | 30 ++-- lstMobile/src/components/ProdScanner.tsx | 30 ++-- lstMobile/src/components/ui/dialog.tsx | 140 ++++++++++++++++++ lstMobile/src/components/ui/icon.tsx | 57 +++++++ .../ui/native-only-animated-view.tsx | 23 +++ lstMobile/src/components/ui/text.tsx | 5 +- 8 files changed, 286 insertions(+), 32 deletions(-) create mode 100644 lstMobile/src/components/ui/dialog.tsx create mode 100644 lstMobile/src/components/ui/icon.tsx create mode 100644 lstMobile/src/components/ui/native-only-animated-view.tsx diff --git a/lstMobile/src/app/login.tsx b/lstMobile/src/app/login.tsx index 4890f35..38ec522 100644 --- a/lstMobile/src/app/login.tsx +++ b/lstMobile/src/app/login.tsx @@ -3,10 +3,14 @@ import axios from "axios"; import { useRouter } from "expo-router"; import { useState } from "react"; import { Alert, Button, Text, View } from "react-native"; +import Toast from "react-native-toast-message"; import { Input } from "../components/ui/input"; import { useAppStore } from "../hooks/useAppStore"; import { useMobileAuthStore } from "../hooks/useMobileAuth"; +const formatName = (name?: string) => + name ? name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() : ""; + export default function Login() { // doing this causes rerender and sub //const { setUser } = useMobileAuthStore(); @@ -33,12 +37,18 @@ export default function Login() { if (res.status === 200) { // this way to set the user is direct and basically a 1 shot + Toast.show({ + type: "success", + text1: `Welcome back ${formatName(res.data.data.name)}`, + }); useMobileAuthStore.getState().setUser(res.data.data); return router.replace("/(tabs)/scanner"); } } catch (error) { console.log(error); - Alert.alert("Login Error", `Invalid pin please try again`); + //Alert.alert("Login Error", `Invalid pin please try again`); + + Toast.show({ type: "error", text1: `Invalid pin please try again` }); } }; diff --git a/lstMobile/src/app/setup.tsx b/lstMobile/src/app/setup.tsx index 8f081ca..8163821 100644 --- a/lstMobile/src/app/setup.tsx +++ b/lstMobile/src/app/setup.tsx @@ -2,6 +2,7 @@ import Constants from "expo-constants"; import { useRouter } from "expo-router"; import { useState } from "react"; import { Alert, Button, Text, TextInput, View } from "react-native"; +import Toast from "react-native-toast-message"; import { useAppStore } from "../hooks/useAppStore"; import { useServerStore } from "../hooks/useServerCheck"; @@ -31,14 +32,23 @@ export default function Setup() { if (pin === "6971") { setAuth(true); } else { - Alert.alert("Incorrect pin entered please try again"); + //Alert.alert("Incorrect pin entered please try again"); + Toast.show({ + type: "error", + text1: "Incorrect pin entered please try again", + }); setPin(""); } }; const handleSave = async () => { if (!serverIp.trim() || !serverPort.trim()) { - Alert.alert("Missing info", "Please fill in both fields."); + //Alert.alert("Missing info", "Please fill in both fields."); + Toast.show({ + type: "error", + text1: "Missing info", + text2: "Please fill in both fields.", + }); return; } @@ -50,7 +60,12 @@ export default function Setup() { isRegistered: true, }); - Alert.alert("Saved", "Config saved to device."); + //Alert.alert("Saved", "Config saved to device."); + Toast.show({ + type: "info", + text1: "Saved", + text2: "Config saved to device.", + }); //router.replace("/"); }; return ( diff --git a/lstMobile/src/components/LSTScanner.tsx b/lstMobile/src/components/LSTScanner.tsx index 0c3d981..f9c40e4 100644 --- a/lstMobile/src/components/LSTScanner.tsx +++ b/lstMobile/src/components/LSTScanner.tsx @@ -1,5 +1,6 @@ import axios from "axios"; import { format } from "date-fns-tz"; +import { useFocusEffect } from "expo-router"; import { useCallback, useEffect, useState } from "react"; import { Alert, Button, Text, View } from "react-native"; import { useAppStore } from "../hooks/useAppStore"; @@ -90,7 +91,9 @@ export default function LSTScanner() { user: user?.name ?? "prodScan", runningNumber: scan.data.startsWith("000") ? parseInt(scan.data.slice(10, -1) || "000", 10).toString() - : "0", + : scan.data.startsWith("loc") + ? scan.data + : "0", }); } catch (error) { console.log(error); @@ -153,20 +156,21 @@ export default function LSTScanner() { //console.log(lastScan); - useEffect(() => { - zebraScanner.ensureProfile(); - zebraScanner.startListening(); + useFocusEffect( + useCallback(() => { + zebraScanner.startListening(); - const sub = zebraScanner.addScanListener((scan) => { - //console.log("SCAN:", scan); - handleScan(scan); - }); + const sub = zebraScanner.addScanListener((scan) => { + //console.log("SCAN:", scan); + handleScan(scan); + }); - return () => { - sub.remove(); - zebraScanner.stopListening(); - }; - }, [handleScan]); + return () => { + sub.remove(); + zebraScanner.stopListening(); + }; + }, [handleScan]), + ); return ( diff --git a/lstMobile/src/components/ProdScanner.tsx b/lstMobile/src/components/ProdScanner.tsx index d56fcc4..9af522a 100644 --- a/lstMobile/src/components/ProdScanner.tsx +++ b/lstMobile/src/components/ProdScanner.tsx @@ -1,5 +1,6 @@ import axios from "axios"; import { format } from "date-fns-tz"; +import { useFocusEffect } from "expo-router"; import { useCallback, useEffect, useState } from "react"; import { Text, View } from "react-native"; import { useAppStore } from "../hooks/useAppStore"; @@ -58,7 +59,9 @@ export default function ProdScanner() { ...scanned.data, runningNumber: scan.data.startsWith("000") ? parseInt(scan.data.slice(10, -1) || "000", 10).toString() - : "0", + : scan.data.startsWith("loc") + ? scan.data + : "0", }; try { await axios.post( @@ -118,20 +121,21 @@ export default function ProdScanner() { //console.log(lastScan); - useEffect(() => { - zebraScanner.ensureProfile(); - zebraScanner.startListening(); + useFocusEffect( + useCallback(() => { + zebraScanner.startListening(); - const sub = zebraScanner.addScanListener((scan) => { - //console.log("SCAN:", scan); - handleScan(scan); - }); + const sub = zebraScanner.addScanListener((scan) => { + //console.log("SCAN:", scan); + handleScan(scan); + }); - return () => { - sub.remove(); - zebraScanner.stopListening(); - }; - }, [handleScan]); + return () => { + sub.remove(); + zebraScanner.stopListening(); + }; + }, [handleScan]), + ); return ( diff --git a/lstMobile/src/components/ui/dialog.tsx b/lstMobile/src/components/ui/dialog.tsx new file mode 100644 index 0000000..4709e46 --- /dev/null +++ b/lstMobile/src/components/ui/dialog.tsx @@ -0,0 +1,140 @@ +import { Icon } from '@/components/ui/icon'; +import { NativeOnlyAnimatedView } from '@/components/ui/native-only-animated-view'; +import { cn } from '@/lib/utils'; +import * as DialogPrimitive from '@rn-primitives/dialog'; +import { X } from 'lucide-react-native'; +import * as React from 'react'; +import { Platform, Text, View, type ViewProps } from 'react-native'; +import { FadeIn, FadeOut } from 'react-native-reanimated'; +import { FullWindowOverlay as RNFullWindowOverlay } from 'react-native-screens'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const FullWindowOverlay = Platform.OS === 'ios' ? RNFullWindowOverlay : React.Fragment; + +function DialogOverlay({ + className, + children, + ...props +}: Omit, 'asChild'> & { + children?: React.ReactNode; + }) { + return ( + + *]:cursor-auto', + }), + className + )} + {...props} + asChild={Platform.OS !== 'web'}> + + + <>{children} + + + + + ); +} +function DialogContent({ + className, + portalHost, + children, + ...props +}: React.ComponentProps & { + portalHost?: string; + }) { + return ( + + + + <>{children} + + + Close + + + + + ); +} + +function DialogHeader({ className, ...props }: ViewProps) { + return ( + + ); +} + +function DialogFooter({ className, ...props }: ViewProps) { + return ( + + ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/lstMobile/src/components/ui/icon.tsx b/lstMobile/src/components/ui/icon.tsx new file mode 100644 index 0000000..0bddc7c --- /dev/null +++ b/lstMobile/src/components/ui/icon.tsx @@ -0,0 +1,57 @@ +import { TextClassContext } from '@/components/ui/text'; +import { cn } from '@/lib/utils'; +import type { LucideIcon, LucideProps } from 'lucide-react-native'; +import { cssInterop } from 'nativewind'; +import * as React from 'react'; + +type IconProps = LucideProps & { + as: LucideIcon; +} & React.RefAttributes; + +function IconImpl({ as: IconComponent, ...props }: IconProps) { + return ; +} + +cssInterop(IconImpl, { + className: { + target: 'style', + nativeStyleToProp: { + height: 'size', + width: 'size', + }, + }, +}); + +/** + * A wrapper component for Lucide icons with Nativewind `className` support via `cssInterop`. + * + * This component allows you to render any Lucide icon while applying utility classes + * using `nativewind`. It avoids the need to wrap or configure each icon individually. + * + * @component + * @example + * ```tsx + * import { ArrowRight } from 'lucide-react-native'; + * import { Icon } from '@/registry/components/ui/icon'; + * + * + * ``` + * + * @param {LucideIcon} as - The Lucide icon component to render. + * @param {string} className - Utility classes to style the icon using Nativewind. + * @param {number} size - Icon size (defaults to 14). + * @param {...LucideProps} ...props - Additional Lucide icon props passed to the "as" icon. + */ +function Icon({ as: IconComponent, className, size = 14, ...props }: IconProps) { + const textClass = React.useContext(TextClassContext); + return ( + + ); +} + +export { Icon }; diff --git a/lstMobile/src/components/ui/native-only-animated-view.tsx b/lstMobile/src/components/ui/native-only-animated-view.tsx new file mode 100644 index 0000000..032bd04 --- /dev/null +++ b/lstMobile/src/components/ui/native-only-animated-view.tsx @@ -0,0 +1,23 @@ +import { Platform } from 'react-native'; +import Animated from 'react-native-reanimated'; + +/** + * This component is used to wrap animated views that should only be animated on native. + * @param props - The props for the animated view. + * @returns The animated view if the platform is native, otherwise the children. + * @example + * + * I am only animated on native + * + */ +function NativeOnlyAnimatedView( + props: React.ComponentProps & React.RefAttributes +) { + if (Platform.OS === 'web') { + return <>{props.children as React.ReactNode}; + } else { + return ; + } +} + +export { NativeOnlyAnimatedView }; diff --git a/lstMobile/src/components/ui/text.tsx b/lstMobile/src/components/ui/text.tsx index fc241e9..1b54125 100644 --- a/lstMobile/src/components/ui/text.tsx +++ b/lstMobile/src/components/ui/text.tsx @@ -1,5 +1,5 @@ import { cn } from '@/lib/utils'; -import * as Slot from '@rn-primitives/slot'; +import { Slot } from '@rn-primitives/slot'; import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; import { Platform, Text as RNText, type Role } from 'react-native'; @@ -70,11 +70,12 @@ function Text({ variant = 'default', ...props }: React.ComponentProps & + React.RefAttributes & TextVariantProps & { asChild?: boolean; }) { const textClass = React.useContext(TextClassContext); - const Component = asChild ? Slot.Text : RNText; + const Component = asChild ? Slot : RNText; return (