Update expo app to make it somewhat functional

This commit is contained in:
2026-03-26 12:36:18 -05:00
parent 56fe2a2af3
commit ff8c39dc75
18 changed files with 15732 additions and 7478 deletions

View File

@@ -6,7 +6,7 @@
"build": {
"base": {
"node": "22.12.0",
"pnpm": "9.15.4",
"bun": "1.3.10",
"ios": {
"resourceClass": "m-medium"
}

View File

@@ -1,7 +1,8 @@
import { baseConfig } from '@acme/eslint-config/base';
import { reactConfig } from '@acme/eslint-config/react';
import { defineConfig } from 'eslint/config';
import { baseConfig } from '@gib/eslint-config/base';
import { reactConfig } from '@gib/eslint-config/react';
export default defineConfig(
{
ignores: ['.expo/**', 'expo-plugins/**'],

View File

@@ -49,8 +49,7 @@
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.16.0",
"react-native-web": "~0.21.2",
"react-native-worklets": "~0.5.2",
"superjson": "2.2.3"
"react-native-worklets": "~0.5.2"
},
"devDependencies": {
"@gib/eslint-config": "workspace:*",

View File

@@ -1 +1 @@
module.exports = require('@acme/tailwind-config/postcss-config');
module.exports = require('@gib/tailwind-config/postcss-config');

View File

@@ -1,33 +1,30 @@
import { useColorScheme } from 'react-native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { QueryClientProvider } from '@tanstack/react-query';
import { ConvexAuthProvider } from '@convex-dev/auth/react';
import { queryClient } from '~/utils/api';
import { convex } from '~/utils/convex';
import '../styles.css';
// This is the main layout of the app
// It wraps your pages with the providers they need
export default function RootLayout() {
const RootLayout = () => {
const colorScheme = useColorScheme();
return (
<QueryClientProvider client={queryClient}>
{/*
The Stack component displays the current page.
It also allows you to configure your screens
*/}
<ConvexAuthProvider client={convex}>
<Stack
screenOptions={{
headerStyle: {
backgroundColor: '#c03484',
backgroundColor: colorScheme === 'dark' ? '#1c1917' : '#faf9f7',
},
headerTintColor: colorScheme === 'dark' ? '#fafaf9' : '#1c1917',
contentStyle: {
backgroundColor: colorScheme == 'dark' ? '#09090B' : '#FFFFFF',
backgroundColor: colorScheme === 'dark' ? '#1c1917' : '#faf9f7',
},
}}
/>
<StatusBar />
</QueryClientProvider>
<StatusBar style='auto' />
</ConvexAuthProvider>
);
}
};
export default RootLayout;

View File

@@ -1,172 +1,54 @@
import { useState } from 'react';
import { Pressable, Text, TextInput, View } from 'react-native';
import { Pressable, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Link, Stack } from 'expo-router';
import { LegendList } from '@legendapp/list';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Stack } from 'expo-router';
import { useAuthActions } from '@convex-dev/auth/react';
import { useConvexAuth, useQuery } from 'convex/react';
import type { RouterOutputs } from '~/utils/api';
import { trpc } from '~/utils/api';
import { authClient } from '~/utils/auth';
import { api } from '@gib/backend/convex/_generated/api.js';
const Index = () => {
const { isAuthenticated, isLoading } = useConvexAuth();
const { signOut } = useAuthActions();
const user = useQuery(api.auth.getUser, isAuthenticated ? {} : 'skip');
function PostCard(props: {
post: RouterOutputs['post']['all'][number];
onDelete: () => void;
}) {
return (
<View className='bg-muted flex flex-row rounded-lg p-4'>
<View className='grow'>
<Link
asChild
href={{
pathname: '/post/[id]',
params: { id: props.post.id },
}}
>
<Pressable className=''>
<Text className='text-primary text-xl font-semibold'>
{props.post.title}
<SafeAreaView className='bg-background flex-1'>
<Stack.Screen options={{ title: 'Home' }} />
<View className='flex-1 items-center justify-center gap-4 p-6'>
<Text className='text-foreground text-4xl font-bold'>
Convex Monorepo
</Text>
<Text className='text-muted-foreground text-center text-base'>
Your self-hosted Expo + Convex starter
</Text>
{isLoading ? (
<Text className='text-muted-foreground'>Loading...</Text>
) : isAuthenticated ? (
<View className='w-full gap-3'>
<Text className='text-foreground text-center text-lg'>
Welcome{user?.name ? `, ${user.name}` : ''}!
</Text>
<Text className='text-foreground mt-2'>{props.post.content}</Text>
</Pressable>
</Link>
</View>
<Pressable onPress={props.onDelete}>
<Text className='text-primary font-bold uppercase'>Delete</Text>
</Pressable>
</View>
);
}
function CreatePost() {
const queryClient = useQueryClient();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const { mutate, error } = useMutation(
trpc.post.create.mutationOptions({
async onSuccess() {
setTitle('');
setContent('');
await queryClient.invalidateQueries(trpc.post.all.queryFilter());
},
}),
);
return (
<View className='mt-4 flex gap-2'>
<TextInput
className='border-input bg-background text-foreground items-center rounded-md border px-3 text-lg leading-tight'
value={title}
onChangeText={setTitle}
placeholder='Title'
/>
{error?.data?.zodError?.fieldErrors.title && (
<Text className='text-destructive mb-2'>
{error.data.zodError.fieldErrors.title}
</Text>
)}
<TextInput
className='border-input bg-background text-foreground items-center rounded-md border px-3 text-lg leading-tight'
value={content}
onChangeText={setContent}
placeholder='Content'
/>
{error?.data?.zodError?.fieldErrors.content && (
<Text className='text-destructive mb-2'>
{error.data.zodError.fieldErrors.content}
</Text>
)}
<Pressable
className='bg-primary flex items-center rounded-sm p-2'
onPress={() => {
mutate({
title,
content,
});
}}
>
<Text className='text-foreground'>Create</Text>
</Pressable>
{error?.data?.code === 'UNAUTHORIZED' && (
<Text className='text-destructive mt-2'>
You need to be logged in to create a post
</Text>
)}
</View>
);
}
function MobileAuth() {
const { data: session } = authClient.useSession();
return (
<>
<Text className='text-foreground pb-2 text-center text-xl font-semibold'>
{session?.user.name ? `Hello, ${session.user.name}` : 'Not logged in'}
</Text>
<Pressable
onPress={() =>
session
? authClient.signOut()
: authClient.signIn.social({
provider: 'discord',
callbackURL: '/',
})
}
className='bg-primary flex items-center rounded-sm p-2'
>
<Text>{session ? 'Sign Out' : 'Sign In With Discord'}</Text>
</Pressable>
</>
);
}
export default function Index() {
const queryClient = useQueryClient();
const postQuery = useQuery(trpc.post.all.queryOptions());
const deletePostMutation = useMutation(
trpc.post.delete.mutationOptions({
onSettled: () =>
queryClient.invalidateQueries(trpc.post.all.queryFilter()),
}),
);
return (
<SafeAreaView className='bg-background'>
{/* Changes page title visible on the header */}
<Stack.Screen options={{ title: 'Home Page' }} />
<View className='bg-background h-full w-full p-4'>
<Text className='text-foreground pb-2 text-center text-5xl font-bold'>
Create <Text className='text-primary'>T3</Text> Turbo
</Text>
<MobileAuth />
<View className='py-2'>
<Text className='text-primary font-semibold italic'>
Press on a post
</Text>
</View>
<LegendList
data={postQuery.data ?? []}
estimatedItemSize={20}
keyExtractor={(item) => item.id}
ItemSeparatorComponent={() => <View className='h-2' />}
renderItem={(p) => (
<PostCard
post={p.item}
onDelete={() => deletePostMutation.mutate(p.item.id)}
/>
)}
/>
<CreatePost />
<Pressable
className='bg-primary items-center rounded-md p-3'
onPress={() => void signOut()}
>
<Text className='text-primary-foreground font-semibold'>
Sign Out
</Text>
</Pressable>
</View>
) : (
<View className='w-full gap-3'>
<Text className='text-muted-foreground text-center'>
Sign in to get started
</Text>
{/* Add sign-in UI here — see apps/next/src/app/(auth)/sign-in for patterns */}
</View>
)}
</View>
</SafeAreaView>
);
}
};
export default Index;

View File

@@ -1,24 +1,21 @@
import { SafeAreaView, Text, View } from 'react-native';
import { Stack, useGlobalSearchParams } from 'expo-router';
import { useQuery } from '@tanstack/react-query';
import { Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Stack, useLocalSearchParams } from 'expo-router';
import { trpc } from '~/utils/api';
export default function Post() {
const { id } = useGlobalSearchParams<{ id: string }>();
const { data } = useQuery(trpc.post.byId.queryOptions({ id }));
if (!data) return null;
const Post = () => {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<SafeAreaView className='bg-background'>
<Stack.Screen options={{ title: data.title }} />
<View className='h-full w-full p-4'>
<Text className='text-primary py-2 text-3xl font-bold'>
{data.title}
<SafeAreaView className='bg-background flex-1'>
<Stack.Screen options={{ title: 'Post' }} />
<View className='flex-1 p-4'>
<Text className='text-foreground text-2xl font-bold'>Post {id}</Text>
<Text className='text-muted-foreground mt-2'>
Implement your post detail screen here using Convex queries.
</Text>
<Text className='text-foreground py-4'>{data.content}</Text>
</View>
</SafeAreaView>
);
}
};
export default Post;

View File

@@ -1,3 +1,3 @@
@import 'tailwindcss';
@import 'nativewind/theme';
@import '@acme/tailwind-config/theme';
@import '@gib/tailwind-config/theme';

View File

@@ -1,49 +0,0 @@
import type { AppRouter } from '@acme/api';
import { QueryClient } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client';
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import superjson from 'superjson';
import { authClient } from './auth';
import { getBaseUrl } from './base-url';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// ...
},
},
});
/**
* A set of typesafe hooks for consuming your API.
*/
export const trpc = createTRPCOptionsProxy<AppRouter>({
client: createTRPCClient({
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === 'development' ||
(opts.direction === 'down' && opts.result instanceof Error),
colorMode: 'ansi',
}),
httpBatchLink({
transformer: superjson,
url: `${getBaseUrl()}/api/trpc`,
headers() {
const headers = new Map<string, string>();
headers.set('x-trpc-source', 'expo-react');
const cookies = authClient.getCookie();
if (cookies) {
headers.set('Cookie', cookies);
}
return headers;
},
}),
],
}),
queryClient,
});
export type { RouterInputs, RouterOutputs } from '@acme/api';

