added 260907 version of worker toolkit
This commit is contained in:
14
worker-toolkit-flaredown/repo/native/src/App.test.tsx
Normal file
14
worker-toolkit-flaredown/repo/native/src/App.test.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { render, screen } from "@testing-library/react-native";
|
||||
import React from "react";
|
||||
|
||||
import App from "../src/App";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("<App />", () => {
|
||||
it("renders title", async () => {
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByText("Welcome to Flaredown")).toBeDefined();
|
||||
});
|
||||
});
|
||||
38
worker-toolkit-flaredown/repo/native/src/App.tsx
Normal file
38
worker-toolkit-flaredown/repo/native/src/App.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import React from "react";
|
||||
import { Provider } from "react-native-paper";
|
||||
|
||||
import { Theme } from "./Theme";
|
||||
import CreateAccountScreen from "./screens/CreateAccountScreen";
|
||||
import Dashboard from "./screens/Dashboard";
|
||||
import LoginScreen from "./screens/LoginScreen";
|
||||
import ResetPasswordScreen from "./screens/ResetPasswordScreen";
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Provider theme={Theme}>
|
||||
<NavigationContainer>
|
||||
<Stack.Navigator
|
||||
initialRouteName="LoginScreen"
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="LoginScreen" component={LoginScreen} />
|
||||
<Stack.Screen
|
||||
name="CreateAccountScreen"
|
||||
component={CreateAccountScreen}
|
||||
/>
|
||||
<Stack.Screen name="Dashboard" component={Dashboard} />
|
||||
<Stack.Screen
|
||||
name="ResetPasswordScreen"
|
||||
component={ResetPasswordScreen}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
8
worker-toolkit-flaredown/repo/native/src/Theme.ts
Normal file
8
worker-toolkit-flaredown/repo/native/src/Theme.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { DefaultTheme } from "react-native-paper";
|
||||
|
||||
export const Theme = {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { TouchableOpacity, StyleSheet } from "react-native";
|
||||
import { getStatusBarHeight } from "react-native-status-bar-height";
|
||||
|
||||
export default function BackButton({ goBack }) {
|
||||
return (
|
||||
<TouchableOpacity onPress={goBack} style={styles.container}>
|
||||
{/* <Text>{"< Back"}</Text> */}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
top: 20 + getStatusBarHeight(),
|
||||
left: 4,
|
||||
},
|
||||
image: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, KeyboardAvoidingView, View } from "react-native";
|
||||
|
||||
import { Theme } from "../Theme";
|
||||
|
||||
export default function Background({ children }) {
|
||||
return (
|
||||
<View style={styles.background}>
|
||||
<KeyboardAvoidingView style={styles.container} behavior="padding">
|
||||
{children}
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
background: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
backgroundColor: Theme.colors.background,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
width: "100%",
|
||||
maxWidth: 500,
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Button as PaperButton } from "react-native-paper";
|
||||
|
||||
import { Theme } from "../Theme";
|
||||
|
||||
export default function Button({ mode, style = {}, children, ...props }) {
|
||||
return (
|
||||
<PaperButton
|
||||
style={[
|
||||
styles.button,
|
||||
mode === "outlined" && { backgroundColor: Theme.colors.surface },
|
||||
style,
|
||||
]}
|
||||
labelStyle={styles.text}
|
||||
mode={mode}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</PaperButton>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
width: "100%",
|
||||
marginVertical: 10,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
text: {
|
||||
fontWeight: "bold",
|
||||
fontSize: 15,
|
||||
lineHeight: 26,
|
||||
},
|
||||
});
|
||||
15
worker-toolkit-flaredown/repo/native/src/components/Card.tsx
Normal file
15
worker-toolkit-flaredown/repo/native/src/components/Card.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Card } from "react-native-paper";
|
||||
|
||||
export default function Background({ children }) {
|
||||
return <Card style={styles.card}>{children}</Card>;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
padding: 30,
|
||||
width: "100%",
|
||||
maxWidth: 500,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, Linking } from "react-native";
|
||||
|
||||
import Button from "./Button";
|
||||
import Link from "./Link";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
export default function Footer({ navigation }) {
|
||||
const handleHelpPress = () => {
|
||||
Linking.openURL(`mailto:contact@flaredown.com`);
|
||||
};
|
||||
|
||||
const handleTosPress = () => {
|
||||
navigation.navigate("ToS");
|
||||
};
|
||||
|
||||
const handlePrivacyPress = () => {
|
||||
navigation.navigate("PrivacyPolicy");
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.footer}>
|
||||
<Button onPress={handleHelpPress} mode="outlined">
|
||||
Help & Feedback
|
||||
</Button>
|
||||
<View style={styles.links}>
|
||||
<Link onPress={handleTosPress}>Terms of Service</Link>
|
||||
<Link onPress={handlePrivacyPress}>Privacy Policy</Link>
|
||||
</View>
|
||||
<Text>Flaredown © {currentYear}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
footer: {
|
||||
marginTop: 10,
|
||||
gap: 10,
|
||||
alignItems: "center",
|
||||
},
|
||||
links: {
|
||||
flexDirection: "row",
|
||||
gap: 10,
|
||||
display: "flex",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Text } from "react-native-paper";
|
||||
|
||||
import { Theme } from "../Theme";
|
||||
|
||||
export default function Header(props) {
|
||||
return <Text style={styles.header} {...props} />;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
fontSize: 21,
|
||||
color: Theme.colors.primary,
|
||||
fontWeight: "bold",
|
||||
paddingVertical: 12,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, KeyboardAvoidingView, View } from "react-native";
|
||||
|
||||
import Footer from "./Footer";
|
||||
import { Theme } from "../Theme";
|
||||
|
||||
export default function Background({ navigation, children }) {
|
||||
return (
|
||||
<View style={styles.background}>
|
||||
<KeyboardAvoidingView style={styles.container} behavior="padding">
|
||||
{children}
|
||||
<Footer navigation={navigation} />
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
background: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
backgroundColor: Theme.colors.background,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
width: "100%",
|
||||
maxWidth: 500,
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
24
worker-toolkit-flaredown/repo/native/src/components/Link.tsx
Normal file
24
worker-toolkit-flaredown/repo/native/src/components/Link.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, Text, TouchableOpacity } from "react-native";
|
||||
|
||||
import { Theme } from "../Theme";
|
||||
|
||||
interface LinkProps {
|
||||
onPress: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Link({ onPress, children }: LinkProps) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<Text style={styles.link}>{children}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
link: {
|
||||
fontWeight: "bold",
|
||||
color: Theme.colors.primary,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import { View, StyleSheet, Text } from "react-native";
|
||||
import { TextInput as Input } from "react-native-paper";
|
||||
|
||||
import { Theme } from "../Theme";
|
||||
|
||||
export default function TextInput({ errorText, description = "", ...props }) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Input
|
||||
style={styles.input}
|
||||
selectionColor={Theme.colors.primary}
|
||||
underlineColor="transparent"
|
||||
mode="outlined"
|
||||
{...props}
|
||||
/>
|
||||
{description && !errorText ? (
|
||||
<Text style={styles.description}>{description}</Text>
|
||||
) : null}
|
||||
{errorText ? <Text style={styles.error}>{errorText}</Text> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
marginVertical: 12,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: Theme.colors.surface,
|
||||
},
|
||||
description: {
|
||||
fontSize: 13,
|
||||
color: Theme.colors.secondary,
|
||||
paddingTop: 8,
|
||||
},
|
||||
error: {
|
||||
fontSize: 13,
|
||||
color: Theme.colors.error,
|
||||
paddingTop: 8,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export function emailValidator(email) {
|
||||
const re = /\S+@\S+\.\S+/;
|
||||
if (!email) return "Email address is required.";
|
||||
if (!re.test(email)) return "Please enter a valid email address.";
|
||||
return "";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export function passwordValidator(password) {
|
||||
if (!password) return "Password is required.";
|
||||
if (password.length < 8)
|
||||
return "Password should contain at least 8 characters.";
|
||||
return "";
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export function usernameValidator(name) {
|
||||
if (!name) return "Username is required.";
|
||||
return "";
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useState } from "react";
|
||||
import { View, StyleSheet } from "react-native";
|
||||
import { Text } from "react-native-paper";
|
||||
|
||||
import BackButton from "../components/BackButton";
|
||||
import Button from "../components/Button";
|
||||
import Card from "../components/Card";
|
||||
import Header from "../components/Header";
|
||||
import Layout from "../components/Layout";
|
||||
import Link from "../components/Link";
|
||||
import TextInput from "../components/TextInput";
|
||||
import { emailValidator } from "../helpers/emailValidator";
|
||||
import { passwordValidator } from "../helpers/passwordValidator";
|
||||
import { usernameValidator } from "../helpers/usernameValidator";
|
||||
|
||||
export default function CreateAccountScreen({ navigation }) {
|
||||
const [username, setUsername] = useState({ value: "", error: "" });
|
||||
const [email, setEmail] = useState({ value: "", error: "" });
|
||||
const [password, setPassword] = useState({ value: "", error: "" });
|
||||
|
||||
const onSignUpPressed = () => {
|
||||
const usernameError = usernameValidator(username.value);
|
||||
const emailError = emailValidator(email.value);
|
||||
const passwordError = passwordValidator(password.value);
|
||||
if (emailError || passwordError || usernameError) {
|
||||
setUsername({ ...username, error: usernameError });
|
||||
setEmail({ ...email, error: emailError });
|
||||
setPassword({ ...password, error: passwordError });
|
||||
return;
|
||||
}
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: "Dashboard" }],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout navigation={navigation}>
|
||||
<BackButton goBack={navigation.goBack} />
|
||||
<Header>Create account</Header>
|
||||
<Card>
|
||||
<TextInput
|
||||
label="Username"
|
||||
returnKeyType="next"
|
||||
value={username.value}
|
||||
onChangeText={(text) => setUsername({ value: text, error: "" })}
|
||||
error={!!username.error}
|
||||
errorText={username.error}
|
||||
/>
|
||||
<TextInput
|
||||
label="Email"
|
||||
returnKeyType="next"
|
||||
value={email.value}
|
||||
onChangeText={(text) => setEmail({ value: text, error: "" })}
|
||||
error={!!email.error}
|
||||
errorText={email.error}
|
||||
autoCapitalize="none"
|
||||
autoCompleteType="email"
|
||||
textContentType="emailAddress"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
<TextInput
|
||||
label="Password"
|
||||
returnKeyType="done"
|
||||
value={password.value}
|
||||
onChangeText={(text) => setPassword({ value: text, error: "" })}
|
||||
error={!!password.error}
|
||||
errorText={password.error}
|
||||
secureTextEntry
|
||||
/>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={onSignUpPressed}
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<View style={styles.row}>
|
||||
<Text>Already a member? </Text>
|
||||
<Link onPress={() => navigation.replace("LoginScreen")}>Log in</Link>
|
||||
</View>
|
||||
</Card>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import Layout from "../components/Layout";
|
||||
import Link from "../components/Link";
|
||||
|
||||
export default function Dashboard({ navigation }) {
|
||||
return (
|
||||
<Layout navigation={navigation}>
|
||||
<View>
|
||||
<Link onPress={() => navigation.replace("LoginScreen")}>Log out</Link>
|
||||
</View>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import React, { useState } from "react";
|
||||
import { TouchableOpacity, StyleSheet, View } from "react-native";
|
||||
import { Text } from "react-native-paper";
|
||||
|
||||
import { Theme } from "../Theme";
|
||||
import BackButton from "../components/BackButton";
|
||||
import Button from "../components/Button";
|
||||
import Card from "../components/Card";
|
||||
import Header from "../components/Header";
|
||||
import Layout from "../components/Layout";
|
||||
import Link from "../components/Link";
|
||||
import TextInput from "../components/TextInput";
|
||||
import { emailValidator } from "../helpers/emailValidator";
|
||||
import { passwordValidator } from "../helpers/passwordValidator";
|
||||
|
||||
export default function LoginScreen({ navigation }) {
|
||||
const [email, setEmail] = useState({ value: "", error: "" });
|
||||
const [password, setPassword] = useState({ value: "", error: "" });
|
||||
|
||||
const onLoginPressed = () => {
|
||||
const emailError = emailValidator(email.value);
|
||||
const passwordError = passwordValidator(password.value);
|
||||
if (emailError || passwordError) {
|
||||
setEmail({ ...email, error: emailError });
|
||||
setPassword({ ...password, error: passwordError });
|
||||
return;
|
||||
}
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: "Dashboard" }],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout navigation={navigation}>
|
||||
<BackButton goBack={navigation.goBack} />
|
||||
<Header>Welcome to Flaredown</Header>
|
||||
<Card>
|
||||
<TextInput
|
||||
label="Email"
|
||||
returnKeyType="next"
|
||||
value={email.value}
|
||||
onChangeText={(text) => setEmail({ value: text, error: "" })}
|
||||
error={!!email.error}
|
||||
errorText={email.error}
|
||||
autoCapitalize="none"
|
||||
autoCompleteType="email"
|
||||
textContentType="emailAddress"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
<TextInput
|
||||
label="Password"
|
||||
returnKeyType="done"
|
||||
value={password.value}
|
||||
onChangeText={(text) => setPassword({ value: text, error: "" })}
|
||||
error={!!password.error}
|
||||
errorText={password.error}
|
||||
secureTextEntry
|
||||
/>
|
||||
<View style={styles.forgotPassword}>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate("ResetPasswordScreen")}
|
||||
>
|
||||
<Text style={styles.forgot}>Forgot password</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Button mode="contained" onPress={onLoginPressed}>
|
||||
Log in
|
||||
</Button>
|
||||
<View style={styles.row}>
|
||||
<Text>Not a member yet? </Text>
|
||||
<Link onPress={() => navigation.replace("CreateAccountScreen")}>
|
||||
Sign up now
|
||||
</Link>
|
||||
</View>
|
||||
</Card>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
forgotPassword: {
|
||||
width: "100%",
|
||||
alignItems: "flex-end",
|
||||
marginBottom: 10,
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
marginTop: 4,
|
||||
},
|
||||
forgot: {
|
||||
fontSize: 13,
|
||||
color: Theme.colors.secondary,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import BackButton from "../components/BackButton";
|
||||
import Button from "../components/Button";
|
||||
import Card from "../components/Card";
|
||||
import Header from "../components/Header";
|
||||
import Layout from "../components/Layout";
|
||||
import TextInput from "../components/TextInput";
|
||||
import { emailValidator } from "../helpers/emailValidator";
|
||||
|
||||
export default function ResetPasswordScreen({ navigation }) {
|
||||
const [email, setEmail] = useState({ value: "", error: "" });
|
||||
|
||||
const sendResetPasswordEmail = () => {
|
||||
const emailError = emailValidator(email.value);
|
||||
if (emailError) {
|
||||
setEmail({ ...email, error: emailError });
|
||||
return;
|
||||
}
|
||||
navigation.navigate("LoginScreen");
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout navigation={navigation}>
|
||||
<BackButton goBack={navigation.goBack} />
|
||||
<Header>Reset password</Header>
|
||||
<Card>
|
||||
<TextInput
|
||||
label="Email"
|
||||
returnKeyType="done"
|
||||
value={email.value}
|
||||
onChangeText={(text) => setEmail({ value: text, error: "" })}
|
||||
error={!!email.error}
|
||||
errorText={email.error}
|
||||
autoCapitalize="none"
|
||||
autoCompleteType="email"
|
||||
textContentType="emailAddress"
|
||||
keyboardType="email-address"
|
||||
description="You will receive an email with the reset link."
|
||||
/>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={sendResetPasswordEmail}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Card>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user