init commit

This commit is contained in:
2025-10-28 10:57:53 -05:00
commit 693a10957c
109 changed files with 19075 additions and 0 deletions

View File

@@ -0,0 +1 @@
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21"],{"key":"22","value":"23"},{"key":"24","value":"25"},{"key":"26","value":"27"},{"key":"28","value":"29"},{"key":"30","value":"31"},{"key":"32","value":"33"},{"key":"34","value":"35"},{"key":"36","value":"37"},{"key":"38","value":"39"},{"key":"40","value":"41"},{"key":"42","value":"43"},{"key":"44","value":"45"},{"key":"46","value":"47"},{"key":"48","value":"49"},{"key":"50","value":"51"},{"key":"52","value":"53"},{"key":"54","value":"55"},{"key":"56","value":"57"},{"key":"58","value":"59"},{"key":"60","value":"61"},{"key":"62","value":"63"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/eslint.config.mts",{"size":276,"mtime":1761443987000,"hash":"64","data":"65"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/src/app/index.tsx",{"size":5019,"mtime":1761443987000,"hash":"66","data":"67"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/app.config.ts",{"size":1333,"mtime":1761443987000,"hash":"68","data":"69"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/package.json",{"size":1736,"mtime":1761665033217,"hash":"70","data":"71"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/src/app/_layout.tsx",{"size":927,"mtime":1761443987000,"hash":"72","data":"73"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/metro.config.js",{"size":511,"mtime":1761443987000,"hash":"74","data":"75"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/src/utils/auth.ts",{"size":398,"mtime":1761443987000,"hash":"76","data":"77"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/index.ts",{"size":28,"mtime":1761443987000,"hash":"78","data":"79"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/src/utils/api.tsx",{"size":1327,"mtime":1761443987000,"hash":"80","data":"81"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/src/utils/session-store.ts",{"size":272,"mtime":1761443987000,"hash":"82","data":"83"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/turbo.json",{"size":163,"mtime":1761443987000,"hash":"84","data":"85"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/assets/icon-light.png",{"size":19133,"mtime":1761443987000,"hash":"86"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/eas.json",{"size":567,"mtime":1761443987000,"hash":"87","data":"88"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/postcss.config.js",{"size":66,"mtime":1761443987000,"hash":"89","data":"90"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/src/app/post/[id].tsx",{"size":757,"mtime":1761443987000,"hash":"91","data":"92"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/assets/icon-dark.png",{"size":19633,"mtime":1761443987000,"hash":"93"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/src/styles.css",{"size":90,"mtime":1761443987000,"hash":"94","data":"95"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/tsconfig.json",{"size":388,"mtime":1761663711587,"hash":"96","data":"97"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/src/utils/base-url.ts",{"size":880,"mtime":1761443987000,"hash":"98","data":"99"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/.expo-shared/assets.json",{"size":155,"mtime":1761443987000,"hash":"100","data":"101"},"/home/gib/Documents/Code/monorepo/convex-monorepo/apps/expo/nativewind-env.d.ts",{"size":246,"mtime":1761443987000,"hash":"102","data":"103"},"3157b700ecbb4aa217059352507464a2",{"hashOfOptions":"104"},"2be11479779af34f59ec5003513b8e0b",{"hashOfOptions":"105"},"bd476a3950d078330d3e06a560e9f747",{"hashOfOptions":"106"},"f137cf230cf525ca8c2f0bdf233a6e3f",{"hashOfOptions":"107"},"518cd9c33ede87c649b01dd727bb00a0",{"hashOfOptions":"108"},"fbe52c661bc5cbe8d2f7c1058981b896",{"hashOfOptions":"109"},"9455acf80595f4373e11d98e5bc87d89",{"hashOfOptions":"110"},"cc3395d2be4587e21093428fdb6f69e6",{"hashOfOptions":"111"},"0adb5df94af971795b21eddb560c26b5",{"hashOfOptions":"112"},"691dcf997a384f03d1ba5c043c1a3405",{"hashOfOptions":"113"},"c7d4dcf839dfeaa02e0407adfd5e47a6",{"hashOfOptions":"114"},"863da15dbd856008b7c24077ca746d91","a3c1487f8318513ae7c156acc857fde2",{"hashOfOptions":"115"},"5080fb7f49a201ad809050ee57249d41",{"hashOfOptions":"116"},"28454ca5c544a35129bb829072b8dbc1",{"hashOfOptions":"117"},"1e8ac0d261e95efb19d290ffcf70ce36","5068090396cd9f4f9463f09f43c9e257",{"hashOfOptions":"118"},"74d22c2830502b877ac3b806a788bfa3",{"hashOfOptions":"119"},"29e520338914a80764ca0866a87fac3d",{"hashOfOptions":"120"},"0f7f54c7161b8403d3bc42d91f59cd91",{"hashOfOptions":"121"},"d4d589c153ac8b5e7bf0fb130a5b5a7d",{"hashOfOptions":"122"},"3529965326","1818025557","2842754191","1504153565","3815129580","3236408049","1664229517","2979851528","3930068749","2135093465","2923444549","366302476","4133218715","3075567833","3597942159","1044122342","1450990178","3667891651","3412980713"]

View File

@@ -0,0 +1,4 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}

60
apps/expo/app.config.ts Normal file
View File

@@ -0,0 +1,60 @@
import type { ConfigContext, ExpoConfig } from "expo/config";
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: "expo",
slug: "expo",
scheme: "expo",
version: "0.1.0",
orientation: "portrait",
icon: "./assets/icon-light.png",
userInterfaceStyle: "automatic",
updates: {
fallbackToCacheTimeout: 0,
},
newArchEnabled: true,
assetBundlePatterns: ["**/*"],
ios: {
bundleIdentifier: "your.bundle.identifier",
supportsTablet: true,
icon: {
light: "./assets/icon-light.png",
dark: "./assets/icon-dark.png",
},
},
android: {
package: "your.bundle.identifier",
adaptiveIcon: {
foregroundImage: "./assets/icon-light.png",
backgroundColor: "#1F104A",
},
edgeToEdgeEnabled: true,
},
// extra: {
// eas: {
// projectId: "your-eas-project-id",
// },
// },
experiments: {
tsconfigPaths: true,
typedRoutes: true,
reactCanary: true,
reactCompiler: true,
},
plugins: [
"expo-router",
"expo-secure-store",
"expo-web-browser",
[
"expo-splash-screen",
{
backgroundColor: "#E4E4E7",
image: "./assets/icon-light.png",
dark: {
backgroundColor: "#18181B",
image: "./assets/icon-dark.png",
},
},
],
],
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

33
apps/expo/eas.json Normal file
View File

@@ -0,0 +1,33 @@
{
"cli": {
"version": ">= 4.1.2",
"appVersionSource": "remote"
},
"build": {
"base": {
"node": "22.12.0",
"pnpm": "9.15.4",
"ios": {
"resourceClass": "m-medium"
}
},
"development": {
"extends": "base",
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"extends": "base",
"distribution": "internal",
"ios": {
"simulator": true
}
},
"production": {
"extends": "base"
}
},
"submit": {
"production": {}
}
}

View File

@@ -0,0 +1,12 @@
import { defineConfig } from "eslint/config";
import { baseConfig } from "@acme/eslint-config/base";
import { reactConfig } from "@acme/eslint-config/react";
export default defineConfig(
{
ignores: [".expo/**", "expo-plugins/**"],
},
baseConfig,
reactConfig,
);

1
apps/expo/index.ts Normal file
View File

@@ -0,0 +1 @@
import "expo-router/entry";

16
apps/expo/metro.config.js Normal file
View File

@@ -0,0 +1,16 @@
// Learn more: https://docs.expo.dev/guides/monorepos/
const path = require("node:path");
const { getDefaultConfig } = require("expo/metro-config");
const { FileStore } = require("metro-cache");
const { withNativewind } = require("nativewind/metro");
const config = getDefaultConfig(__dirname);
config.cacheStores = [
new FileStore({
root: path.join(__dirname, "node_modules", ".cache", "metro"),
}),
];
/** @type {import('expo/metro-config').MetroConfig} */
module.exports = withNativewind(config);

3
apps/expo/nativewind-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference types="react-native-css/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by react-native-css. If you need to move or disable this file, please see the documentation.

55
apps/expo/package.json Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "@acme/expo",
"private": true,
"main": "index.ts",
"scripts": {
"clean": "git clean -xdf .cache .expo .turbo android ios node_modules",
"dev": "expo start",
"dev:android": "expo start --android",
"dev:ios": "expo start --ios",
"android": "expo run:android",
"ios": "expo run:ios",
"format": "prettier --check . --ignore-path ../../.gitignore --ignore-path .prettierignore",
"lint": "eslint --flag unstable_native_nodejs_ts_config",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@acme/backend": "workspace:*",
"@convex-dev/auth": "workspace:*",
"@legendapp/list": "^2.0.14",
"convex": "workspace:*",
"expo": "~54.0.20",
"expo-constants": "~18.0.10",
"expo-dev-client": "~6.0.16",
"expo-linking": "~8.0.8",
"expo-router": "~6.0.13",
"expo-secure-store": "~15.0.7",
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
"expo-system-ui": "~6.0.8",
"expo-web-browser": "~15.0.8",
"nativewind": "5.0.0-preview.2",
"react": "catalog:react19",
"react-dom": "catalog:react19",
"react-native": "~0.81.5",
"react-native-css": "3.0.1",
"react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~4.1.3",
"react-native-safe-area-context": "~5.6.1",
"react-native-screens": "~4.16.0",
"react-native-worklets": "~0.5.1",
"superjson": "2.2.3"
},
"devDependencies": {
"@acme/eslint-config": "workspace:*",
"@acme/prettier-config": "workspace:*",
"@acme/tailwind-config": "workspace:*",
"@acme/tsconfig": "workspace:*",
"@types/react": "catalog:react19",
"eslint": "catalog:",
"prettier": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:"
},
"prettier": "@acme/prettier-config"
}

View File

@@ -0,0 +1 @@
module.exports = require("@acme/tailwind-config/postcss-config");

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);

20
apps/expo/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"extends": ["@acme/tsconfig/base.json"],
"compilerOptions": {
"jsx": "react-native",
"checkJs": false,
"moduleSuffixes": [".ios", ".android", ".native", ""],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"src",
"*.ts",
"*.js",
".expo/types/**/*.ts",
"expo-env.d.ts",
"nativewind-env.d.ts"
],
"exclude": ["node_modules"]
}

10
apps/expo/turbo.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "https://turborepo.com/schema.json",
"extends": ["//"],
"tasks": {
"dev": {
"persistent": true,
"interactive": true
}
}
}