Skip to content

Commit 36f195b

Browse files
authored
Merge pull request #113 from woustachemax/feat/toast-feature
toast-feature
2 parents 1525dd4 + 259f8f8 commit 36f195b

File tree

6 files changed

+9350
-0
lines changed

6 files changed

+9350
-0
lines changed
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
import { ComponentPreview } from "@/components/docs/component-preview";
2+
3+
export default function ToastPage() {
4+
return (
5+
<ComponentPreview
6+
name="Toast"
7+
description="Display temporary notification messages to users."
8+
examples={[
9+
{
10+
title: "Default",
11+
value: "default",
12+
content:
13+
'import { Button } from "@nativeui/ui";\nimport { ToastProvider, useToast } from "@nativeui/ui";\nimport { Text } from "react-native";\n\nfunction ToastDemo() {\n const { show } = useToast();\n \n return (\n <Button onPress={() => show("Your changes have been saved")}>\n <Text>Show Toast</Text>\n </Button>\n );\n}\n\nexport default function App() {\n return (\n <ToastProvider>\n <ToastDemo />\n </ToastProvider>\n );\n}',
14+
language: "tsx",
15+
},
16+
{
17+
title: "Variants",
18+
value: "variants",
19+
content:
20+
'import { Button } from "@nativeui/ui";\nimport { ToastProvider, useToast } from "@nativeui/ui";\nimport { View, Text } from "react-native";\n\nfunction ToastVariants() {\n const { show } = useToast();\n \n return (\n <View className="flex flex-col gap-4">\n <Button onPress={() => show("Default notification")}>\n <Text>Default</Text>\n </Button>\n <Button onPress={() => show("Success!", "success")}>\n <Text>Success</Text>\n </Button>\n <Button onPress={() => show("Error occurred", "error")}>\n <Text>Error</Text>\n </Button>\n <Button onPress={() => show("Warning message", "warning")}>\n <Text>Warning</Text>\n </Button>\n <Button onPress={() => show("Info message", "info")}>\n <Text>Info</Text>\n </Button>\n </View>\n );\n}\n\nexport default function App() {\n return (\n <ToastProvider>\n <ToastVariants />\n </ToastProvider>\n );\n}',
21+
language: "tsx",
22+
},
23+
{
24+
title: "Custom Duration & Position",
25+
value: "custom",
26+
content:
27+
'import { Button } from "@nativeui/ui";\nimport { ToastProvider, useToast } from "@nativeui/ui";\nimport { View, Text } from "react-native";\n\nfunction ToastCustom() {\n const { show } = useToast();\n \n return (\n <View className="flex flex-col gap-4">\n <Button onPress={() => show("This toast appears at the top")}>\n <Text>Top Position</Text>\n </Button>\n </View>\n );\n}\n\nexport default function App() {\n return (\n <ToastProvider duration={5000} position="top">\n <ToastCustom />\n </ToastProvider>\n );\n}',
28+
language: "tsx",
29+
},
30+
]}
31+
componentCode={`import { cn } from "@/lib/utils";
32+
import { cva, type VariantProps } from "class-variance-authority";
33+
import * as React from "react";
34+
import { Animated, StyleSheet, Text, View } from "react-native";
35+
36+
type ToastType = "default" | "success" | "error" | "warning" | "info";
37+
38+
interface ToastContextValue {
39+
show: (message: string, type?: ToastType) => void;
40+
}
41+
42+
const ToastContext = React.createContext<ToastContextValue | undefined>(
43+
undefined,
44+
);
45+
46+
export const useToast = () => {
47+
const context = React.useContext(ToastContext);
48+
if (!context) {
49+
throw new Error("useToast must be used within a ToastProvider");
50+
}
51+
return context;
52+
};
53+
54+
const toastVariants = cva("px-4 py-3 rounded-lg shadow-lg border", {
55+
variants: {
56+
variant: {
57+
default: "bg-background border-border",
58+
success:
59+
"bg-green-50 border-green-200 dark:bg-green-950 dark:border-green-800",
60+
error:
61+
"bg-red-50 border-red-200 dark:bg-red-950 dark:border-red-800",
62+
warning:
63+
"bg-yellow-50 border-yellow-200 dark:bg-yellow-950 dark:border-yellow-800",
64+
info: "bg-blue-50 border-blue-200 dark:bg-blue-950 dark:border-blue-800",
65+
},
66+
},
67+
defaultVariants: {
68+
variant: "default",
69+
},
70+
});
71+
72+
const toastTextVariants = cva("text-sm font-medium", {
73+
variants: {
74+
variant: {
75+
default: "text-foreground",
76+
success: "text-green-900 dark:text-green-100",
77+
error: "text-red-900 dark:text-red-100",
78+
warning: "text-yellow-900 dark:text-yellow-100",
79+
info: "text-blue-900 dark:text-blue-100",
80+
},
81+
},
82+
defaultVariants: {
83+
variant: "default",
84+
},
85+
});
86+
87+
interface ToastProviderProps {
88+
children: React.ReactNode;
89+
duration?: number;
90+
position?: "top" | "bottom";
91+
}
92+
93+
export function ToastProvider({
94+
children,
95+
duration = 3000,
96+
position = "bottom",
97+
}: ToastProviderProps) {
98+
const [toast, setToast] = React.useState<{
99+
message: string;
100+
type: ToastType;
101+
} | null>(null);
102+
const fadeAnim = React.useRef(new Animated.Value(0)).current;
103+
const timeoutRef = React.useRef<NodeJS.Timeout | undefined>(undefined);
104+
105+
const show = React.useCallback(
106+
(message: string, type: ToastType = "default") => {
107+
if (timeoutRef.current) {
108+
clearTimeout(timeoutRef.current);
109+
}
110+
111+
setToast({ message, type });
112+
113+
Animated.timing(fadeAnim, {
114+
toValue: 1,
115+
duration: 200,
116+
useNativeDriver: true,
117+
}).start(() => {
118+
timeoutRef.current = setTimeout(() => {
119+
Animated.timing(fadeAnim, {
120+
toValue: 0,
121+
duration: 200,
122+
useNativeDriver: true,
123+
}).start(() => setToast(null));
124+
}, duration);
125+
});
126+
},
127+
[fadeAnim, duration],
128+
);
129+
130+
React.useEffect(() => {
131+
return () => {
132+
if (timeoutRef.current) {
133+
clearTimeout(timeoutRef.current);
134+
}
135+
};
136+
}, []);
137+
138+
const positionStyle = position === "top" ? { top: 50 } : { bottom: 50 };
139+
140+
return (
141+
<ToastContext.Provider value={{ show }}>
142+
{children}
143+
{toast && (
144+
<Animated.View
145+
style={[
146+
styles.container,
147+
positionStyle,
148+
{
149+
opacity: fadeAnim,
150+
transform: [
151+
{
152+
translateY: fadeAnim.interpolate({
153+
inputRange: [0, 1],
154+
outputRange: position === "top" ? [-40, 0] : [40, 0],
155+
}),
156+
},
157+
],
158+
},
159+
]}
160+
>
161+
<View className={cn(toastVariants({ variant: toast.type }))}>
162+
<Text className={cn(toastTextVariants({ variant: toast.type }))}>
163+
{toast.message}
164+
</Text>
165+
</View>
166+
</Animated.View>
167+
)}
168+
</ToastContext.Provider>
169+
);
170+
}
171+
172+
const styles = StyleSheet.create({
173+
container: {
174+
position: "absolute",
175+
alignSelf: "center",
176+
left: 0,
177+
right: 0,
178+
alignItems: "center",
179+
paddingHorizontal: 16,
180+
zIndex: 9999,
181+
},
182+
});
183+
184+
export { toastVariants, toastTextVariants };
185+
export type { ToastType, ToastProviderProps };
186+
`}
187+
previewCode={`import { Button } from "@/components/ui/button";
188+
import { ToastProvider, useToast } from "@/components/ui/toast";
189+
import { Feather } from "@expo/vector-icons";
190+
import { useColorScheme } from "nativewind";
191+
import React from "react";
192+
import { ScrollView, Text, View } from "react-native";
193+
import { SafeAreaView } from "react-native-safe-area-context";
194+
195+
function ToastDemo() {
196+
const { show } = useToast();
197+
const { colorScheme } = useColorScheme();
198+
const isDark = colorScheme === "dark";
199+
200+
return (
201+
<SafeAreaView className="flex-1 bg-background" edges={["bottom"]}>
202+
<ScrollView className="px-5 py-5">
203+
<View className="mb-6">
204+
<Text className="text-2xl font-bold mb-2 text-foreground">Toast</Text>
205+
<Text className="text-base mb-4 text-muted-foreground">
206+
Display temporary notification messages
207+
</Text>
208+
<Text className="text-base mb-4 text-foreground">
209+
Current mode: {isDark ? "dark" : "light"}
210+
</Text>
211+
</View>
212+
213+
<View className="mb-6">
214+
<Text className="text-xl font-semibold mb-4 text-foreground">
215+
Toast Variants
216+
</Text>
217+
218+
<View className="gap-4">
219+
<View className="flex-row gap-3 flex-wrap">
220+
<Button onPress={() => show("This is a default toast")}>
221+
<Text className="text-primary-foreground dark:text-primary-foreground">
222+
Default
223+
</Text>
224+
</Button>
225+
226+
<Button
227+
variant="outline"
228+
onPress={() => show("Operation successful!", "success")}
229+
>
230+
<Feather
231+
name="check-circle"
232+
size={16}
233+
color={isDark ? "white" : "#111827"}
234+
/>
235+
<Text className="text-foreground dark:text-foreground ml-2">
236+
Success
237+
</Text>
238+
</Button>
239+
240+
<Button
241+
variant="destructive"
242+
onPress={() => show("Something went wrong", "error")}
243+
>
244+
<Feather
245+
name="alert-circle"
246+
size={16}
247+
color={isDark ? "#111827" : "white"}
248+
/>
249+
<Text className="text-destructive-foreground dark:text-destructive-foreground ml-2">
250+
Error
251+
</Text>
252+
</Button>
253+
</View>
254+
255+
<View className="flex-row gap-3 flex-wrap">
256+
<Button
257+
variant="secondary"
258+
onPress={() => show("Please review your changes", "warning")}
259+
>
260+
<Feather
261+
name="alert-triangle"
262+
size={16}
263+
color={isDark ? "white" : "#111827"}
264+
/>
265+
<Text className="text-secondary-foreground dark:text-secondary-foreground ml-2">
266+
Warning
267+
</Text>
268+
</Button>
269+
270+
<Button
271+
variant="ghost"
272+
onPress={() => show("Here's some information", "info")}
273+
>
274+
<Feather
275+
name="info"
276+
size={16}
277+
color={isDark ? "white" : "#111827"}
278+
/>
279+
<Text className="text-foreground dark:text-foreground ml-2">
280+
Info
281+
</Text>
282+
</Button>
283+
</View>
284+
</View>
285+
</View>
286+
287+
<View className="mb-6">
288+
<Text className="text-xl font-semibold mb-4 text-foreground">
289+
Toast with Actions
290+
</Text>
291+
292+
<View className="gap-4">
293+
<View className="flex-row gap-3 flex-wrap">
294+
<Button
295+
onPress={async () => {
296+
show("Processing...", "info");
297+
await new Promise((resolve) => setTimeout(resolve, 1500));
298+
show("Changes saved successfully!", "success");
299+
}}
300+
>
301+
<Feather
302+
name="save"
303+
size={16}
304+
color={isDark ? "#111827" : "white"}
305+
/>
306+
<Text className="text-primary-foreground dark:text-primary-foreground ml-2">
307+
Save Changes
308+
</Text>
309+
</Button>
310+
311+
<Button
312+
variant="outline"
313+
onPress={async () => {
314+
show("Uploading file...", "info");
315+
await new Promise((resolve) => setTimeout(resolve, 2000));
316+
show("File uploaded!", "success");
317+
}}
318+
>
319+
<Feather
320+
name="upload"
321+
size={16}
322+
color={isDark ? "white" : "#111827"}
323+
/>
324+
<Text className="text-foreground dark:text-foreground ml-2">
325+
Upload File
326+
</Text>
327+
</Button>
328+
</View>
329+
</View>
330+
</View>
331+
</ScrollView>
332+
</SafeAreaView>
333+
);
334+
}
335+
336+
export default function ToastPreview() {
337+
return (
338+
<ToastProvider duration={3000} position="bottom">
339+
<ToastDemo />
340+
</ToastProvider>
341+
);
342+
}
343+
`}
344+
registryName="toast"
345+
packageName="@nativeui/ui"
346+
dependencies={["react-native", "class-variance-authority"]}
347+
changelog={[]}
348+
/>
349+
);
350+
}

0 commit comments

Comments
 (0)