intial files

This commit is contained in:
sai karthik
2026-07-23 00:19:53 +05:30
parent 677a355397
commit 74a209a807
45 changed files with 1941 additions and 1016 deletions

View File

@@ -0,0 +1,5 @@
import { Stack } from "expo-router";
export default function AuthLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}

View File

@@ -0,0 +1,24 @@
import { NativeLoginForm } from "@code/auth/native";
import { router } from "expo-router";
import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
export default function LoginScreen() {
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined}
className="flex-1 bg-background"
>
<ScrollView
contentContainerClassName="flex-grow justify-center p-6"
keyboardShouldPersistTaps="handled"
>
<View className="mx-auto w-full max-w-md">
<NativeLoginForm
onNavigateSignUp={() => router.push("/(auth)/signup")}
onSuccess={() => router.replace("/")}
/>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}

View File

@@ -0,0 +1,24 @@
import { NativeSignupForm } from "@code/auth/native";
import { router } from "expo-router";
import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
export default function SignupScreen() {
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined}
className="flex-1 bg-background"
>
<ScrollView
contentContainerClassName="flex-grow justify-center p-6"
keyboardShouldPersistTaps="handled"
>
<View className="mx-auto w-full max-w-md py-6">
<NativeSignupForm
onNavigateSignIn={() => router.push("/(auth)/login")}
onSuccess={() => router.replace("/")}
/>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}

View File

@@ -1,72 +1,53 @@
import { authClient } from "@code/auth/native";
import { api } from "@code/backend/convex/_generated/api";
import { useConvexAuth, useQuery } from "convex/react";
import { Button, Chip, Separator, Spinner, Surface, useThemeColor } from "heroui-native";
import { useQuery } from "convex/react";
import { Button, Spinner, Surface } from "heroui-native";
import { Text, View } from "react-native";
import { Container } from "@/components/container";
import { SignIn } from "@/components/sign-in";
import { SignUp } from "@/components/sign-up";
import { authClient } from "@/lib/auth-client";
export default function Home() {
const healthCheck = useQuery(api.healthCheck.get);
const { isAuthenticated } = useConvexAuth();
const user = useQuery(api.auth.getCurrentUser, isAuthenticated ? {} : "skip");
const successColor = useThemeColor("success");
const dangerColor = useThemeColor("danger");
const isConnected = healthCheck === "OK";
const isLoading = healthCheck === undefined;
const user = useQuery(api.auth.getCurrentUser);
return (
<Container className="px-4 pb-4">
<View className="py-6 mb-5">
<Text className="text-3xl font-semibold text-foreground tracking-tight">
Better T Stack
</Text>
<Text className="text-muted text-sm mt-1">Full-stack TypeScript starter</Text>
<View className="mb-5 py-6">
<Text className="text-3xl font-semibold tracking-tight text-foreground">Zopu</Text>
<Text className="mt-1 text-sm text-muted">Your authenticated workspace</Text>
</View>
{user ? (
<Surface variant="secondary" className="mb-4 p-4 rounded-xl">
<View className="flex-row items-center justify-between">
<Surface className="mb-4 rounded-xl p-4" variant="secondary">
{user === undefined ? (
<Spinner size="sm" />
) : (
<View className="flex-row items-center justify-between gap-4">
<View className="flex-1">
<Text className="text-foreground font-medium">{user.name}</Text>
<Text className="text-muted text-xs mt-0.5">{user.email}</Text>
<Text className="font-medium text-foreground">{user?.name ?? "Signed in"}</Text>
<Text className="mt-0.5 text-xs text-muted">{user?.email}</Text>
</View>
<Button
variant="danger"
size="sm"
onPress={() => {
authClient.signOut();
}}
>
Sign Out
<Button onPress={() => void authClient.signOut()} size="sm" variant="danger">
<Button.Label>Sign out</Button.Label>
</Button>
</View>
</Surface>
) : null}
<Surface variant="secondary" className="p-4 rounded-xl">
<Text className="text-foreground font-medium mb-2">API Status</Text>
)}
</Surface>
<Surface className="rounded-xl p-4" variant="secondary">
<Text className="mb-2 font-medium text-foreground">API status</Text>
<View className="flex-row items-center gap-2">
<View
className={`w-2 h-2 rounded-full ${healthCheck === "OK" ? "bg-success" : "bg-danger"}`}
className={`h-2 w-2 rounded-full ${healthCheck === "OK" ? "bg-success" : "bg-danger"}`}
/>
<Text className="text-muted text-xs">
<Text className="text-xs text-muted">
{healthCheck === undefined
? "Checking..."
? "Checking"
: healthCheck === "OK"
? "Connected to API"
: "API Disconnected"}
? "Connected to Convex"
: "Convex unavailable"}
</Text>
</View>
</Surface>
{!user && (
<View className="mt-5 gap-4">
<SignIn />
<SignUp />
</View>
)}
</Container>
);
}

