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
+29
View File
@@ -0,0 +1,29 @@
import { Redirect, Stack } from "expo-router";
import { View } from "react-native";
import { useAuthContext } from "@/auth/AuthContext";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
const AuthRoutesLayout = () => {
const { isAuthenticated, bootstrapping } = useAuthContext();
if (bootstrapping) {
return (
<View className="flex-1 p-5 bg-primaryPurpleLight">
<View
style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
>
<LoadingSpinner />
</View>
</View>
);
}
if (isAuthenticated) {
return <Redirect href={"/"} />;
}
return <Stack screenOptions={{ headerShown: false }} />;
};
export default AuthRoutesLayout;
+113
View File
@@ -0,0 +1,113 @@
import { useRouter } from "expo-router";
import { useState } from "react";
import { View, Text } from "react-native";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { AuthLayout } from "@/components/auth/AuthLayout";
import { AuthHeader } from "@/components/auth/AuthHeader";
import { AuthCard } from "@/components/auth/AuthCard";
import { AuthFooter } from "@/components/auth/AuthFooter";
import { FormInput } from "@/components/auth/FormInput";
import { getApiUrl } from "@/store/api/baseApi";
import { AuthButton } from "@/components/auth/AuthButton";
const schema = z.object({
email: z.email("Enter a valid email"),
});
type FormValues = z.infer<typeof schema>;
const ForgotPasswordScreen = () => {
const router = useRouter();
const [submitting, setSubmitting] = useState(false);
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const {
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
mode: "onSubmit",
reValidateMode: "onChange",
shouldUnregister: false,
defaultValues: { email: "" },
});
const email = watch("email");
const onSubmit = async (values: FormValues) => {
setSubmitting(true);
setError("");
setMessage("");
try {
const res = await fetch(
`${getApiUrl()?.replace(/\/$/, "")}/auth/password/forgot`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: values.email }),
}
);
const data = await res.json();
if (!res.ok) {
throw new Error(data?.error || "Failed to request reset");
}
setMessage("If an account exists for this email, a reset link was sent");
} catch (error: unknown) {
if (error instanceof Error) {
setError(error.message);
} else {
setError("Failed to request reset");
}
} finally {
setSubmitting(false);
}
};
return (
<AuthLayout>
<AuthHeader title="Project Athena" />
<AuthCard title="Forgot Password" subtitle="We'll email you a reset link">
{!!error && (
<Text style={{ color: "#F83434", marginBottom: 8 }}>{error}</Text>
)}
{!!message && (
<Text style={{ color: "#22C927", marginBottom: 8 }}>{message}</Text>
)}
<FormInput
label={errors.email?.message || "Email"}
value={email}
onChangeText={(t) => setValue("email", t, { shouldValidate: true })}
icon="email"
disabled={submitting}
onSubmitEditing={handleSubmit(onSubmit)}
returnKeyType="send"
submitBehavior="blurAndSubmit"
/>
<View style={{ marginTop: 8 }}>
<AuthButton
onPress={handleSubmit(onSubmit)}
loading={submitting}
disabled={submitting}
>
{submitting ? "Sending..." : "Send Reset Link"}
</AuthButton>
</View>
</AuthCard>
<AuthFooter
question="Remembered your password?"
actionText="Sign in"
onPress={() => router.push("/sign-in")}
disabled={submitting}
/>
</AuthLayout>
);
};
export default ForgotPasswordScreen;
+158
View File
@@ -0,0 +1,158 @@
import { useRouter, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { View, Text } from "react-native";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { AuthLayout } from "@/components/auth/AuthLayout";
import { AuthHeader } from "@/components/auth/AuthHeader";
import { AuthCard } from "@/components/auth/AuthCard";
import { AuthFooter } from "@/components/auth/AuthFooter";
import { FormInput } from "@/components/auth/FormInput";
import { PasswordInput } from "@/components/auth/PasswordInput";
import { AuthButton } from "@/components/auth/AuthButton";
import { getApiUrl } from "@/store/api/baseApi";
const schema = z
.object({
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string(),
})
.refine((d) => d.password === d.confirmPassword, {
path: ["confirmPassword"],
message: "Passwords do not match",
});
type FormVals = z.infer<typeof schema>;
const ResetPasswordScreen = () => {
const router = useRouter();
const { token: tokenParam } = useLocalSearchParams<{ token?: string }>();
const [submitting, setSubmitting] = useState(false);
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const [tokenOverride, setTokenOverride] = useState("");
const {
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<FormVals>({
resolver: zodResolver(schema),
mode: "onSubmit",
reValidateMode: "onChange",
shouldUnregister: false,
defaultValues: { password: "", confirmPassword: "" },
});
const password = watch("password");
const confirmPassword = watch("confirmPassword");
const tokenFromLink =
typeof tokenParam === "string" && tokenParam.length > 0 ? tokenParam : "";
const token = tokenFromLink || tokenOverride;
const onSubmit = async (vals: FormVals) => {
setSubmitting(true);
setError("");
setMessage("");
try {
if (!token) {
throw new Error("Reset link token is required");
}
const res = await fetch(
`${getApiUrl()?.replace(/\/$/, "")}/auth/password/reset`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, password: vals.password }),
}
);
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data?.error || "Failed to reset password");
}
setMessage("Password has been reset. Redirecting to sign in...");
setTimeout(() => router.replace("/sign-in"), 1500);
} catch (e: any) {
setError(e?.message || "Failed to reset password");
} finally {
setSubmitting(false);
}
};
return (
<AuthLayout>
<AuthHeader title="Project Athena" />
<AuthCard
title="Reset Password"
subtitle={
tokenFromLink
? "Set a new password"
: "Paste the reset link token and set a new password"
}
>
{!!error && (
<Text style={{ color: "#F83434", marginBottom: 8 }}>{error}</Text>
)}
{!!message && (
<Text style={{ color: "#22C927", marginBottom: 8 }}>{message}</Text>
)}
{!tokenFromLink && (
<FormInput
label={"Reset token"}
value={tokenOverride}
onChangeText={setTokenOverride}
icon="key"
disabled={submitting}
onSubmitEditing={handleSubmit(onSubmit)}
returnKeyType="next"
/>
)}
<PasswordInput
label={errors.password?.message || "New Password"}
value={password}
onChangeText={(t) =>
setValue("password", t, { shouldValidate: true })
}
disabled={submitting}
onSubmitEditing={handleSubmit(onSubmit)}
returnKeyType="next"
/>
<PasswordInput
label={errors.confirmPassword?.message || "Confirm Password"}
value={confirmPassword}
onChangeText={(t) =>
setValue("confirmPassword", t, { shouldValidate: true })
}
disabled={submitting}
onSubmitEditing={handleSubmit(onSubmit)}
returnKeyType="send"
submitBehavior="blurAndSubmit"
/>
<View style={{ marginTop: 8 }}>
<AuthButton
onPress={handleSubmit(onSubmit)}
loading={submitting}
disabled={submitting}
>
{submitting ? "Resetting password..." : "Reset Password"}
</AuthButton>
</View>
</AuthCard>
<AuthFooter
question="Remembered your password?"
actionText="Sign in"
onPress={() => router.push("/sign-in")}
disabled={submitting}
/>
</AuthLayout>
);
};
export default ResetPasswordScreen;
+86
View File
@@ -0,0 +1,86 @@
import { useRouter } from "expo-router";
import { useState, useEffect } from "react";
import * as WebBrowser from "expo-web-browser";
import { Platform } from "react-native";
import { AuthLayout } from "@/components/auth/AuthLayout";
import { AuthHeader } from "@/components/auth/AuthHeader";
import { AuthCard } from "@/components/auth/AuthCard";
import { SignInForm } from "@/components/auth/SignInForm";
import { ForgotPasswordLink } from "@/components/auth/ForgotPasswordLink";
import { useAuthContext } from "@/auth/AuthContext";
WebBrowser.maybeCompleteAuthSession();
const useWarmUpBrowser = () => {
useEffect(() => {
if (Platform.OS !== "web") {
WebBrowser.warmUpAsync();
return () => {
WebBrowser.coolDownAsync();
};
}
}, []);
};
const SignIn = () => {
const router = useRouter();
const { login, isAuthenticated, loading } = useAuthContext();
useWarmUpBrowser();
const [errorMessage, setErrorMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const successMessage = "";
const isResettingPassword = false;
const onForgotPasswordPress = async () => {
router.push("/forgot-password");
};
const onSignInSubmit = async (vals: {
emailAddress: string;
password: string;
}) => {
setErrorMessage("");
setIsLoading(true);
try {
await login(vals.emailAddress, vals.password);
router.replace("/");
} catch (err: any) {
const msg = err?.message || "Invalid credentials";
setErrorMessage(msg);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (!loading && isAuthenticated) {
router.replace("/");
}
}, [loading, isAuthenticated, router]);
return (
<AuthLayout>
<AuthHeader title="Project Athena" />
<AuthCard title="Welcome Back" subtitle="Sign in to continue">
<SignInForm
errorMessage={errorMessage}
successMessage={successMessage}
isLoading={isLoading}
onSignInSubmit={onSignInSubmit}
/>
<ForgotPasswordLink
onPress={onForgotPasswordPress}
isResetting={isResettingPassword}
disabled={isResettingPassword || isLoading}
/>
</AuthCard>
</AuthLayout>
);
};
export default SignIn;