Rewrote turborepo. Hopefully this is a bit more clean & easy to understand for me.

This commit is contained in:
2025-10-29 11:39:17 -05:00
parent 8b0f811ed6
commit 75505759f1
147 changed files with 8671 additions and 925 deletions

View File

@@ -0,0 +1,33 @@
import { useColorScheme } from "react-native";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "~/utils/api";
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 colorScheme = useColorScheme();
return (
<QueryClientProvider client={queryClient}>
{/*
The Stack component displays the current page.
It also allows you to configure your screens
*/}
<Stack
screenOptions={{
headerStyle: {
backgroundColor: "#c03484",
},
contentStyle: {
backgroundColor: colorScheme == "dark" ? "#09090B" : "#FFFFFF",
},
}}
/>
<StatusBar />
</QueryClientProvider>
);
}

172
apps/expo/src/app/index.tsx Normal file
View File

@@ -0,0 +1,172 @@
import { useState } from "react";
import { Pressable, Text, TextInput, 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 type { RouterOutputs } from "~/utils/api";
import { trpc } from "~/utils/api";
import { authClient } from "~/utils/auth";
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}
</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 />
</View>
</SafeAreaView>
);
}

View File

@@ -0,0 +1,24 @@
import { SafeAreaView, Text, View } from "react-native";
import { Stack, useGlobalSearchParams } from "expo-router";
import { useQuery } from "@tanstack/react-query";
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;
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}
</Text>
<Text className="text-foreground py-4">{data.content}</Text>
</View>
</SafeAreaView>
);
}

3
apps/expo/src/styles.css Normal file
View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
@import "nativewind/theme";
@import "@acme/tailwind-config/theme";

View File

@@ -0,0 +1,50 @@
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 type { AppRouter } from "@acme/api";
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

@@ -0,0 +1,16 @@
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";
/**
* Extend this function when going to production by
* setting the baseUrl to your production API URL.
*/
export const getBaseUrl = () => {
/**
* Gets the IP address of your host-machine. If it cannot automatically find it,
* you'll have to manually set it. NOTE: Port 3000 should work for most but confirm
* you don't have anything else running on it, or you'd have to change it.
*
* **NOTE**: This is only for development. In production, you'll want to set the
* baseUrl to your production API URL.
*/
const debuggerHost = Constants.expoConfig?.hostUri;
const localhost = debuggerHost?.split(":")[0];
if (!localhost) {
// return "https://turbo.t3.gg";
throw new Error(
"Failed to get localhost. Please point to your production server.",
);
}
return `http://${localhost}:3000`;
};

View File

@@ -0,0 +1,7 @@
import * as SecureStore from "expo-secure-store";
const key = "session_token";
export const getToken = () => SecureStore.getItem(key);
export const deleteToken = () => SecureStore.deleteItemAsync(key);
export const setToken = (v: string) => SecureStore.setItem(key, v);