View File

@@ -1,44 +1,53 @@
import "@/global.css";
import { env } from "@code/env/native";
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
import { ConvexReactClient } from "convex/react";
import { NativeAuthProvider } from "@code/auth/native";
import { useConvexAuth } from "convex/react";
import { Stack } from "expo-router";
import { HeroUINativeProvider } from "heroui-native";
import { HeroUINativeProvider, Spinner } from "heroui-native";
import { View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { AppThemeProvider } from "@/contexts/app-theme-context";
import { authClient } from "@/lib/auth-client";
export const unstable_settings = {
initialRouteName: "(drawer)",
};
function RootNavigator() {
const { isAuthenticated, isLoading } = useConvexAuth();
const convex = new ConvexReactClient(env.EXPO_PUBLIC_CONVEX_URL, {
unsavedChangesWarning: false,
});
if (isLoading) {
return (
<View className="flex-1 items-center justify-center bg-background">
<Spinner size="lg" />
</View>
);
}
function StackLayout() {
return (
<Stack screenOptions={{}}>
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ title: "Modal", presentation: "modal" }} />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Protected guard={!isAuthenticated}>
<Stack.Screen name="(auth)" />
</Stack.Protected>
<Stack.Protected guard={isAuthenticated}>
<Stack.Screen name="(drawer)" />
<Stack.Screen
name="modal"
options={{ headerShown: true, presentation: "modal", title: "Modal" }}
/>
</Stack.Protected>
</Stack>
);
}
export default function Layout() {
return (
<ConvexBetterAuthProvider client={convex} authClient={authClient}>
<NativeAuthProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<KeyboardProvider>
<AppThemeProvider>
<HeroUINativeProvider>
<StackLayout />
<RootNavigator />
</HeroUINativeProvider>
</AppThemeProvider>
</KeyboardProvider>
</GestureHandlerRootView>
</ConvexBetterAuthProvider>
</NativeAuthProvider>
);
}

View File