View File

@@ -1,16 +0,0 @@
import * as SecureStore from 'expo-secure-store';
import { expoClient } from '@better-auth/expo/client';
import { createAuthClient } from 'better-auth/react';
import { getBaseUrl } from './base-url';
export const authClient = createAuthClient({
baseURL: getBaseUrl(),
plugins: [
expoClient({
scheme: 'expo',
storagePrefix: 'expo',
storage: SecureStore,
}),
],
});

View File

@@ -0,0 +1,26 @@
import Constants from 'expo-constants';
import { ConvexReactClient } from 'convex/react';
const getConvexUrl = (): string => {
// Allow override via Expo extra config (set in app.config.ts for production)
const fromConfig = Constants.expoConfig?.extra?.convexUrl as
| string
| undefined;
if (fromConfig) return fromConfig;
// Fall back to deriving from the dev server host (same pattern as getBaseUrl)
const debuggerHost = Constants.expoConfig?.hostUri;
const localhost = debuggerHost?.split(':')[0];
if (!localhost) {
throw new Error(
'Could not determine Convex URL. Set extra.convexUrl in app.config.ts for production.',
);
}
// Point at the self-hosted Convex backend on the local network
// Update this port if your Convex backend runs on a different port
return `http://${localhost}:3210`;
};
export const convex = new ConvexReactClient(getConvexUrl());

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ const Profile = async () => {
{/* Profile Card */}
<Card className='border-border/40'>
<ProfileHeader preloadedUser={preloadedUser} />
<ProfileHeader />
<AvatarUpload preloadedUser={preloadedUser} />
<Separator className='my-6' />
<UserInfoForm

View File

@@ -3,6 +3,7 @@
"compilerOptions": {
"lib": ["ES2022", "dom", "dom.iterable"],
"jsx": "preserve",
"types": ["node"],
"paths": {
"@/*": ["./src/*"]
},