Update expo app to make it somewhat functional
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"build": {
|
||||
"base": {
|
||||
"node": "22.12.0",
|
||||
"pnpm": "9.15.4",
|
||||
"bun": "1.3.10",
|
||||
"ios": {
|
||||
"resourceClass": "m-medium"
|
||||
}
|
||||
|
||||
@@ -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/**'],
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = require('@acme/tailwind-config/postcss-config');
|
||||
module.exports = require('@gib/tailwind-config/postcss-config');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'nativewind/theme';
|
||||
@import '@acme/tailwind-config/theme';
|
||||
@import '@gib/tailwind-config/theme';
|
||||
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
});
|
||||
26
apps/expo/src/utils/convex.ts
Normal file
26
apps/expo/src/utils/convex.ts
Normal 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
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "dom", "dom.iterable"],
|
||||
"jsx": "preserve",
|
||||
"types": ["node"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user