@@ -1,164 +0,0 @@
import { useForm } from "@tanstack/react-form";
import {
Button,
FieldError,
Input,
Label,
Spinner,
Surface,
TextField,
useToast,
} from "heroui-native";
import { useRef } from "react";
import { Text, TextInput, View } from "react-native";
import z from "zod";
import { authClient } from "@/lib/auth-client";
const signInSchema = z.object({
email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"),
password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"),
});
function getErrorMessage(error: unknown): string | null {
if (!error) return null;
if (typeof error === "string") {
return error;
}
if (Array.isArray(error)) {
for (const issue of error) {
const message = getErrorMessage(issue);
if (message) {
return message;
}
}
return null;
}
if (typeof error === "object" && error !== null) {
const maybeError = error as { message?: unknown };
if (typeof maybeError.message === "string") {
return maybeError.message;
}
}
return null;
}
export function SignIn() {
const passwordInputRef = useRef<TextInput>(null);
const { toast } = useToast();
const form = useForm({
defaultValues: {
email: "",
password: "",
},
validators: {
onSubmit: signInSchema,
},
onSubmit: async ({ value, formApi }) => {
await authClient.signIn.email(
{
email: value.email.trim(),
password: value.password,
},
{
onError(error) {
toast.show({
variant: "danger",
label: error.error?.message || "Failed to sign in",
});
},
onSuccess() {
formApi.reset();
toast.show({
variant: "success",
label: "Signed in successfully",
});
},
},
);
},
});
return (
<Surface variant="secondary" className="p-4 rounded-lg">
<Text className="text-foreground font-medium mb-4">Sign In</Text>
<form.Subscribe
selector={(state) => ({
isSubmitting: state.isSubmitting,
validationError: getErrorMessage(state.errorMap.onSubmit),
})}
>
{({ isSubmitting, validationError }) => {
const formError = validationError;
return (
<>
<FieldError isInvalid={!!formError} className="mb-3">
{formError}
</FieldError>
<View className="gap-3">
<form.Field name="email">
{(field) => (
<TextField>
<Label>Email</Label>
<Input
value={field.state.value}
onBlur={field.handleBlur}
onChangeText={field.handleChange}
placeholder="email@example.com"
keyboardType="email-address"
autoCapitalize="none"
autoComplete="email"
textContentType="emailAddress"
returnKeyType="next"
blurOnSubmit={false}
onSubmitEditing={() => {
passwordInputRef.current?.focus();
}}
/>
</TextField>
)}
</form.Field>
<form.Field name="password">
{(field) => (
<TextField>
<Label>Password</Label>
<Input
ref={passwordInputRef}
value={field.state.value}
onBlur={field.handleBlur}
onChangeText={field.handleChange}
placeholder="••••••••"
secureTextEntry
autoComplete="password"
textContentType="password"
returnKeyType="go"
onSubmitEditing={form.handleSubmit}
/>
</TextField>
)}
</form.Field>
<Button onPress={form.handleSubmit} isDisabled={isSubmitting} className="mt-1">
{isSubmitting ? (
<Spinner size="sm" color="default" />
) : (
<Button.Label>Sign In</Button.Label>
)}
</Button>
</View>
</>
);
}}
</form.Subscribe>
</Surface>
);
}

View File

@@ -1,190 +0,0 @@
import { useForm } from "@tanstack/react-form";
import {
Button,
FieldError,
Input,
Label,
Spinner,
Surface,
TextField,
useToast,
} from "heroui-native";
import { useRef } from "react";
import { Text, TextInput, View } from "react-native";
import z from "zod";
import { authClient } from "@/lib/auth-client";
const signUpSchema = z.object({
name: z.string().trim().min(1, "Name is required").min(2, "Name must be at least 2 characters"),
email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"),
password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"),
});
function getErrorMessage(error: unknown): string | null {
if (!error) return null;
if (typeof error === "string") {
return error;
}
if (Array.isArray(error)) {
for (const issue of error) {
const message = getErrorMessage(issue);
if (message) {
return message;
}
}
return null;
}
if (typeof error === "object" && error !== null) {
const maybeError = error as { message?: unknown };
if (typeof maybeError.message === "string") {
return maybeError.message;
}
}
return null;
}
export function SignUp() {
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const { toast } = useToast();
const form = useForm({
defaultValues: {
name: "",
email: "",
password: "",
},
validators: {
onSubmit: signUpSchema,
},
onSubmit: async ({ value, formApi }) => {
await authClient.signUp.email(
{
name: value.name.trim(),
email: value.email.trim(),
password: value.password,
},
{
onError(error) {
toast.show({
variant: "danger",
label: error.error?.message || "Failed to sign up",
});
},
onSuccess() {
formApi.reset();
toast.show({
variant: "success",
label: "Account created successfully",
});
},
},
);
},
});
return (
<Surface variant="secondary" className="p-4 rounded-lg">
<Text className="text-foreground font-medium mb-4">Create Account</Text>
<form.Subscribe
selector={(state) => ({
isSubmitting: state.isSubmitting,
validationError: getErrorMessage(state.errorMap.onSubmit),
})}
>
{({ isSubmitting, validationError }) => {
const formError = validationError;
return (
<>
<FieldError isInvalid={!!formError} className="mb-3">
{formError}
</FieldError>
<View className="gap-3">
<form.Field name="name">
{(field) => (
<TextField>
<Label>Name</Label>
<Input
value={field.state.value}
onBlur={field.handleBlur}
onChangeText={field.handleChange}
placeholder="John Doe"
autoComplete="name"
textContentType="name"
returnKeyType="next"
blurOnSubmit={false}
onSubmitEditing={() => {
emailInputRef.current?.focus();
}}
/>
</TextField>
)}
</form.Field>
<form.Field name="email">
{(field) => (
<TextField>
<Label>Email</Label>
<Input
ref={emailInputRef}
value={field.state.value}
onBlur={field.handleBlur}
onChangeText={field.handleChange}
placeholder="email@example.com"
keyboardType="email-address"
autoCapitalize="none"
autoComplete="email"
textContentType="emailAddress"
returnKeyType="next"
blurOnSubmit={false}
onSubmitEditing={() => {
passwordInputRef.current?.focus();
}}
/>
</TextField>
)}
</form.Field>
<form.Field name="password">
{(field) => (
<TextField>
<Label>Password</Label>
<Input
ref={passwordInputRef}
value={field.state.value}
onBlur={field.handleBlur}
onChangeText={field.handleChange}
placeholder="••••••••"
secureTextEntry
autoComplete="new-password"
textContentType="newPassword"
returnKeyType="go"
onSubmitEditing={form.handleSubmit}
/>
</TextField>
)}
</form.Field>
<Button onPress={form.handleSubmit} isDisabled={isSubmitting} className="mt-1">
{isSubmitting ? (
<Spinner size="sm" color="default" />
) : (
<Button.Label>Create Account</Button.Label>
)}
</Button>
</View>
</>
);
}}
</form.Subscribe>
</Surface>
);
}

