diff options
| author | Saumit <justsaumit@protonmail.com> | 2025-09-27 02:14:26 +0530 |
|---|---|---|
| committer | Saumit <justsaumit@protonmail.com> | 2025-09-27 02:14:26 +0530 |
| commit | 82e03978b89938219958032efb1448cc76baa181 (patch) | |
| tree | 626f3e54d52ecd49be0ed3bee30abacc0453d081 /src/react-native-app/components | |
Initial snapshot - OpenTelemetry demo 2.1.3 -f
Diffstat (limited to 'src/react-native-app/components')
14 files changed, 628 insertions, 0 deletions
diff --git a/src/react-native-app/components/CheckoutForm/CheckoutForm.tsx b/src/react-native-app/components/CheckoutForm/CheckoutForm.tsx new file mode 100644 index 0000000..378246a --- /dev/null +++ b/src/react-native-app/components/CheckoutForm/CheckoutForm.tsx @@ -0,0 +1,198 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +/** + * Copied with modification from src/frontend/components/CheckoutForm/CheckoutForm.tsx + */ +import { ThemedScrollView } from "@/components/ThemedScrollView"; +import { Field } from "@/components/Field"; +import { StyleSheet, Pressable } from "react-native"; +import { useForm, Controller } from "react-hook-form"; +import { ThemedText } from "@/components/ThemedText"; +import { ThemedView } from "@/components/ThemedView"; + +export interface IFormData { + email: string; + streetAddress: string; + city: string; + state: string; + country: string; + zipCode: string; + creditCardNumber: string; + creditCardCvv: number; + creditCardExpirationYear: number; + creditCardExpirationMonth: number; +} + +interface IProps { + onSubmit(formData: IFormData): void; +} + +const CheckoutForm = ({ onSubmit }: IProps) => { + const { control, handleSubmit } = useForm({ + defaultValues: { + email: "someone@example.com", + streetAddress: "1600 Amphitheatre Parkway", + city: "Mountain View", + state: "CA", + country: "United States", + zipCode: "94043", + creditCardNumber: "4432-8015-6152-0454", + creditCardCvv: 672, + creditCardExpirationYear: 2030, + creditCardExpirationMonth: 1, + }, + }); + + return ( + <ThemedScrollView style={styles.container}> + <Controller + control={control} + rules={{ required: true }} + render={({ field: { onChange, onBlur, value } }) => ( + <Field + label="E-mail Address" + placeholder="E-mail Address" + onBlur={onBlur} + onChangeText={onChange} + value={value} + /> + )} + name="email" + /> + <Controller + control={control} + rules={{ required: true }} + render={({ field: { onChange, onBlur, value } }) => ( + <Field + label="Street Address" + placeholder="Street Address" + onBlur={onBlur} + onChangeText={onChange} + value={value} + /> + )} + name="streetAddress" + /> + <Controller + control={control} + rules={{ required: true }} + render={({ field: { onChange, onBlur, value } }) => ( + <Field + label="Zip Code" + placeholder="Zip Code" + onBlur={onBlur} + onChangeText={onChange} + value={value} + /> + )} + name="zipCode" + /> + <Controller + control={control} + rules={{ required: true }} + render={({ field: { onChange, onBlur, value } }) => ( + <Field + label="Country" + placeholder="Country" + onBlur={onBlur} + onChangeText={onChange} + value={value} + /> + )} + name="country" + /> + <Controller + control={control} + rules={{ required: true }} + render={({ field: { onChange, onBlur, value } }) => ( + <Field + label="Credit Card Number" + placeholder="Credit Card Number" + onBlur={onBlur} + onChangeText={onChange} + value={value} + keyboardType="numeric" + /> + )} + name="creditCardNumber" + /> + <Controller + control={control} + rules={{ required: true }} + render={({ field: { onChange, onBlur, value } }) => ( + <Field + label="Expiration Month" + placeholder="Month" + onBlur={onBlur} + onChangeText={onChange} + value={value.toString()} + keyboardType="numeric" + /> + )} + name="creditCardExpirationMonth" + /> + <Controller + control={control} + rules={{ required: true }} + render={({ field: { onChange, onBlur, value } }) => ( + <Field + label="Expiration Year" + placeholder="Year" + onBlur={onBlur} + onChangeText={onChange} + value={value.toString()} + keyboardType="numeric" + /> + )} + name="creditCardExpirationYear" + /> + <Controller + control={control} + rules={{ required: true }} + render={({ field: { onChange, onBlur, value } }) => ( + <Field + label="CVV" + placeholder="CVV" + onBlur={onBlur} + onChangeText={onChange} + value={value.toString()} + keyboardType="numeric" + /> + )} + name="creditCardCvv" + /> + <ThemedView style={styles.submitContainer}> + <Pressable style={styles.submit} onPress={handleSubmit(onSubmit)}> + <ThemedText style={styles.submitText}>Place Order</ThemedText> + </Pressable> + </ThemedView> + </ThemedScrollView> + ); +}; + +const styles = StyleSheet.create({ + container: { + marginLeft: 30, + }, + submitContainer: { + display: "flex", + justifyContent: "center", + alignItems: "center", + margin: 20, + }, + submit: { + borderRadius: 4, + backgroundColor: "blue", + alignItems: "center", + justifyContent: "center", + width: 150, + padding: 10, + position: "relative", + }, + submitText: { + color: "white", + fontSize: 20, + }, +}); + +export default CheckoutForm; diff --git a/src/react-native-app/components/CheckoutForm/index.ts b/src/react-native-app/components/CheckoutForm/index.ts new file mode 100644 index 0000000..dc8dd0b --- /dev/null +++ b/src/react-native-app/components/CheckoutForm/index.ts @@ -0,0 +1,3 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +export { default } from "./CheckoutForm"; diff --git a/src/react-native-app/components/EmptyCart/EmptyCart.tsx b/src/react-native-app/components/EmptyCart/EmptyCart.tsx new file mode 100644 index 0000000..962cf28 --- /dev/null +++ b/src/react-native-app/components/EmptyCart/EmptyCart.tsx @@ -0,0 +1,37 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +/** + * Copied with modification from src/frontend/components/Cart/EmptyCart.tsx + */ +import { ThemedView } from "@/components/ThemedView"; +import { ThemedText } from "@/components/ThemedText"; +import { StyleSheet } from "react-native"; + +const EmptyCart = () => { + return ( + <ThemedView style={styles.container}> + <ThemedText style={styles.header}> + Your shopping cart is empty! + </ThemedText> + <ThemedText style={styles.subHeader}> + Items you add to your shopping cart will appear here. + </ThemedText> + </ThemedView> + ); +}; + +export default EmptyCart; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + header: { + fontSize: 20, + }, + subHeader: { + fontSize: 14, + }, +}); diff --git a/src/react-native-app/components/EmptyCart/index.ts b/src/react-native-app/components/EmptyCart/index.ts new file mode 100644 index 0000000..9f421d1 --- /dev/null +++ b/src/react-native-app/components/EmptyCart/index.ts @@ -0,0 +1,3 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +export { default } from "./EmptyCart"; diff --git a/src/react-native-app/components/Field.tsx b/src/react-native-app/components/Field.tsx new file mode 100644 index 0000000..d0f8e96 --- /dev/null +++ b/src/react-native-app/components/Field.tsx @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +import { StyleSheet, TextInput, type TextInputProps } from "react-native"; +import { ThemedView } from "@/components/ThemedView"; +import { ThemedText } from "@/components/ThemedText"; +import { useThemeColor } from "@/hooks/useThemeColor"; + +export type FieldProps = TextInputProps & { + label: string; +}; + +export function Field({ label, ...otherProps }: FieldProps) { + const color = useThemeColor({}, "text"); + + return ( + <ThemedView style={styles.container}> + <ThemedText>{label}:</ThemedText> + <TextInput style={{ color }} {...otherProps} /> + </ThemedView> + ); +} + +const styles = StyleSheet.create({ + container: { + display: "flex", + flexDirection: "row", + gap: 10, + alignItems: "center", + }, +}); diff --git a/src/react-native-app/components/ProductCard/ProductCard.tsx b/src/react-native-app/components/ProductCard/ProductCard.tsx new file mode 100644 index 0000000..d5bd301 --- /dev/null +++ b/src/react-native-app/components/ProductCard/ProductCard.tsx @@ -0,0 +1,102 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +/** + * Copied with modification from src/frontend/components/ProductCard/ProductCard.tsx + */ +import { Product } from "@/protos/demo"; +import { ThemedView } from "@/components/ThemedView"; +import { useState, useEffect, useMemo } from "react"; +import { Image, Pressable, StyleSheet } from "react-native"; +import { ThemedText } from "@/components/ThemedText"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import getFrontendProxyURL from "@/utils/Settings"; + +interface IProps { + product: Product; + onClickAdd: () => void; +} + +async function getImageURL(picture: string) { + const proxyURL = await getFrontendProxyURL(); + return `${proxyURL}/images/products/${picture}`; +} + +const ProductCard = ({ + product: { + picture, + name, + priceUsd = { + currencyCode: "USD", + units: 0, + nanos: 0, + }, + }, + onClickAdd, +}: IProps) => { + const tint = useThemeColor({}, "tint"); + const styles = useMemo(() => getStyles(tint), [tint]); + const [imageSrc, setImageSrc] = useState<string>(""); + + useEffect(() => { + getImageURL(picture) + .then(setImageSrc) + .catch((reason) => { + console.warn("Failed to get image URL: ", reason); + }); + }, [picture]); + + // TODO simplify react native demo for now by hard-coding the selected currency + const price = (priceUsd?.units + priceUsd?.nanos / 100000000).toFixed(2); + + return ( + <ThemedView style={styles.container}> + <ThemedView> + {imageSrc && <Image style={styles.image} source={{ uri: imageSrc }} />} + </ThemedView> + <ThemedView style={styles.productInfo}> + <ThemedText style={styles.name}>{name}</ThemedText> + <ThemedText style={styles.price}>$ {price}</ThemedText> + <Pressable style={styles.add} onPress={onClickAdd}> + <ThemedText style={styles.addText}>Add to Cart</ThemedText> + </Pressable> + </ThemedView> + </ThemedView> + ); +}; + +const getStyles = (tint: string) => + StyleSheet.create({ + container: { + display: "flex", + flexDirection: "row", + padding: 20, + marginLeft: 10, + marginRight: 10, + borderStyle: "solid", + borderBottomWidth: 1, + borderColor: tint, + gap: 30, + }, + image: { + width: 100, + height: 100, + }, + productInfo: { + flexShrink: 1, + }, + name: {}, + price: { + fontWeight: "bold", + }, + add: { + borderRadius: 4, + backgroundColor: "green", + alignItems: "center", + width: 100, + }, + addText: { + color: "white", + }, + }); + +export default ProductCard; diff --git a/src/react-native-app/components/ProductCard/index.ts b/src/react-native-app/components/ProductCard/index.ts new file mode 100644 index 0000000..e088670 --- /dev/null +++ b/src/react-native-app/components/ProductCard/index.ts @@ -0,0 +1,3 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +export { default } from "./ProductCard"; diff --git a/src/react-native-app/components/ProductList/ProductList.tsx b/src/react-native-app/components/ProductList/ProductList.tsx new file mode 100644 index 0000000..30bd6ef --- /dev/null +++ b/src/react-native-app/components/ProductList/ProductList.tsx @@ -0,0 +1,50 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +/** + * Copied with modification from: + * src/frontend/pages/product/[productId]/index.tsx + * src/frontend/components/ProductList/ProductList.tsx + */ +import Toast from "react-native-toast-message"; +import { useCallback } from "react"; +import ProductCard from "@/components/ProductCard"; +import { Product } from "@/protos/demo"; +import { ThemedScrollView } from "@/components/ThemedScrollView"; +import { useCart } from "@/providers/Cart.provider"; + +interface IProps { + productList: Product[]; +} + +const ProductList = ({ productList }: IProps) => { + const { addItem } = useCart(); + const onAddItem = useCallback( + async (id: string) => { + addItem({ + productId: id, + quantity: 1, + }); + + Toast.show({ + type: "success", + position: "bottom", + text1: "This item has been added to your cart", + }); + }, + [addItem], + ); + + return ( + <ThemedScrollView> + {productList.map((product) => ( + <ProductCard + key={product.id} + product={product} + onClickAdd={() => onAddItem(product.id)} + /> + ))} + </ThemedScrollView> + ); +}; + +export default ProductList; diff --git a/src/react-native-app/components/ProductList/index.ts b/src/react-native-app/components/ProductList/index.ts new file mode 100644 index 0000000..d3e14bb --- /dev/null +++ b/src/react-native-app/components/ProductList/index.ts @@ -0,0 +1,3 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +export { default } from "./ProductList"; diff --git a/src/react-native-app/components/Setting.tsx b/src/react-native-app/components/Setting.tsx new file mode 100644 index 0000000..d7db519 --- /dev/null +++ b/src/react-native-app/components/Setting.tsx @@ -0,0 +1,76 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +import {Pressable, StyleSheet, TextInput, type TextInputProps} from "react-native"; +import { ThemedView } from "@/components/ThemedView"; +import { ThemedText } from "@/components/ThemedText"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import {useCallback, useEffect, useState} from "react"; +import Toast from "react-native-toast-message"; + +export type SettingProps = TextInputProps & { + name: string; + get: () => Promise<string>; + set: (value: string) => Promise<void>; +}; + +export function Setting({ name, get, set, ...otherProps }: SettingProps) { + const color = useThemeColor({}, "text"); + const [loading, setLoading] = useState(false); + const [text, setText] = useState(''); + + useEffect(() => { + get().then(existingValue => { + setText(existingValue); + setLoading(false); + }); + }, []); + + const onApply = useCallback(async () => { + await set(text); + + Toast.show({ + type: "success", + position: "bottom", + text1: `${name} applied`, + }); + }, [text]); + + return ( + <ThemedView style={styles.container}> + <ThemedText>{name}:</ThemedText> + {loading ? ( + <ThemedText>Fetching current value...</ThemedText> + ) : ( + <> + <TextInput + style={{ color }} + onChangeText={setText} + value={text} + {...otherProps} + /> + <Pressable style={styles.apply} onPress={onApply}> + <ThemedText style={styles.applyText}>Apply</ThemedText> + </Pressable> + </> + )} + </ThemedView> + ); +} + + +const styles = StyleSheet.create({ + container: { + display: "flex", + gap: 5, + }, + apply: { + borderRadius: 4, + backgroundColor: "green", + alignItems: "center", + width: 100, + position: "relative", + }, + applyText: { + color: "white", + }, +}); diff --git a/src/react-native-app/components/ThemedScrollView.tsx b/src/react-native-app/components/ThemedScrollView.tsx new file mode 100644 index 0000000..34f5d21 --- /dev/null +++ b/src/react-native-app/components/ThemedScrollView.tsx @@ -0,0 +1,23 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +import { ScrollView, type ScrollViewProps } from "react-native"; +import { useThemeColor } from "@/hooks/useThemeColor"; + +export type ThemedViewProps = ScrollViewProps & { + lightColor?: string; + darkColor?: string; +}; + +export function ThemedScrollView({ + style, + lightColor, + darkColor, + ...otherProps +}: ThemedViewProps) { + const backgroundColor = useThemeColor( + { light: lightColor, dark: darkColor }, + "background", + ); + + return <ScrollView style={[{ backgroundColor }, style]} {...otherProps} />; +} diff --git a/src/react-native-app/components/ThemedText.tsx b/src/react-native-app/components/ThemedText.tsx new file mode 100644 index 0000000..cf6abf8 --- /dev/null +++ b/src/react-native-app/components/ThemedText.tsx @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +import { Text, type TextProps, StyleSheet } from "react-native"; + +import { useThemeColor } from "@/hooks/useThemeColor"; + +export type ThemedTextProps = TextProps & { + lightColor?: string; + darkColor?: string; + type?: "default" | "title" | "defaultSemiBold" | "subtitle" | "link"; +}; + +export function ThemedText({ + style, + lightColor, + darkColor, + type = "default", + ...rest +}: ThemedTextProps) { + const color = useThemeColor({ light: lightColor, dark: darkColor }, "text"); + + return ( + <Text + style={[ + { color }, + type === "default" ? styles.default : undefined, + type === "title" ? styles.title : undefined, + type === "defaultSemiBold" ? styles.defaultSemiBold : undefined, + type === "subtitle" ? styles.subtitle : undefined, + type === "link" ? styles.link : undefined, + style, + ]} + {...rest} + /> + ); +} + +const styles = StyleSheet.create({ + default: { + fontSize: 16, + lineHeight: 24, + }, + defaultSemiBold: { + fontSize: 16, + lineHeight: 24, + fontWeight: "600", + }, + title: { + fontSize: 32, + fontWeight: "bold", + lineHeight: 32, + }, + subtitle: { + fontSize: 20, + fontWeight: "bold", + }, + link: { + lineHeight: 30, + fontSize: 16, + color: "#0a7ea4", + }, +}); diff --git a/src/react-native-app/components/ThemedView.tsx b/src/react-native-app/components/ThemedView.tsx new file mode 100644 index 0000000..94668b3 --- /dev/null +++ b/src/react-native-app/components/ThemedView.tsx @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +import { View, type ViewProps } from "react-native"; + +import { useThemeColor } from "@/hooks/useThemeColor"; + +export type ThemedViewProps = ViewProps & { + lightColor?: string; + darkColor?: string; +}; + +export function ThemedView({ + style, + lightColor, + darkColor, + ...otherProps +}: ThemedViewProps) { + const backgroundColor = useThemeColor( + { light: lightColor, dark: darkColor }, + "background", + ); + + return <View style={[{ backgroundColor }, style]} {...otherProps} />; +} diff --git a/src/react-native-app/components/navigation/TabBarIcon.tsx b/src/react-native-app/components/navigation/TabBarIcon.tsx new file mode 100644 index 0000000..42f43b2 --- /dev/null +++ b/src/react-native-app/components/navigation/TabBarIcon.tsx @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ + +import Ionicons from "@expo/vector-icons/Ionicons"; +import { type IconProps } from "@expo/vector-icons/build/createIconSet"; +import { type ComponentProps } from "react"; + +export function TabBarIcon({ + style, + ...rest +}: IconProps<ComponentProps<typeof Ionicons>["name"]>) { + return <Ionicons size={28} style={[{ marginBottom: -3 }, style]} {...rest} />; +} |
