mojmajstor/components/ui/button.tsx
echo 3e5b85a08e feat: Phase 7 - polish, error handling, pagination, accessibility
- Error boundary with Macedonian fallback screen and retry button
  wrapping all navigation
- Pull-to-refresh on all list/detail screens (home, explore, posts,
  chat, profile, ad detail, post detail)
- Accessibility: labels on all interactive elements, roles on
  Pressable/Button, selectable text for user data
- Deep linking: mojmajstor:// scheme configured in app.json,
  ad/[id] and chat/[id] routes work via Expo Router auto-routing
- Pagination: cursor-based paginated queries for ads, posts,
  messages, chats, reviews with Load More buttons on frontend
- Profile: shows customer posts with PostCard, loading/error states,
  create-post navigation
- Auth error messages in Macedonian (Невалидни акредитиви, итн.)

All UI text in Macedonian. TypeScript clean, Convex functions deployed.
2026-05-29 18:53:26 +02:00

78 lines
2.3 KiB
TypeScript

import { Pressable, Text, ActivityIndicator, type ViewStyle } from "react-native";
import { useTheme } from "../theme";
type Variant = "primary" | "secondary" | "outline" | "destructive";
interface ButtonProps {
variant?: Variant;
loading?: boolean;
size?: "sm" | "md" | "lg";
disabled?: boolean;
onPress?: () => void;
children: React.ReactNode;
style?: ViewStyle;
accessibilityLabel?: string;
}
export function Button({
variant = "primary",
loading = false,
size = "md",
disabled = false,
children,
style,
onPress,
accessibilityLabel,
}: ButtonProps) {
const theme = useTheme();
const sizeStyles: Record<string, ViewStyle> = {
sm: { paddingVertical: theme.spacing.sm, paddingHorizontal: theme.spacing.md, borderRadius: theme.radius.sm },
md: { paddingVertical: 12, paddingHorizontal: theme.spacing.lg, borderRadius: theme.radius.md },
lg: { paddingVertical: 16, paddingHorizontal: theme.spacing.xl, borderRadius: theme.radius.md },
};
const variantStyles: Record<Variant, ViewStyle> = {
primary: { backgroundColor: theme.colors.primary },
secondary: { backgroundColor: theme.colors.secondary },
outline: { backgroundColor: "transparent", borderWidth: 1, borderColor: theme.colors.primary },
destructive: { backgroundColor: theme.colors.error },
};
const textColors: Record<Variant, string> = {
primary: "#FFFFFF",
secondary: "#FFFFFF",
outline: theme.colors.primary,
destructive: "#FFFFFF",
};
const label = typeof children === "string" ? children : accessibilityLabel;
return (
<Pressable
accessible
accessibilityLabel={label}
accessibilityRole="button"
accessibilityState={{ disabled: disabled || loading }}
disabled={disabled || loading}
onPress={onPress}
style={[
{ alignItems: "center", justifyContent: "center" },
sizeStyles[size],
variantStyles[variant],
disabled && { opacity: 0.5 },
style,
]}
>
{loading ? (
<ActivityIndicator color={variant === "outline" ? theme.colors.primary : "#FFFFFF"} size="small" />
) : typeof children === "string" ? (
<Text style={{ color: textColors[variant], fontWeight: "600", fontSize: size === "sm" ? 14 : 16 }}>
{children}
</Text>
) : (
children
)}
</Pressable>
);
}