View File

@@ -1,21 +0,0 @@
import { expoClient } from "@better-auth/expo/client";
import { env } from "@code/env/native";
import { convexClient, crossDomainClient } from "@convex-dev/better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import Constants from "expo-constants";
import * as SecureStore from "expo-secure-store";
import { Platform } from "react-native";
export const authClient = createAuthClient({
baseURL: env.EXPO_PUBLIC_CONVEX_SITE_URL,
plugins: [
convexClient(),
Platform.OS === "web"
? crossDomainClient()
: expoClient({
scheme: Constants.expoConfig?.scheme as string,
storagePrefix: Constants.expoConfig?.scheme as string,
storage: SecureStore,
}),
],
});

View File

@@ -12,24 +12,18 @@
"web": "bun --env-file=../../.env expo start --web"
},
"dependencies": {
"@better-auth/expo": "catalog:",
"@code/auth": "workspace:*",
"@code/backend": "workspace:*",
"@code/env": "workspace:*",
"@convex-dev/better-auth": "catalog:",
"@expo/metro-runtime": "~57.0.2",
"@expo/vector-icons": "^15.1.1",
"@gorhom/bottom-sheet": "^5.2.14",
"@tanstack/react-form": "catalog:",
"better-auth": "catalog:",
"convex": "catalog:",
"expo": "~57.0.1",
"expo-constants": "~57.0.2",
"expo-font": "~57.0.0",
"expo-haptics": "~57.0.0",
"expo-linking": "~57.0.1",
"expo-network": "~57.0.0",
"expo-router": "~57.0.2",
"expo-secure-store": "~57.0.0",
"expo-status-bar": "~57.0.0",
"expo-web-browser": "~57.0.0",
"heroui-native": "^1.0.5",
@@ -47,8 +41,7 @@
"tailwind-merge": "catalog:",
"tailwind-variants": "^3.2.2",
"tailwindcss": "catalog:",
"uniwind": "^1.10.0",
"zod": "catalog:"
"uniwind": "^1.10.0"
},
"devDependencies": {
"@code/config": "workspace:*",

View File

@@ -10,17 +10,15 @@
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
"@code/auth": "workspace:*",
"@code/backend": "workspace:*",
"@code/env": "workspace:*",
"@code/ui": "workspace:*",
"@convex-dev/better-auth": "catalog:",
"@flue/react": "1.0.0-beta.9",
"@flue/sdk": "1.0.0-beta.9",
"@react-router/fs-routes": "^8.1.0",
"@react-router/node": "^8.1.0",
"@react-router/serve": "^8.1.0",
"@tanstack/react-form": "catalog:",
"better-auth": "catalog:",
"convex": "catalog:",
"isbot": "^5.1.44",
"lucide-react": "catalog:",
@@ -29,8 +27,7 @@
"react-dom": "^19.2.7",
"react-router": "^8.1.0",
"sonner": "catalog:",
"streamdown": "2.5.0",
"zod": "catalog:"
"streamdown": "2.5.0"
},
"devDependencies": {
"@code/config": "workspace:*",

View File

@@ -1,127 +0,0 @@
import { Button } from "@code/ui/components/button";
import { Input } from "@code/ui/components/input";
import { Label } from "@code/ui/components/label";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import z from "zod";
import { authClient } from "@/lib/auth-client";
export default function SignInForm({ onSwitchToSignUp }: { onSwitchToSignUp: () => void }) {
const navigate = useNavigate();
const form = useForm({
defaultValues: {
email: "",
password: "",
},
onSubmit: async ({ value }) => {
await authClient.signIn.email(
{
email: value.email,
password: value.password,
},
{
onSuccess: () => {
navigate("/dashboard");
toast.success("Sign in successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
},
);
},
validators: {
onSubmit: z.object({
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
return (
<div className="mx-auto mt-10 w-full max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error, index) => (
<p key={`${field.name}-error-${index}`} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error, index) => (
<p key={`${field.name}-error-${index}`} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe
selector={(state) => ({
canSubmit: state.canSubmit,
isSubmitting: state.isSubmitting,
})}
>
{({ canSubmit, isSubmitting }) => (
<Button type="submit" className="w-full" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? "Submitting..." : "Sign In"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignUp}
className="text-indigo-600 hover:text-indigo-800"
>
Need an account? Sign Up
</Button>
</div>
</div>
);
}

View File

@@ -1,152 +0,0 @@
import { Button } from "@code/ui/components/button";
import { Input } from "@code/ui/components/input";
import { Label } from "@code/ui/components/label";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import z from "zod";
import { authClient } from "@/lib/auth-client";
export default function SignUpForm({ onSwitchToSignIn }: { onSwitchToSignIn: () => void }) {
const navigate = useNavigate();
const form = useForm({
defaultValues: {
email: "",
password: "",
name: "",
},
onSubmit: async ({ value }) => {
await authClient.signUp.email(
{
email: value.email,
password: value.password,
name: value.name,
},
{
onSuccess: () => {
navigate("/dashboard");
toast.success("Sign up successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
},
);
},
validators: {
onSubmit: z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
return (
<div className="mx-auto mt-10 w-full max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="name">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Name</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error, index) => (
<p key={`${field.name}-error-${index}`} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error, index) => (
<p key={`${field.name}-error-${index}`} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error, index) => (
<p key={`${field.name}-error-${index}`} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe
selector={(state) => ({
canSubmit: state.canSubmit,
isSubmitting: state.isSubmitting,
})}
>
{({ canSubmit, isSubmitting }) => (
<Button type="submit" className="w-full" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? "Submitting..." : "Sign Up"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignIn}
className="text-indigo-600 hover:text-indigo-800"
>
Already have an account? Sign In
</Button>
</div>
</div>
);
}

View File

@@ -12,7 +12,7 @@ import {
import { useQuery } from "convex/react";
import { useNavigate } from "react-router";
import { authClient } from "@/lib/auth-client";
import { authClient } from "@code/auth/web";
export default function UserMenu() {
const navigate = useNavigate();

View File

@@ -1,8 +0,0 @@
import { env } from "@code/env/web";
import { convexClient, crossDomainClient } from "@convex-dev/better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: env.VITE_CONVEX_SITE_URL,
plugins: [convexClient(), crossDomainClient()],
});

View File

@@ -1,11 +1,10 @@
import { env } from "@code/env/web";
import { WebAuthProvider } from "@code/auth/web";
import { Toaster } from "@code/ui/components/sonner";
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
import { FlueProvider } from "@flue/react";
import "./index.css";
import { createFlueClient } from "@flue/sdk";
import { ConvexReactClient } from "convex/react";
import {
isRouteErrorResponse,
Links,
@@ -15,7 +14,6 @@ import {
ScrollRestoration,
} from "react-router";
import { authClient } from "@/lib/auth-client";
import type { Route } from "./+types/root";
import { ThemeProvider } from "./components/theme-provider";
@@ -87,26 +85,23 @@ export const Layout = ({ children }: { children: React.ReactNode }) => (
</html>
);
const App = () => {
const convex = new ConvexReactClient(env.VITE_CONVEX_URL);
return (
<FlueProvider client={flueClient}>
<ConvexBetterAuthProvider client={convex} authClient={authClient}>
<ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
storageKey="vite-ui-theme"
>
<div className="h-svh min-h-0 overflow-hidden">
<Outlet />
</div>
<Toaster richColors />
</ThemeProvider>
</ConvexBetterAuthProvider>
</FlueProvider>
);
};
const App = () => (
<FlueProvider client={flueClient}>
<WebAuthProvider>
<ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
storageKey="vite-ui-theme"
>
<div className="h-svh min-h-0 overflow-hidden">
<Outlet />
</div>
<Toaster richColors />
</ThemeProvider>
</WebAuthProvider>
</FlueProvider>
);
export default App;

View File

@@ -1,6 +1,6 @@
import { ChatPage } from "@/components/chat/chat-page";
import type { Route } from "./+types/_index";
import type { Route } from "./+types/_app._index";
export const meta = (_args: Route.MetaArgs) => [
{ title: "Zopu" },

View File

@@ -0,0 +1,24 @@
import { api } from "@code/backend/convex/_generated/api";
import { useQuery } from "convex/react";
import UserMenu from "@/components/user-menu";
export default function Dashboard() {
const privateData = useQuery(api.privateData.get);
return (
<main className="mx-auto flex min-h-svh max-w-3xl flex-col gap-6 p-6 md:p-10">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Authenticated workspace</p>
<h1 className="text-3xl font-semibold tracking-tight">Dashboard</h1>
</div>
<UserMenu />
</div>
<section className="rounded-xl border bg-card p-6 text-card-foreground">
<p>{privateData?.message ?? "Loading private data…"}</p>
</section>
</main>
);
}

View File

@@ -0,0 +1,17 @@
import { useConvexAuth } from "convex/react";
import { Navigate, Outlet, useLocation } from "react-router";
export default function AppLayout() {
const { isAuthenticated, isLoading } = useConvexAuth();
const location = useLocation();
if (isLoading) {
return <div className="grid min-h-svh place-items-center text-sm text-muted-foreground">Loading</div>;
}
if (!isAuthenticated) {
return <Navigate replace state={{ from: location.pathname }} to="/login" />;
}
return <Outlet />;
}

View File

@@ -0,0 +1,15 @@
import { SignupForm } from "@code/auth/web";
import { useNavigate } from "react-router";
import type { Route } from "./+types/_auth.signup";
export const meta = (_args: Route.MetaArgs) => [
{ title: "Create account | Zopu" },
{ content: "Create your Zopu account", name: "description" },
];
export default function SignupRoute() {
const navigate = useNavigate();
return <SignupForm onSuccess={() => navigate("/", { replace: true })} />;
}

View File

@@ -0,0 +1,20 @@
import { useConvexAuth } from "convex/react";
import { Navigate, Outlet } from "react-router";
export default function AuthLayout() {
const { isAuthenticated, isLoading } = useConvexAuth();
if (isLoading) {
return <div className="grid min-h-svh place-items-center text-sm text-muted-foreground">Loading</div>;
}
if (isAuthenticated) return <Navigate replace to="/" />;
return (
<main className="grid min-h-svh place-items-center bg-muted/30 p-6 md:p-10">
<div className="w-full max-w-sm">
<Outlet />
</div>
</main>
);
}

View File

@@ -1,41 +0,0 @@
import { api } from "@code/backend/convex/_generated/api";
import { Authenticated, AuthLoading, Unauthenticated, useQuery } from "convex/react";
import { useState } from "react";
import SignInForm from "@/components/sign-in-form";
import SignUpForm from "@/components/sign-up-form";
import UserMenu from "@/components/user-menu";
function PrivateDashboardContent() {
const privateData = useQuery(api.privateData.get);
return (
<div>
<h1>Dashboard</h1>
<p>privateData: {privateData?.message}</p>
<UserMenu />
</div>
);
}
export default function Dashboard() {
const [showSignIn, setShowSignIn] = useState(false);
return (
<>
<Authenticated>
<PrivateDashboardContent />
</Authenticated>
<Unauthenticated>
{showSignIn ? (
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
) : (
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
)}
</Unauthenticated>
<AuthLoading>
<div>Loading...</div>
</AuthLoading>
</>
);
}