@beyondcorp/beyond-ui-native 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-2B6HTSGM.mjs → chunk-RNHEGFYR.mjs} +16 -2
- package/dist/chunk-RNHEGFYR.mjs.map +1 -0
- package/dist/index.d.mts +2 -145
- package/dist/index.d.ts +2 -145
- package/dist/index.js +37 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -8
- package/dist/index.mjs.map +1 -1
- package/dist/preset-BmUHLoAa.d.mts +274 -0
- package/dist/preset-BmUHLoAa.d.ts +274 -0
- package/dist/preset.d.mts +1 -117
- package/dist/preset.d.ts +1 -117
- package/dist/preset.js +19 -3
- package/dist/preset.js.map +1 -1
- package/dist/preset.mjs +5 -3
- package/package.json +11 -5
- package/src/components/accordion/accordion.tsx +245 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/alert/alert.tsx +118 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/avatar/avatar.tsx +125 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/badge/badge.tsx +89 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/bottom-sheet/bottom-sheet.tsx +336 -0
- package/src/components/bottom-sheet/index.ts +1 -0
- package/src/components/button/button.tsx +172 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/card/card.tsx +121 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/checkbox/checkbox.tsx +100 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/data-table/data-table.tsx +277 -0
- package/src/components/data-table/index.ts +1 -0
- package/src/components/dialog/dialog.tsx +278 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.tsx +130 -0
- package/src/components/radio/index.ts +1 -0
- package/src/components/radio/radio.tsx +117 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.tsx +375 -0
- package/src/components/separator/index.ts +1 -0
- package/src/components/separator/separator.tsx +28 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.tsx +43 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/spinner/spinner.tsx +46 -0
- package/src/components/stats-card/index.ts +1 -0
- package/src/components/stats-card/stats-card.tsx +92 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +80 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.tsx +172 -0
- package/src/components/text/heading.tsx +50 -0
- package/src/components/text/index.ts +2 -0
- package/src/components/text/text.tsx +62 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.tsx +87 -0
- package/src/components/toast/index.ts +2 -0
- package/src/components/toast/toast.tsx +136 -0
- package/src/components/toast/types.ts +32 -0
- package/src/index.ts +42 -0
- package/src/primitives/box.tsx +25 -0
- package/src/primitives/index.ts +2 -0
- package/src/primitives/pressable.tsx +49 -0
- package/src/providers/BeyondUIProvider.tsx +100 -0
- package/src/templates/auth/ForgotPasswordTemplate.tsx +132 -0
- package/src/templates/auth/SignInTemplate.tsx +213 -0
- package/src/templates/auth/SignUpTemplate.tsx +233 -0
- package/src/templates/auth/index.ts +3 -0
- package/src/templates/dashboard/DashboardTemplate.tsx +205 -0
- package/src/templates/dashboard/index.ts +1 -0
- package/src/templates/index.ts +3 -0
- package/src/templates/settings/SettingsTemplate.tsx +226 -0
- package/src/templates/settings/index.ts +1 -0
- package/src/theme/preset.ts +45 -0
- package/src/theme/tokens.ts +144 -0
- package/src/types/nativewind-env.d.ts +26 -0
- package/src/utilities/accessibility.ts +43 -0
- package/src/utilities/cn.ts +9 -0
- package/src/utilities/index.ts +2 -0
- package/dist/chunk-2B6HTSGM.mjs.map +0 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { View, useColorScheme as useRNColorScheme } from "react-native";
|
|
3
|
+
import { PortalHost } from "@rn-primitives/portal";
|
|
4
|
+
import { ToastContext, ToastItem, ToastContextValue } from "../components/toast/types";
|
|
5
|
+
import { ToastContainer } from "../components/toast/toast";
|
|
6
|
+
|
|
7
|
+
export type ColorScheme = "light" | "dark" | "system";
|
|
8
|
+
|
|
9
|
+
export interface BeyondUIContextValue {
|
|
10
|
+
colorScheme: "light" | "dark";
|
|
11
|
+
setColorScheme: (scheme: ColorScheme) => void;
|
|
12
|
+
toggleColorScheme: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const BeyondUIContext = React.createContext<BeyondUIContextValue>({
|
|
16
|
+
colorScheme: "light",
|
|
17
|
+
setColorScheme: () => {},
|
|
18
|
+
toggleColorScheme: () => {},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function useBeyondUI() {
|
|
22
|
+
return React.useContext(BeyondUIContext);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BeyondUIProviderProps {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
defaultColorScheme?: ColorScheme;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function BeyondUIProvider({
|
|
31
|
+
children,
|
|
32
|
+
defaultColorScheme = "system",
|
|
33
|
+
}: BeyondUIProviderProps) {
|
|
34
|
+
const systemScheme = useRNColorScheme() ?? "light";
|
|
35
|
+
const [selectedScheme, setSelectedScheme] = React.useState<ColorScheme>(defaultColorScheme);
|
|
36
|
+
const [toasts, setToasts] = React.useState<ToastItem[]>([]);
|
|
37
|
+
|
|
38
|
+
const activeScheme: "light" | "dark" =
|
|
39
|
+
selectedScheme === "system" ? systemScheme : selectedScheme;
|
|
40
|
+
|
|
41
|
+
const toggleColorScheme = React.useCallback(() => {
|
|
42
|
+
setSelectedScheme((prev) => {
|
|
43
|
+
const current = prev === "system" ? systemScheme : prev;
|
|
44
|
+
return current === "dark" ? "light" : "dark";
|
|
45
|
+
});
|
|
46
|
+
}, [systemScheme]);
|
|
47
|
+
|
|
48
|
+
const dismissToast = React.useCallback((id: string) => {
|
|
49
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const showToast = React.useCallback((toast: Omit<ToastItem, "id">) => {
|
|
53
|
+
const id = Math.random().toString(36).substring(2, 9);
|
|
54
|
+
setToasts((prev) => [...prev, { ...toast, id }]);
|
|
55
|
+
return id;
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const toastContextValue: ToastContextValue = React.useMemo(
|
|
59
|
+
() => ({
|
|
60
|
+
toasts,
|
|
61
|
+
show: showToast,
|
|
62
|
+
dismiss: dismissToast,
|
|
63
|
+
success: (title, description) =>
|
|
64
|
+
showToast({ title, description, variant: "success" }),
|
|
65
|
+
danger: (title, description) =>
|
|
66
|
+
showToast({ title, description, variant: "danger" }),
|
|
67
|
+
error: (title, description) =>
|
|
68
|
+
showToast({ title, description, variant: "danger" }),
|
|
69
|
+
warning: (title, description) =>
|
|
70
|
+
showToast({ title, description, variant: "warning" }),
|
|
71
|
+
info: (title, description) =>
|
|
72
|
+
showToast({ title, description, variant: "info" }),
|
|
73
|
+
}),
|
|
74
|
+
[toasts, showToast, dismissToast]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const contextValue: BeyondUIContextValue = React.useMemo(
|
|
78
|
+
() => ({
|
|
79
|
+
colorScheme: activeScheme,
|
|
80
|
+
setColorScheme: setSelectedScheme,
|
|
81
|
+
toggleColorScheme,
|
|
82
|
+
}),
|
|
83
|
+
[activeScheme, toggleColorScheme]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<BeyondUIContext.Provider value={contextValue}>
|
|
88
|
+
<ToastContext.Provider value={toastContextValue}>
|
|
89
|
+
<View
|
|
90
|
+
style={{ flex: 1 }}
|
|
91
|
+
className={activeScheme === "dark" ? "dark flex-1" : "flex-1"}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
<PortalHost />
|
|
95
|
+
<ToastContainer toasts={toasts} onDismiss={dismissToast} />
|
|
96
|
+
</View>
|
|
97
|
+
</ToastContext.Provider>
|
|
98
|
+
</BeyondUIContext.Provider>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
ScrollView,
|
|
5
|
+
KeyboardAvoidingView,
|
|
6
|
+
Platform,
|
|
7
|
+
Pressable,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import { cn } from "../../utilities/cn";
|
|
10
|
+
import { Text } from "../../components/text/text";
|
|
11
|
+
import { Heading } from "../../components/text/heading";
|
|
12
|
+
import { Input } from "../../components/input/input";
|
|
13
|
+
import { Button } from "../../components/button/button";
|
|
14
|
+
import { Alert, AlertDescription } from "../../components/alert/alert";
|
|
15
|
+
|
|
16
|
+
export interface ForgotPasswordTemplateProps {
|
|
17
|
+
title?: string;
|
|
18
|
+
subtitle?: string;
|
|
19
|
+
onSubmit?: (data: { email: string }) => void;
|
|
20
|
+
onBackToSignIn?: () => void;
|
|
21
|
+
isLoading?: boolean;
|
|
22
|
+
isSuccess?: boolean;
|
|
23
|
+
error?: string | null;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ForgotPasswordTemplate: React.FC<ForgotPasswordTemplateProps> = ({
|
|
28
|
+
title = "Reset Password",
|
|
29
|
+
subtitle = "Enter your email address and we'll send you recovery instructions",
|
|
30
|
+
onSubmit,
|
|
31
|
+
onBackToSignIn,
|
|
32
|
+
isLoading = false,
|
|
33
|
+
isSuccess = false,
|
|
34
|
+
error,
|
|
35
|
+
className,
|
|
36
|
+
}) => {
|
|
37
|
+
const [email, setEmail] = React.useState("");
|
|
38
|
+
const [emailError, setEmailError] = React.useState<string | undefined>();
|
|
39
|
+
|
|
40
|
+
const handleSubmit = () => {
|
|
41
|
+
if (!email || !email.includes("@")) {
|
|
42
|
+
setEmailError("Please enter a valid email address");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setEmailError(undefined);
|
|
46
|
+
onSubmit?.({ email });
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<KeyboardAvoidingView
|
|
51
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
52
|
+
style={{ flex: 1 }}
|
|
53
|
+
className={cn("flex-1 bg-background", className)}
|
|
54
|
+
>
|
|
55
|
+
<ScrollView
|
|
56
|
+
style={{ flex: 1 }}
|
|
57
|
+
contentContainerStyle={{ flexGrow: 1, justifyContent: "center" }}
|
|
58
|
+
className="px-6 py-8"
|
|
59
|
+
keyboardShouldPersistTaps="handled"
|
|
60
|
+
showsVerticalScrollIndicator={false}
|
|
61
|
+
>
|
|
62
|
+
<View className="w-full max-w-md mx-auto gap-6">
|
|
63
|
+
<View className="items-center gap-2">
|
|
64
|
+
<Heading level={2} className="text-3xl font-extrabold text-foreground text-center">
|
|
65
|
+
{title}
|
|
66
|
+
</Heading>
|
|
67
|
+
<Text className="text-muted text-center text-base">
|
|
68
|
+
{subtitle}
|
|
69
|
+
</Text>
|
|
70
|
+
</View>
|
|
71
|
+
|
|
72
|
+
{error && (
|
|
73
|
+
<Alert variant="danger">
|
|
74
|
+
<AlertDescription>{error}</AlertDescription>
|
|
75
|
+
</Alert>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{isSuccess ? (
|
|
79
|
+
<View className="gap-4">
|
|
80
|
+
<Alert variant="success">
|
|
81
|
+
<AlertDescription>
|
|
82
|
+
Password reset link sent! Check your email inbox.
|
|
83
|
+
</AlertDescription>
|
|
84
|
+
</Alert>
|
|
85
|
+
|
|
86
|
+
{onBackToSignIn && (
|
|
87
|
+
<Button variant="primary" size="lg" onPress={onBackToSignIn}>
|
|
88
|
+
Back to Sign In
|
|
89
|
+
</Button>
|
|
90
|
+
)}
|
|
91
|
+
</View>
|
|
92
|
+
) : (
|
|
93
|
+
<View className="gap-4">
|
|
94
|
+
<Input
|
|
95
|
+
label="Email Address"
|
|
96
|
+
placeholder="you@company.com"
|
|
97
|
+
keyboardType="email-address"
|
|
98
|
+
autoCapitalize="none"
|
|
99
|
+
value={email}
|
|
100
|
+
onChangeText={(text) => {
|
|
101
|
+
setEmail(text);
|
|
102
|
+
if (emailError) setEmailError(undefined);
|
|
103
|
+
}}
|
|
104
|
+
error={emailError}
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<Button
|
|
108
|
+
variant="primary"
|
|
109
|
+
size="lg"
|
|
110
|
+
loading={isLoading}
|
|
111
|
+
onPress={handleSubmit}
|
|
112
|
+
className="mt-2"
|
|
113
|
+
>
|
|
114
|
+
Send Reset Link
|
|
115
|
+
</Button>
|
|
116
|
+
|
|
117
|
+
{onBackToSignIn && (
|
|
118
|
+
<View className="flex-row justify-center items-center gap-1.5 pt-2">
|
|
119
|
+
<Pressable onPress={onBackToSignIn}>
|
|
120
|
+
<Text className="text-sm font-semibold text-primary">
|
|
121
|
+
Back to Sign In
|
|
122
|
+
</Text>
|
|
123
|
+
</Pressable>
|
|
124
|
+
</View>
|
|
125
|
+
)}
|
|
126
|
+
</View>
|
|
127
|
+
)}
|
|
128
|
+
</View>
|
|
129
|
+
</ScrollView>
|
|
130
|
+
</KeyboardAvoidingView>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
ScrollView,
|
|
5
|
+
KeyboardAvoidingView,
|
|
6
|
+
Platform,
|
|
7
|
+
Pressable,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import { cn } from "../../utilities/cn";
|
|
10
|
+
import { Text } from "../../components/text/text";
|
|
11
|
+
import { Heading } from "../../components/text/heading";
|
|
12
|
+
import { Input } from "../../components/input/input";
|
|
13
|
+
import { Button } from "../../components/button/button";
|
|
14
|
+
import { Checkbox } from "../../components/checkbox/checkbox";
|
|
15
|
+
import { Alert, AlertDescription } from "../../components/alert/alert";
|
|
16
|
+
import { Separator } from "../../components/separator/separator";
|
|
17
|
+
|
|
18
|
+
export interface SignInTemplateProps {
|
|
19
|
+
title?: string;
|
|
20
|
+
subtitle?: string;
|
|
21
|
+
logo?: React.ReactNode;
|
|
22
|
+
onSubmit?: (data: { email: string; password: string; rememberMe: boolean }) => void;
|
|
23
|
+
onForgotPassword?: () => void;
|
|
24
|
+
onSignUpPress?: () => void;
|
|
25
|
+
onGooglePress?: () => void;
|
|
26
|
+
onApplePress?: () => void;
|
|
27
|
+
isLoading?: boolean;
|
|
28
|
+
error?: string | null;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const SignInTemplate: React.FC<SignInTemplateProps> = ({
|
|
33
|
+
title = "Welcome Back",
|
|
34
|
+
subtitle = "Sign in to your Beyond account to continue",
|
|
35
|
+
logo,
|
|
36
|
+
onSubmit,
|
|
37
|
+
onForgotPassword,
|
|
38
|
+
onSignUpPress,
|
|
39
|
+
onGooglePress,
|
|
40
|
+
onApplePress,
|
|
41
|
+
isLoading = false,
|
|
42
|
+
error,
|
|
43
|
+
className,
|
|
44
|
+
}) => {
|
|
45
|
+
const [email, setEmail] = React.useState("");
|
|
46
|
+
const [password, setPassword] = React.useState("");
|
|
47
|
+
const [showPassword, setShowPassword] = React.useState(false);
|
|
48
|
+
const [rememberMe, setRememberMe] = React.useState(false);
|
|
49
|
+
const [emailError, setEmailError] = React.useState<string | undefined>();
|
|
50
|
+
const [passwordError, setPasswordError] = React.useState<string | undefined>();
|
|
51
|
+
|
|
52
|
+
const handleSubmit = () => {
|
|
53
|
+
let valid = true;
|
|
54
|
+
setEmailError(undefined);
|
|
55
|
+
setPasswordError(undefined);
|
|
56
|
+
|
|
57
|
+
if (!email || !email.includes("@")) {
|
|
58
|
+
setEmailError("Please enter a valid email address");
|
|
59
|
+
valid = false;
|
|
60
|
+
}
|
|
61
|
+
if (!password || password.length < 6) {
|
|
62
|
+
setPasswordError("Password must be at least 6 characters");
|
|
63
|
+
valid = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (valid) {
|
|
67
|
+
onSubmit?.({ email, password, rememberMe });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<KeyboardAvoidingView
|
|
73
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
74
|
+
style={{ flex: 1 }}
|
|
75
|
+
className={cn("flex-1 bg-background", className)}
|
|
76
|
+
>
|
|
77
|
+
<ScrollView
|
|
78
|
+
style={{ flex: 1 }}
|
|
79
|
+
contentContainerStyle={{ flexGrow: 1, justifyContent: "center" }}
|
|
80
|
+
className="px-6 py-8"
|
|
81
|
+
keyboardShouldPersistTaps="handled"
|
|
82
|
+
showsVerticalScrollIndicator={false}
|
|
83
|
+
>
|
|
84
|
+
<View className="w-full max-w-md mx-auto gap-6">
|
|
85
|
+
{/* Logo & Header */}
|
|
86
|
+
<View className="items-center gap-2">
|
|
87
|
+
{logo && <View className="mb-2">{logo}</View>}
|
|
88
|
+
<Heading level={2} className="text-3xl font-extrabold text-foreground text-center">
|
|
89
|
+
{title}
|
|
90
|
+
</Heading>
|
|
91
|
+
<Text className="text-muted text-center text-base">
|
|
92
|
+
{subtitle}
|
|
93
|
+
</Text>
|
|
94
|
+
</View>
|
|
95
|
+
|
|
96
|
+
{/* Error Banner */}
|
|
97
|
+
{error && (
|
|
98
|
+
<Alert variant="danger">
|
|
99
|
+
<AlertDescription>{error}</AlertDescription>
|
|
100
|
+
</Alert>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{/* Fields */}
|
|
104
|
+
<View className="gap-4">
|
|
105
|
+
<Input
|
|
106
|
+
label="Email Address"
|
|
107
|
+
placeholder="you@company.com"
|
|
108
|
+
keyboardType="email-address"
|
|
109
|
+
autoCapitalize="none"
|
|
110
|
+
autoCorrect={false}
|
|
111
|
+
value={email}
|
|
112
|
+
onChangeText={(text) => {
|
|
113
|
+
setEmail(text);
|
|
114
|
+
if (emailError) setEmailError(undefined);
|
|
115
|
+
}}
|
|
116
|
+
error={emailError}
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
<View className="gap-1">
|
|
120
|
+
<Input
|
|
121
|
+
label="Password"
|
|
122
|
+
placeholder="••••••••"
|
|
123
|
+
secureTextEntry={!showPassword}
|
|
124
|
+
value={password}
|
|
125
|
+
onChangeText={(text) => {
|
|
126
|
+
setPassword(text);
|
|
127
|
+
if (passwordError) setPasswordError(undefined);
|
|
128
|
+
}}
|
|
129
|
+
error={passwordError}
|
|
130
|
+
rightElement={
|
|
131
|
+
<Pressable onPress={() => setShowPassword((p) => !p)} className="p-1">
|
|
132
|
+
<Text className="text-xs text-muted font-semibold">
|
|
133
|
+
{showPassword ? "HIDE" : "SHOW"}
|
|
134
|
+
</Text>
|
|
135
|
+
</Pressable>
|
|
136
|
+
}
|
|
137
|
+
/>
|
|
138
|
+
</View>
|
|
139
|
+
|
|
140
|
+
{/* Remember & Forgot */}
|
|
141
|
+
<View className="flex-row items-center justify-between py-1">
|
|
142
|
+
<View className="flex-row items-center gap-2">
|
|
143
|
+
<Checkbox checked={rememberMe} onCheckedChange={setRememberMe} />
|
|
144
|
+
<Text className="text-sm text-foreground">Remember me</Text>
|
|
145
|
+
</View>
|
|
146
|
+
|
|
147
|
+
{onForgotPassword && (
|
|
148
|
+
<Pressable onPress={onForgotPassword}>
|
|
149
|
+
<Text className="text-sm font-semibold text-primary">
|
|
150
|
+
Forgot password?
|
|
151
|
+
</Text>
|
|
152
|
+
</Pressable>
|
|
153
|
+
)}
|
|
154
|
+
</View>
|
|
155
|
+
|
|
156
|
+
{/* Submit */}
|
|
157
|
+
<Button
|
|
158
|
+
variant="primary"
|
|
159
|
+
size="lg"
|
|
160
|
+
loading={isLoading}
|
|
161
|
+
onPress={handleSubmit}
|
|
162
|
+
className="mt-2"
|
|
163
|
+
>
|
|
164
|
+
Sign In
|
|
165
|
+
</Button>
|
|
166
|
+
</View>
|
|
167
|
+
|
|
168
|
+
{/* Social Logins */}
|
|
169
|
+
{(onGooglePress || onApplePress) && (
|
|
170
|
+
<View className="gap-4">
|
|
171
|
+
<View className="flex-row items-center gap-3">
|
|
172
|
+
<Separator className="flex-1" />
|
|
173
|
+
<Text className="text-xs text-muted uppercase">or continue with</Text>
|
|
174
|
+
<Separator className="flex-1" />
|
|
175
|
+
</View>
|
|
176
|
+
|
|
177
|
+
<View className="flex-row gap-3">
|
|
178
|
+
{onGooglePress && (
|
|
179
|
+
<Button
|
|
180
|
+
variant="outline"
|
|
181
|
+
className="flex-1"
|
|
182
|
+
onPress={onGooglePress}
|
|
183
|
+
>
|
|
184
|
+
Google
|
|
185
|
+
</Button>
|
|
186
|
+
)}
|
|
187
|
+
{onApplePress && (
|
|
188
|
+
<Button
|
|
189
|
+
variant="outline"
|
|
190
|
+
className="flex-1"
|
|
191
|
+
onPress={onApplePress}
|
|
192
|
+
>
|
|
193
|
+
Apple
|
|
194
|
+
</Button>
|
|
195
|
+
)}
|
|
196
|
+
</View>
|
|
197
|
+
</View>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{/* Switch to SignUp */}
|
|
201
|
+
{onSignUpPress && (
|
|
202
|
+
<View className="flex-row justify-center items-center gap-1.5 pt-2">
|
|
203
|
+
<Text className="text-sm text-muted">Don't have an account?</Text>
|
|
204
|
+
<Pressable onPress={onSignUpPress}>
|
|
205
|
+
<Text className="text-sm font-bold text-primary">Sign Up</Text>
|
|
206
|
+
</Pressable>
|
|
207
|
+
</View>
|
|
208
|
+
)}
|
|
209
|
+
</View>
|
|
210
|
+
</ScrollView>
|
|
211
|
+
</KeyboardAvoidingView>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
ScrollView,
|
|
5
|
+
KeyboardAvoidingView,
|
|
6
|
+
Platform,
|
|
7
|
+
Pressable,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import { cn } from "../../utilities/cn";
|
|
10
|
+
import { Text } from "../../components/text/text";
|
|
11
|
+
import { Heading } from "../../components/text/heading";
|
|
12
|
+
import { Input } from "../../components/input/input";
|
|
13
|
+
import { Button } from "../../components/button/button";
|
|
14
|
+
import { Checkbox } from "../../components/checkbox/checkbox";
|
|
15
|
+
import { Alert, AlertDescription } from "../../components/alert/alert";
|
|
16
|
+
import { Separator } from "../../components/separator/separator";
|
|
17
|
+
|
|
18
|
+
export interface SignUpTemplateProps {
|
|
19
|
+
title?: string;
|
|
20
|
+
subtitle?: string;
|
|
21
|
+
logo?: React.ReactNode;
|
|
22
|
+
onSubmit?: (data: {
|
|
23
|
+
fullName: string;
|
|
24
|
+
email: string;
|
|
25
|
+
password: string;
|
|
26
|
+
agreeToTerms: boolean;
|
|
27
|
+
}) => void;
|
|
28
|
+
onSignInPress?: () => void;
|
|
29
|
+
onGooglePress?: () => void;
|
|
30
|
+
onApplePress?: () => void;
|
|
31
|
+
isLoading?: boolean;
|
|
32
|
+
error?: string | null;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const SignUpTemplate: React.FC<SignUpTemplateProps> = ({
|
|
37
|
+
title = "Create Account",
|
|
38
|
+
subtitle = "Get started with your free Beyond account",
|
|
39
|
+
logo,
|
|
40
|
+
onSubmit,
|
|
41
|
+
onSignInPress,
|
|
42
|
+
onGooglePress,
|
|
43
|
+
onApplePress,
|
|
44
|
+
isLoading = false,
|
|
45
|
+
error,
|
|
46
|
+
className,
|
|
47
|
+
}) => {
|
|
48
|
+
const [fullName, setFullName] = React.useState("");
|
|
49
|
+
const [email, setEmail] = React.useState("");
|
|
50
|
+
const [password, setPassword] = React.useState("");
|
|
51
|
+
const [agreeToTerms, setAgreeToTerms] = React.useState(false);
|
|
52
|
+
|
|
53
|
+
const [nameError, setNameError] = React.useState<string | undefined>();
|
|
54
|
+
const [emailError, setEmailError] = React.useState<string | undefined>();
|
|
55
|
+
const [passwordError, setPasswordError] = React.useState<string | undefined>();
|
|
56
|
+
const [termsError, setTermsError] = React.useState<string | undefined>();
|
|
57
|
+
|
|
58
|
+
const handleSubmit = () => {
|
|
59
|
+
let valid = true;
|
|
60
|
+
setNameError(undefined);
|
|
61
|
+
setEmailError(undefined);
|
|
62
|
+
setPasswordError(undefined);
|
|
63
|
+
setTermsError(undefined);
|
|
64
|
+
|
|
65
|
+
if (!fullName.trim()) {
|
|
66
|
+
setNameError("Full name is required");
|
|
67
|
+
valid = false;
|
|
68
|
+
}
|
|
69
|
+
if (!email || !email.includes("@")) {
|
|
70
|
+
setEmailError("Please enter a valid email address");
|
|
71
|
+
valid = false;
|
|
72
|
+
}
|
|
73
|
+
if (!password || password.length < 8) {
|
|
74
|
+
setPasswordError("Password must be at least 8 characters");
|
|
75
|
+
valid = false;
|
|
76
|
+
}
|
|
77
|
+
if (!agreeToTerms) {
|
|
78
|
+
setTermsError("You must agree to the Terms of Service");
|
|
79
|
+
valid = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (valid) {
|
|
83
|
+
onSubmit?.({ fullName, email, password, agreeToTerms });
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<KeyboardAvoidingView
|
|
89
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
90
|
+
style={{ flex: 1 }}
|
|
91
|
+
className={cn("flex-1 bg-background", className)}
|
|
92
|
+
>
|
|
93
|
+
<ScrollView
|
|
94
|
+
style={{ flex: 1 }}
|
|
95
|
+
contentContainerStyle={{ flexGrow: 1, justifyContent: "center" }}
|
|
96
|
+
className="px-6 py-8"
|
|
97
|
+
keyboardShouldPersistTaps="handled"
|
|
98
|
+
showsVerticalScrollIndicator={false}
|
|
99
|
+
>
|
|
100
|
+
<View className="w-full max-w-md mx-auto gap-6">
|
|
101
|
+
{/* Header */}
|
|
102
|
+
<View className="items-center gap-2">
|
|
103
|
+
{logo && <View className="mb-2">{logo}</View>}
|
|
104
|
+
<Heading level={2} className="text-3xl font-extrabold text-foreground text-center">
|
|
105
|
+
{title}
|
|
106
|
+
</Heading>
|
|
107
|
+
<Text className="text-muted text-center text-base">
|
|
108
|
+
{subtitle}
|
|
109
|
+
</Text>
|
|
110
|
+
</View>
|
|
111
|
+
|
|
112
|
+
{/* Error Alert */}
|
|
113
|
+
{error && (
|
|
114
|
+
<Alert variant="danger">
|
|
115
|
+
<AlertDescription>{error}</AlertDescription>
|
|
116
|
+
</Alert>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{/* Form */}
|
|
120
|
+
<View className="gap-4">
|
|
121
|
+
<Input
|
|
122
|
+
label="Full Name"
|
|
123
|
+
placeholder="Mark Otto"
|
|
124
|
+
value={fullName}
|
|
125
|
+
onChangeText={(text) => {
|
|
126
|
+
setFullName(text);
|
|
127
|
+
if (nameError) setNameError(undefined);
|
|
128
|
+
}}
|
|
129
|
+
error={nameError}
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
<Input
|
|
133
|
+
label="Email Address"
|
|
134
|
+
placeholder="you@company.com"
|
|
135
|
+
keyboardType="email-address"
|
|
136
|
+
autoCapitalize="none"
|
|
137
|
+
value={email}
|
|
138
|
+
onChangeText={(text) => {
|
|
139
|
+
setEmail(text);
|
|
140
|
+
if (emailError) setEmailError(undefined);
|
|
141
|
+
}}
|
|
142
|
+
error={emailError}
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
<Input
|
|
146
|
+
label="Password"
|
|
147
|
+
placeholder="At least 8 characters"
|
|
148
|
+
secureTextEntry
|
|
149
|
+
value={password}
|
|
150
|
+
onChangeText={(text) => {
|
|
151
|
+
setPassword(text);
|
|
152
|
+
if (passwordError) setPasswordError(undefined);
|
|
153
|
+
}}
|
|
154
|
+
error={passwordError}
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
{/* Terms checkbox */}
|
|
158
|
+
<View className="gap-1">
|
|
159
|
+
<View className="flex-row items-center gap-2 pt-1">
|
|
160
|
+
<Checkbox
|
|
161
|
+
checked={agreeToTerms}
|
|
162
|
+
onCheckedChange={(checked) => {
|
|
163
|
+
setAgreeToTerms(checked);
|
|
164
|
+
if (termsError) setTermsError(undefined);
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
<Text className="text-sm text-foreground flex-1">
|
|
168
|
+
I agree to the Terms of Service & Privacy Policy
|
|
169
|
+
</Text>
|
|
170
|
+
</View>
|
|
171
|
+
{termsError && (
|
|
172
|
+
<Text className="text-xs text-danger font-medium">{termsError}</Text>
|
|
173
|
+
)}
|
|
174
|
+
</View>
|
|
175
|
+
|
|
176
|
+
{/* Submit */}
|
|
177
|
+
<Button
|
|
178
|
+
variant="primary"
|
|
179
|
+
size="lg"
|
|
180
|
+
loading={isLoading}
|
|
181
|
+
onPress={handleSubmit}
|
|
182
|
+
className="mt-2"
|
|
183
|
+
>
|
|
184
|
+
Create Account
|
|
185
|
+
</Button>
|
|
186
|
+
</View>
|
|
187
|
+
|
|
188
|
+
{/* Social Logins */}
|
|
189
|
+
{(onGooglePress || onApplePress) && (
|
|
190
|
+
<View className="gap-4">
|
|
191
|
+
<View className="flex-row items-center gap-3">
|
|
192
|
+
<Separator className="flex-1" />
|
|
193
|
+
<Text className="text-xs text-muted uppercase">or continue with</Text>
|
|
194
|
+
<Separator className="flex-1" />
|
|
195
|
+
</View>
|
|
196
|
+
|
|
197
|
+
<View className="flex-row gap-3">
|
|
198
|
+
{onGooglePress && (
|
|
199
|
+
<Button
|
|
200
|
+
variant="outline"
|
|
201
|
+
className="flex-1"
|
|
202
|
+
onPress={onGooglePress}
|
|
203
|
+
>
|
|
204
|
+
Google
|
|
205
|
+
</Button>
|
|
206
|
+
)}
|
|
207
|
+
{onApplePress && (
|
|
208
|
+
<Button
|
|
209
|
+
variant="outline"
|
|
210
|
+
className="flex-1"
|
|
211
|
+
onPress={onApplePress}
|
|
212
|
+
>
|
|
213
|
+
Apple
|
|
214
|
+
</Button>
|
|
215
|
+
)}
|
|
216
|
+
</View>
|
|
217
|
+
</View>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Back to SignIn */}
|
|
221
|
+
{onSignInPress && (
|
|
222
|
+
<View className="flex-row justify-center items-center gap-1.5 pt-2">
|
|
223
|
+
<Text className="text-sm text-muted">Already have an account?</Text>
|
|
224
|
+
<Pressable onPress={onSignInPress}>
|
|
225
|
+
<Text className="text-sm font-bold text-primary">Sign In</Text>
|
|
226
|
+
</Pressable>
|
|
227
|
+
</View>
|
|
228
|
+
)}
|
|
229
|
+
</View>
|
|
230
|
+
</ScrollView>
|
|
231
|
+
</KeyboardAvoidingView>
|
|
232
|
+
);
|
|
233
|
+
};
|