From 04572a8e5081b1e4871e273cde9dbaaa44c52fe0 Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Wed, 13 Mar 2024 21:43:44 +0000 Subject: structure: Create apps dir and copy tooling dir from t3-turbo repo --- apps/mobile/components/ui/ActionButton.tsx | 21 +++ apps/mobile/components/ui/Button.tsx | 81 ++++++++++++ apps/mobile/components/ui/Divider.tsx | 28 ++++ apps/mobile/components/ui/FullPageSpinner.tsx | 9 ++ apps/mobile/components/ui/Input.tsx | 28 ++++ apps/mobile/components/ui/Skeleton.tsx | 38 ++++++ apps/mobile/components/ui/Toast.tsx | 183 ++++++++++++++++++++++++++ 7 files changed, 388 insertions(+) create mode 100644 apps/mobile/components/ui/ActionButton.tsx create mode 100644 apps/mobile/components/ui/Button.tsx create mode 100644 apps/mobile/components/ui/Divider.tsx create mode 100644 apps/mobile/components/ui/FullPageSpinner.tsx create mode 100644 apps/mobile/components/ui/Input.tsx create mode 100644 apps/mobile/components/ui/Skeleton.tsx create mode 100644 apps/mobile/components/ui/Toast.tsx (limited to 'apps/mobile/components/ui') diff --git a/apps/mobile/components/ui/ActionButton.tsx b/apps/mobile/components/ui/ActionButton.tsx new file mode 100644 index 00000000..c51eb332 --- /dev/null +++ b/apps/mobile/components/ui/ActionButton.tsx @@ -0,0 +1,21 @@ +import { ActivityIndicator, Pressable, PressableProps } from "react-native"; + +export function ActionButton({ + children, + loading, + disabled, + ...props +}: PressableProps & { + loading: boolean; +}) { + if (disabled !== undefined) { + disabled ||= loading; + } else if (loading) { + disabled = true; + } + return ( + + {loading ? : children} + + ); +} diff --git a/apps/mobile/components/ui/Button.tsx b/apps/mobile/components/ui/Button.tsx new file mode 100644 index 00000000..4c3cbc69 --- /dev/null +++ b/apps/mobile/components/ui/Button.tsx @@ -0,0 +1,81 @@ +import { type VariantProps, cva } from "class-variance-authority"; +import { Text, TouchableOpacity } from "react-native"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "flex flex-row items-center justify-center rounded-md", + { + variants: { + variant: { + default: "bg-primary", + secondary: "bg-secondary", + destructive: "bg-destructive", + ghost: "bg-slate-700", + link: "text-primary underline-offset-4", + }, + size: { + default: "h-10 px-4", + sm: "h-8 px-2", + lg: "h-12 px-8", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +const buttonTextVariants = cva("text-center font-medium", { + variants: { + variant: { + default: "text-primary-foreground", + secondary: "text-secondary-foreground", + destructive: "text-destructive-foreground", + ghost: "text-primary-foreground", + link: "text-primary-foreground underline", + }, + size: { + default: "text-base", + sm: "text-sm", + lg: "text-xl", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, +}); + +interface ButtonProps + extends React.ComponentPropsWithoutRef, + VariantProps { + label: string; + labelClasses?: string; +} +function Button({ + label, + labelClasses, + className, + variant, + size, + ...props +}: ButtonProps) { + return ( + + + {label} + + + ); +} + +export { Button, buttonVariants, buttonTextVariants }; diff --git a/apps/mobile/components/ui/Divider.tsx b/apps/mobile/components/ui/Divider.tsx new file mode 100644 index 00000000..1da0a71e --- /dev/null +++ b/apps/mobile/components/ui/Divider.tsx @@ -0,0 +1,28 @@ +import { View } from "react-native"; + +import { cn } from "@/lib/utils"; + +function Divider({ + color = "#DFE4EA", + className, + orientation, + ...props +}: { + color?: string; + orientation: "horizontal" | "vertical"; +} & React.ComponentPropsWithoutRef) { + const dividerStyles = [{ backgroundColor: color }]; + + return ( + + ); +} + +export { Divider }; diff --git a/apps/mobile/components/ui/FullPageSpinner.tsx b/apps/mobile/components/ui/FullPageSpinner.tsx new file mode 100644 index 00000000..01187f11 --- /dev/null +++ b/apps/mobile/components/ui/FullPageSpinner.tsx @@ -0,0 +1,9 @@ +import { View, ActivityIndicator } from "react-native"; + +export default function FullPageSpinner() { + return ( + + + + ); +} diff --git a/apps/mobile/components/ui/Input.tsx b/apps/mobile/components/ui/Input.tsx new file mode 100644 index 00000000..2fcb2764 --- /dev/null +++ b/apps/mobile/components/ui/Input.tsx @@ -0,0 +1,28 @@ +import { forwardRef } from "react"; +import { Text, TextInput, View } from "react-native"; + +import { cn } from "@/lib/utils"; + +export interface InputProps + extends React.ComponentPropsWithoutRef { + label?: string; + labelClasses?: string; + inputClasses?: string; +} + +const Input = forwardRef, InputProps>( + ({ className, label, labelClasses, inputClasses, ...props }, ref) => ( + + {label && {label}} + + + ), +); + +export { Input }; diff --git a/apps/mobile/components/ui/Skeleton.tsx b/apps/mobile/components/ui/Skeleton.tsx new file mode 100644 index 00000000..68b22e1e --- /dev/null +++ b/apps/mobile/components/ui/Skeleton.tsx @@ -0,0 +1,38 @@ +import { useEffect, useRef } from "react"; +import { Animated, type View } from "react-native"; + +import { cn } from "@/lib/utils"; + +function Skeleton({ + className, + ...props +}: { className?: string } & React.ComponentPropsWithoutRef) { + const fadeAnim = useRef(new Animated.Value(0.5)).current; + + useEffect(() => { + Animated.loop( + Animated.sequence([ + Animated.timing(fadeAnim, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 0.5, + duration: 1000, + useNativeDriver: true, + }), + ]), + ).start(); + }, [fadeAnim]); + + return ( + + ); +} + +export { Skeleton }; diff --git a/apps/mobile/components/ui/Toast.tsx b/apps/mobile/components/ui/Toast.tsx new file mode 100644 index 00000000..fb319f84 --- /dev/null +++ b/apps/mobile/components/ui/Toast.tsx @@ -0,0 +1,183 @@ +import { createContext, useContext, useEffect, useRef, useState } from "react"; +import { Animated, Text, View } from "react-native"; + +import { cn } from "@/lib/utils"; + +const toastVariants = { + default: "bg-foreground", + destructive: "bg-destructive", + success: "bg-green-500", + info: "bg-blue-500", +}; + +interface ToastProps { + id: number; + message: string; + onHide: (id: number) => void; + variant?: keyof typeof toastVariants; + duration?: number; + showProgress?: boolean; +} +function Toast({ + id, + message, + onHide, + variant = "default", + duration = 3000, + showProgress = true, +}: ToastProps) { + const opacity = useRef(new Animated.Value(0)).current; + const progress = useRef(new Animated.Value(0)).current; + + useEffect(() => { + Animated.sequence([ + Animated.timing(opacity, { + toValue: 1, + duration: 500, + useNativeDriver: true, + }), + Animated.timing(progress, { + toValue: 1, + duration: duration - 1000, + useNativeDriver: false, + }), + Animated.timing(opacity, { + toValue: 0, + duration: 500, + useNativeDriver: true, + }), + ]).start(() => onHide(id)); + }, [duration]); + + return ( + + {message} + {showProgress && ( + + + + )} + + ); +} + +type ToastVariant = keyof typeof toastVariants; + +interface ToastMessage { + id: number; + text: string; + variant: ToastVariant; + duration?: number; + position?: string; + showProgress?: boolean; +} +interface ToastContextProps { + toast: (t: { + message: string; + variant?: keyof typeof toastVariants; + duration?: number; + position?: "top" | "bottom"; + showProgress?: boolean; + }) => void; + removeToast: (id: number) => void; +} +const ToastContext = createContext(undefined); + +// TODO: refactor to pass position to Toast instead of ToastProvider +function ToastProvider({ + children, + position = "top", +}: { + children: React.ReactNode; + position?: "top" | "bottom"; +}) { + const [messages, setMessages] = useState([]); + + const toast: ToastContextProps["toast"] = ({ + message, + variant = "default", + duration = 3000, + position = "top", + showProgress = true, + }: { + message: string; + variant?: ToastVariant; + duration?: number; + position?: "top" | "bottom"; + showProgress?: boolean; + }) => { + setMessages((prev) => [ + ...prev, + { + id: Date.now(), + text: message, + variant, + duration, + position, + showProgress, + }, + ]); + }; + + const removeToast = (id: number) => { + setMessages((prev) => prev.filter((message) => message.id !== id)); + }; + + return ( + + {children} + + {messages.map((message) => ( + + ))} + + + ); +} + +function useToast() { + const context = useContext(ToastContext); + if (!context) { + throw new Error("useToast must be used within ToastProvider"); + } + return context; +} + +export { ToastProvider, ToastVariant, Toast, toastVariants, useToast }; -- cgit v1.2.3-70-g09d2