project setup

This commit is contained in:
Sone
2025-11-02 10:08:56 +01:00
commit 568838003e
174 changed files with 27100 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
import { Button } from "react-native-paper";
import { ReactNode } from "react";
interface AuthButtonProps {
onPress: () => void;
loading?: boolean;
disabled?: boolean;
textColor?: string;
children: ReactNode;
className?: string;
}
export const AuthButton = ({
onPress,
loading = false,
disabled = false,
children,
}: AuthButtonProps) => {
return (
<Button
mode="contained"
onPress={onPress}
className="py-2 rounded-lg"
buttonColor="#6750a4"
disabled={disabled || loading}
loading={loading}
>
{children}
</Button>
);
};
+45
View File
@@ -0,0 +1,45 @@
import { Surface } from "react-native-paper";
import { View, Text } from "react-native";
interface AuthCardProps {
children: React.ReactNode;
title?: string;
subtitle?: string;
}
export const AuthCard = ({ children, title, subtitle }: AuthCardProps) => {
return (
<Surface
style={{
borderRadius: 20,
padding: 24,
backgroundColor: "rgba(255,255,255,0.9)",
elevation: 4,
}}
>
{(title || subtitle) && (
<View style={{ alignItems: "center", marginBottom: 24 }}>
{title && (
<Text
style={{
fontSize: 24,
fontWeight: "bold",
color: "#6750a4",
marginBottom: subtitle ? 12 : 0,
textAlign: "center",
}}
>
{title}
</Text>
)}
{subtitle && (
<Text style={{ fontSize: 16, color: "rgba(0,0,0,0.6)" }}>
{subtitle}
</Text>
)}
</View>
)}
{children}
</Surface>
);
};
+24
View File
@@ -0,0 +1,24 @@
import { View, Text, TouchableOpacity } from "react-native";
interface AuthFooterProps {
question: string;
actionText: string;
onPress: () => void;
disabled?: boolean;
}
export const AuthFooter = ({
question,
actionText,
onPress,
disabled,
}: AuthFooterProps) => {
return (
<View className="flex-row items-center justify-center mt-6">
<Text className="text-white">{question}</Text>
<TouchableOpacity onPress={onPress} disabled={disabled}>
<Text className="text-white font-bold ml-2">{actionText}</Text>
</TouchableOpacity>
</View>
);
};
+27
View File
@@ -0,0 +1,27 @@
import { View, Text, Image } from "react-native";
interface AuthHeaderProps {
title?: string;
}
export const AuthHeader = ({ title = "Project Athena" }: AuthHeaderProps) => {
return (
<View style={{ alignItems: "center", marginBottom: 36 }}>
<Image
source={require("../../assets/images/logo_no_background.png")}
style={{ width: 80, height: 80, marginBottom: 16 }}
resizeMode="contain"
/>
<Text
style={{
fontSize: 28,
fontWeight: "bold",
color: "white",
marginBottom: 4,
}}
>
{title}
</Text>
</View>
);
};
+50
View File
@@ -0,0 +1,50 @@
import {
View,
ScrollView,
KeyboardAvoidingView,
Platform,
useWindowDimensions,
} from "react-native";
import { StatusBar } from "expo-status-bar";
import { LinearGradient } from "expo-linear-gradient";
interface AuthLayoutProps {
children: React.ReactNode;
}
export const AuthLayout = ({ children }: AuthLayoutProps) => {
const { width } = useWindowDimensions();
const isWideScreen = width > 768;
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<StatusBar style="light" />
<LinearGradient
colors={["#A78FA1", "#6750a4", "#49385D"]}
style={{ flex: 1 }}
>
<ScrollView
contentContainerStyle={{
flexGrow: 1,
justifyContent: "center",
paddingHorizontal: isWideScreen ? 32 : 16,
paddingVertical: isWideScreen ? 32 : 24,
}}
keyboardShouldPersistTaps="handled"
>
<View
style={{
alignSelf: isWideScreen ? "center" : "stretch",
width: isWideScreen ? 550 : "100%",
}}
>
{children}
</View>
</ScrollView>
</LinearGradient>
</KeyboardAvoidingView>
);
};
+25
View File
@@ -0,0 +1,25 @@
import { TouchableOpacity, Text } from "react-native";
interface ForgotPasswordLinkProps {
onPress: () => void;
isResetting: boolean;
disabled: boolean;
}
export const ForgotPasswordLink = ({
onPress,
isResetting,
disabled,
}: ForgotPasswordLinkProps) => {
return (
<TouchableOpacity
className="items-center mt-4"
onPress={onPress}
disabled={disabled || isResetting}
>
<Text className="text-primaryPurple">
{isResetting ? "Sending reset email..." : "Forgot Password?"}
</Text>
</TouchableOpacity>
);
};
+68
View File
@@ -0,0 +1,68 @@
import { TextInput, TextInputProps } from "react-native-paper";
import {
KeyboardTypeOptions,
Platform,
NativeSyntheticEvent,
TextInputSubmitEditingEventData,
SubmitBehavior,
} from "react-native";
interface FormInputProps {
label: string;
value: string;
onChangeText: (text: string) => void;
onBlur?: () => void;
icon?: string;
keyboardType?: KeyboardTypeOptions;
disabled?: boolean;
autoCapitalize?: TextInputProps["autoCapitalize"];
style?: any;
onSubmitEditing?: (
e?: NativeSyntheticEvent<TextInputSubmitEditingEventData>
) => void;
returnKeyType?: any;
submitBehavior?: SubmitBehavior;
}
export const FormInput = ({
label,
value,
onChangeText,
onBlur,
icon = "pencil",
keyboardType = "default",
disabled = false,
autoCapitalize = "none",
style = {},
onSubmitEditing,
returnKeyType,
submitBehavior,
}: FormInputProps) => {
return (
<TextInput
label={label}
mode="flat"
value={value}
onChangeText={onChangeText}
onBlur={onBlur}
style={{
marginBottom: 16,
backgroundColor: "transparent",
...style,
}}
keyboardType={keyboardType}
autoCapitalize={autoCapitalize}
onSubmitEditing={onSubmitEditing}
returnKeyType={returnKeyType}
submitBehavior={submitBehavior ?? "submit"}
onKeyPress={(e) => {
if (Platform.OS === "web" && e.nativeEvent.key === "Enter") {
onSubmitEditing?.();
}
}}
left={<TextInput.Icon icon={icon} color="#6750a4" />}
theme={{ colors: { primary: "#6750a4" } }}
disabled={disabled}
/>
);
};
+28
View File
@@ -0,0 +1,28 @@
import { View, Text } from "react-native";
interface MessageDisplayProps {
message: string;
type: "error" | "success";
}
export const MessageDisplay = ({ message, type }: MessageDisplayProps) => {
if (!message) return null;
const backgroundColor =
type === "error" ? "rgba(255,0,0,0.1)" : "rgba(0,255,0,0.1)";
const textColor = type === "error" ? "#F83434" : "#22C927";
return (
<View
style={{
marginBottom: 16,
padding: 10,
backgroundColor,
borderRadius: 4,
}}
>
<Text style={{ color: textColor }}>{message}</Text>
</View>
);
};
+70
View File
@@ -0,0 +1,70 @@
import { useState } from "react";
import {
Platform,
NativeSyntheticEvent,
TextInputSubmitEditingEventData,
} from "react-native";
import { TextInput, TextInputProps } from "react-native-paper";
interface PasswordInputProps {
label: string;
value: string;
onChangeText: (text: string) => void;
onBlur?: () => void;
disabled?: boolean;
style?: any;
onSubmitEditing?: (
e?: NativeSyntheticEvent<TextInputSubmitEditingEventData>
) => void;
returnKeyType?: any;
submitBehavior?: TextInputProps["submitBehavior"];
}
export const PasswordInput = ({
label,
value,
onChangeText,
onBlur,
disabled = false,
style = {},
onSubmitEditing,
returnKeyType,
submitBehavior,
}: PasswordInputProps) => {
const [secureTextEntry, setSecureTextEntry] = useState(true);
return (
<TextInput
label={label}
mode="flat"
value={value}
secureTextEntry={secureTextEntry}
onChangeText={onChangeText}
onBlur={onBlur}
style={{
marginBottom: 16,
backgroundColor: "transparent",
...style,
}}
onSubmitEditing={onSubmitEditing}
returnKeyType={returnKeyType}
submitBehavior={submitBehavior ?? "submit"}
onKeyPress={(e) => {
if (Platform.OS === "web" && e?.nativeEvent?.key === "Enter") {
onSubmitEditing?.();
}
}}
left={<TextInput.Icon icon="lock" color="#6750a4" />}
right={
<TextInput.Icon
icon={secureTextEntry ? "eye" : "eye-off"}
color="#6750a4"
onPress={() => setSecureTextEntry(!secureTextEntry)}
/>
}
theme={{ colors: { primary: "#6750a4" } }}
disabled={disabled}
placeholderTextColor="#d9d9d9"
/>
);
};
+123
View File
@@ -0,0 +1,123 @@
import { Text } from "react-native";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { FormInput } from "@/components/auth/FormInput";
import { PasswordInput } from "@/components/auth/PasswordInput";
import { MessageDisplay } from "@/components/auth/MessageDisplay";
import { AuthButton } from "@/components/auth/AuthButton";
const signInSchema = z.object({
emailAddress: z
.string()
.trim()
.min(1, "Email is required")
.pipe(z.email("Enter a valid email address")),
password: z
.string()
.min(1, "Password is required")
.max(128, "Password is too long"),
});
type SignInValues = z.infer<typeof signInSchema>;
interface SignInFormProps {
errorMessage: string;
successMessage: string;
isLoading: boolean;
onSignInSubmit: (values: {
emailAddress: string;
password: string;
}) => void | Promise<void>;
}
export const SignInForm = ({
errorMessage,
successMessage,
isLoading,
onSignInSubmit,
}: SignInFormProps) => {
const { control, handleSubmit } = useForm<SignInValues>({
resolver: zodResolver(signInSchema),
defaultValues: { emailAddress: "", password: "" },
mode: "onBlur",
reValidateMode: "onBlur",
shouldUnregister: false,
});
const submit = (vals: SignInValues) =>
onSignInSubmit({
emailAddress: vals.emailAddress.trim().toLowerCase(),
password: vals.password,
});
return (
<>
<Controller
control={control}
name="emailAddress"
render={({ field: { onChange, onBlur, value }, fieldState: { error } }) => (
<>
<FormInput
label="Email"
value={value}
onChangeText={onChange}
onBlur={onBlur}
icon="email"
keyboardType="email-address"
disabled={isLoading}
style={{ marginBottom: 8 }}
returnKeyType="next"
/>
{error ? (
<Text style={{ color: "#F83434", marginBottom: 8 }}>
{error.message}
</Text>
) : null}
</>
)}
/>
<Controller
control={control}
name="password"
render={({ field: { onChange, onBlur, value }, fieldState: { error } }) => (
<>
<PasswordInput
label="Password"
value={value}
onChangeText={onChange}
onBlur={onBlur}
disabled={isLoading}
style={{ marginBottom: 8 }}
returnKeyType="done"
submitBehavior="blurAndSubmit"
onSubmitEditing={() => handleSubmit(submit)()}
/>
{error ? (
<Text style={{ color: "#F83434", marginBottom: 8 }}>
{error.message}
</Text>
) : null}
</>
)}
/>
{errorMessage ? (
<MessageDisplay message={errorMessage} type="error" />
) : null}
{successMessage ? (
<MessageDisplay message={successMessage} type="success" />
) : null}
<AuthButton
onPress={handleSubmit(submit)}
loading={isLoading}
disabled={isLoading}
>
{isLoading ? "Signing in..." : "Sign In"}
</AuthButton>
</>
);
};
+62
View File
@@ -0,0 +1,62 @@
import { Text, Animated, Pressable } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import { useAuthContext } from "@/auth/AuthContext";
interface SignOutButtonProps {
textOpacity: Animated.Value;
iconTranslateX: Animated.Value;
paddingLeft: Animated.Value;
}
export const SignOutButton = ({
textOpacity,
iconTranslateX,
paddingLeft,
}: SignOutButtonProps) => {
const router = useRouter();
const { logout } = useAuthContext();
const handleSignOut = async () => {
try {
await logout();
router.replace("/sign-in");
} catch (err) {
console.error("Logout error:", err);
}
};
return (
<Animated.View
style={{
paddingLeft,
paddingRight: 4,
}}
>
<Pressable
onPress={handleSignOut}
className="flex-row items-center py-3 rounded-lg w-full justify-start"
>
<Animated.View
style={{
width: 32,
alignItems: "center",
transform: [{ translateX: iconTranslateX }],
}}
>
<Ionicons name="log-out-outline" size={32} color="white" />
</Animated.View>
<Animated.View
style={{
opacity: textOpacity,
marginLeft: 12,
overflow: "hidden",
}}
>
<Text className="text-white" numberOfLines={1}>
Sign out
</Text>
</Animated.View>
</Pressable>
</Animated.View>
);
};
+106
View File
@@ -0,0 +1,106 @@
import { useMemo, useState } from "react";
import { View, TouchableOpacity } from "react-native";
import {
Avatar,
Divider,
Menu,
Text,
Chip,
IconButton,
} from "react-native-paper";
import { useAuthContext } from "@/auth/AuthContext";
import { getInitials, getRoleLabel } from "@/utils/userUtils";
export const UserButton = () => {
const { user, logout } = useAuthContext();
const [visible, setVisible] = useState(false);
const initials = useMemo(
() => getInitials(user?.firstName, user?.lastName),
[user?.firstName, user?.lastName]
);
if (!user) return null;
return (
<View className="flex-row items-center">
<Menu
visible={visible}
onDismiss={() => setVisible(false)}
anchor={
<TouchableOpacity
accessibilityRole="button"
onPress={() => setVisible(true)}
className="ml-3"
>
{user.imageUrl ? (
<Avatar.Image size={32} source={{ uri: user.imageUrl }} />
) : (
<Avatar.Text size={32} label={initials} />
)}
</TouchableOpacity>
}
contentStyle={{
minWidth: 300,
borderRadius: 12,
overflow: "hidden",
padding: 0,
}}
>
<View
style={{
paddingHorizontal: 14,
paddingVertical: 12,
backgroundColor: "#F7F7F8",
}}
>
<View style={{ flexDirection: "row", alignItems: "center" }}>
{user.imageUrl ? (
<Avatar.Image size={44} source={{ uri: user.imageUrl }} />
) : (
<Avatar.Text size={44} label={initials} />
)}
<View
style={{
flexShrink: 1,
maxWidth: 500,
marginLeft: 8,
}}
>
<Text variant="titleSmall" numberOfLines={10}>
{user.firstName} {user.lastName}
</Text>
<Text variant="bodySmall" style={{ opacity: 0.7 }}>
{user.email}
</Text>
</View>
<View style={{ alignSelf: "flex-start", marginLeft: "auto" }}>
<IconButton
icon="close"
size={20}
onPress={() => setVisible(false)}
accessibilityLabel="Close dialog"
style={{ margin: 0 }}
/>
</View>
</View>
<View style={{ height: 10 }} />
<View style={{ flexDirection: "column", flexWrap: "wrap" }}>
<Chip compact>{getRoleLabel(user.role)}</Chip>
</View>
</View>
<Divider />
<Menu.Item
title="Sign out"
leadingIcon="logout"
onPress={async () => {
setVisible(false);
await logout();
}}
/>
</Menu>
</View>
);
};
+45
View File
@@ -0,0 +1,45 @@
import { FormInput } from "@/components/auth/FormInput";
import { MessageDisplay } from "@/components/auth/MessageDisplay";
import { AuthButton } from "@/components/auth/AuthButton";
interface VerificationFormProps {
code: string;
setCode: (value: string) => void;
errorMessage: string;
isLoading: boolean;
onVerifyPress: () => void;
}
export const VerificationForm = ({
code,
setCode,
errorMessage,
isLoading,
onVerifyPress,
}: VerificationFormProps) => {
return (
<>
<FormInput
label="Verification Code"
value={code}
onChangeText={setCode}
icon="key"
keyboardType="number-pad"
disabled={isLoading}
style={{ marginBottom: 24 }}
/>
{errorMessage ? (
<MessageDisplay message={errorMessage} type="error" />
) : null}
<AuthButton
onPress={onVerifyPress}
loading={isLoading}
disabled={isLoading}
>
Verify Email
</AuthButton>
</>
);
};