General fixes. Still far from good or working

This commit is contained in:
2026-01-11 13:21:01 -05:00
parent 0a361f51a1
commit 67daefb919
83 changed files with 733 additions and 1053 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/convex-monorepo/apps/expo/package.json",{"size":2249,"mtime":1766222924000,"hash":"64","data":"65"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts",{"size":28,"mtime":1768155639384,"hash":"66","data":"67"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/metro.config.js",{"size":511,"mtime":1768155639442,"hash":"68","data":"69"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx",{"size":5019,"mtime":1768155639734,"hash":"70","data":"71"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/turbo.json",{"size":163,"mtime":1766222924000,"hash":"72","data":"73"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/postcss.config.js",{"size":66,"mtime":1768155639501,"hash":"74","data":"75"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eas.json",{"size":567,"mtime":1766222924000,"hash":"76","data":"77"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.expo-shared/assets.json",{"size":155,"mtime":1766222924000,"hash":"78","data":"79"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx",{"size":927,"mtime":1768155639581,"hash":"80","data":"81"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts",{"size":246,"mtime":1766222924000,"hash":"82","data":"83"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/auth.ts",{"size":398,"mtime":1768155639911,"hash":"84","data":"85"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx",{"size":757,"mtime":1768155639785,"hash":"86","data":"87"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/styles.css",{"size":90,"mtime":1768155639819,"hash":"88","data":"89"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts",{"size":272,"mtime":1768155639965,"hash":"90","data":"91"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-dark.png",{"size":19633,"mtime":1766222924000,"hash":"92"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eslint.config.mts",{"size":275,"mtime":1768155639360,"hash":"93","data":"94"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/api.tsx",{"size":1326,"mtime":1768155639881,"hash":"95","data":"96"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts",{"size":880,"mtime":1768155639943,"hash":"97","data":"98"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/app.config.ts",{"size":1333,"mtime":1768155639275,"hash":"99","data":"100"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-light.png",{"size":19133,"mtime":1766222924000,"hash":"101"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/tsconfig.json",{"size":387,"mtime":1766228480000,"hash":"102","data":"103"},"d8763702c14cdc382dcfb84f6f9a068f",{"hashOfOptions":"104"},"11cdbef6afa001cd39bc187041ca6865",{"hashOfOptions":"105"},"dbe97bcde588a81538bbcd6a9befdddd",{"hashOfOptions":"106"},"1c10eb388cf5dcbc87d2d63770a227f1",{"hashOfOptions":"107"},"c7d4dcf839dfeaa02e0407adfd5e47a6",{"hashOfOptions":"108"},"b7edffce093c4c84092cc93f3dc208ef",{"hashOfOptions":"109"},"a3c1487f8318513ae7c156acc857fde2",{"hashOfOptions":"110"},"0f7f54c7161b8403d3bc42d91f59cd91",{"hashOfOptions":"111"},"8e407b4b1b0c0bd9c862a00243344be3",{"hashOfOptions":"112"},"d4d589c153ac8b5e7bf0fb130a5b5a7d",{"hashOfOptions":"113"},"cecbed1604a530a7cc099fecddddd76c",{"hashOfOptions":"114"},"4fcefde979d34a7339f7a266d3ec931b",{"hashOfOptions":"115"},"52a1d72379b952dd802f47e1865bd0da",{"hashOfOptions":"116"},"1bc3e15a40c117eecc51294886ea9b38",{"hashOfOptions":"117"},"1e8ac0d261e95efb19d290ffcf70ce36","1c1710ce3de3ce02e8054cc3787c8579",{"hashOfOptions":"118"},"5ff899a601102659dcbd2900e415ce8b",{"hashOfOptions":"119"},"dd2007a211e323deabb3f7fa7d16313f",{"hashOfOptions":"120"},"4f49c6df7733f874fbe72b4e20b3092b",{"hashOfOptions":"121"},"863da15dbd856008b7c24077ca746d91","6937fb7370f1e17491df649888d6ecc9",{"hashOfOptions":"122"},"2742538293","3367041120","3963086909","3173464737","3850474653","3346849767","2045418596","3048291867","3525769784","4000169781","471581913","1427279653","2305419495","818733105","2587128410","1262173017","408469422","2836740315","2315460658"]

View File

@@ -1,32 +1,32 @@
import type { ConfigContext, ExpoConfig } from "expo/config";
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",
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: ["**/*"],
assetBundlePatterns: ['**/*'],
ios: {
bundleIdentifier: "your.bundle.identifier",
bundleIdentifier: 'your.bundle.identifier',
supportsTablet: true,
icon: {
light: "./assets/icon-light.png",
dark: "./assets/icon-dark.png",
light: './assets/icon-light.png',
dark: './assets/icon-dark.png',
},
},
android: {
package: "your.bundle.identifier",
package: 'your.bundle.identifier',
adaptiveIcon: {
foregroundImage: "./assets/icon-light.png",
backgroundColor: "#1F104A",
foregroundImage: './assets/icon-light.png',
backgroundColor: '#1F104A',
},
edgeToEdgeEnabled: true,
},
@@ -42,17 +42,17 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
reactCompiler: true,
},
plugins: [
"expo-router",
"expo-secure-store",
"expo-web-browser",
'expo-router',
'expo-secure-store',
'expo-web-browser',
[
"expo-splash-screen",
'expo-splash-screen',
{
backgroundColor: "#E4E4E7",
image: "./assets/icon-light.png",
backgroundColor: '#E4E4E7',
image: './assets/icon-light.png',
dark: {
backgroundColor: "#18181B",
image: "./assets/icon-dark.png",
backgroundColor: '#18181B',
image: './assets/icon-dark.png',
},
},
],

View File

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

View File

@@ -1 +1 @@
import "expo-router/entry";
import 'expo-router/entry';

View File

@@ -1,14 +1,14 @@
// 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 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"),
root: path.join(__dirname, 'node_modules', '.cache', 'metro'),
}),
];

View File

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

View File

@@ -1,11 +1,11 @@
import { useColorScheme } from "react-native";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { QueryClientProvider } from "@tanstack/react-query";
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 { queryClient } from '~/utils/api';
import "../styles.css";
import '../styles.css';
// This is the main layout of the app
// It wraps your pages with the providers they need
@@ -20,10 +20,10 @@ export default function RootLayout() {
<Stack
screenOptions={{
headerStyle: {
backgroundColor: "#c03484",
backgroundColor: '#c03484',
},
contentStyle: {
backgroundColor: colorScheme == "dark" ? "#09090B" : "#FFFFFF",
backgroundColor: colorScheme == 'dark' ? '#09090B' : '#FFFFFF',
},
}}
/>

View File

@@ -1,16 +1,16 @@
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 { 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";
import type { RouterOutputs } from '~/utils/api';
import { trpc } from '~/utils/api';
import { authClient } from '~/utils/auth';
function PostCard(props: {
post: RouterOutputs["post"]["all"][number];
post: RouterOutputs['post']['all'][number];
onDelete: () => void;
}) {
return (
@@ -19,7 +19,7 @@ function PostCard(props: {
<Link
asChild
href={{
pathname: "/post/[id]",
pathname: '/post/[id]',
params: { id: props.post.id },
}}
>
@@ -41,14 +41,14 @@ function PostCard(props: {
function CreatePost() {
const queryClient = useQueryClient();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const { mutate, error } = useMutation(
trpc.post.create.mutationOptions({
async onSuccess() {
setTitle("");
setContent("");
setTitle('');
setContent('');
await queryClient.invalidateQueries(trpc.post.all.queryFilter());
},
}),
@@ -89,7 +89,7 @@ function CreatePost() {
>
<Text className="text-foreground">Create</Text>
</Pressable>
{error?.data?.code === "UNAUTHORIZED" && (
{error?.data?.code === 'UNAUTHORIZED' && (
<Text className="text-destructive mt-2">
You need to be logged in to create a post
</Text>
@@ -104,20 +104,20 @@ function MobileAuth() {
return (
<>
<Text className="text-foreground pb-2 text-center text-xl font-semibold">
{session?.user.name ? `Hello, ${session.user.name}` : "Not logged in"}
{session?.user.name ? `Hello, ${session.user.name}` : 'Not logged in'}
</Text>
<Pressable
onPress={() =>
session
? authClient.signOut()
: authClient.signIn.social({
provider: "discord",
callbackURL: "/",
provider: 'discord',
callbackURL: '/',
})
}
className="bg-primary flex items-center rounded-sm p-2"
>
<Text>{session ? "Sign Out" : "Sign In With Discord"}</Text>
<Text>{session ? 'Sign Out' : 'Sign In With Discord'}</Text>
</Pressable>
</>
);
@@ -138,7 +138,7 @@ export default function Index() {
return (
<SafeAreaView className="bg-background">
{/* Changes page title visible on the header */}
<Stack.Screen options={{ title: "Home Page" }} />
<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

View File

@@ -1,8 +1,8 @@
import { SafeAreaView, Text, View } from "react-native";
import { Stack, useGlobalSearchParams } from "expo-router";
import { useQuery } from "@tanstack/react-query";
import { SafeAreaView, Text, View } from 'react-native';
import { Stack, useGlobalSearchParams } from 'expo-router';
import { useQuery } from '@tanstack/react-query';
import { trpc } from "~/utils/api";
import { trpc } from '~/utils/api';
export default function Post() {
const { id } = useGlobalSearchParams<{ id: string }>();

View File

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

View File

@@ -1,12 +1,11 @@
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 { 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";
import { authClient } from './auth';
import { getBaseUrl } from './base-url';
export const queryClient = new QueryClient({
defaultOptions: {
@@ -24,20 +23,20 @@ export const trpc = createTRPCOptionsProxy<AppRouter>({
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
colorMode: "ansi",
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");
headers.set('x-trpc-source', 'expo-react');
const cookies = authClient.getCookie();
if (cookies) {
headers.set("Cookie", cookies);
headers.set('Cookie', cookies);
}
return headers;
},
@@ -47,4 +46,4 @@ export const trpc = createTRPCOptionsProxy<AppRouter>({
queryClient,
});
export type { RouterInputs, RouterOutputs } from "@acme/api";
export type { RouterInputs, RouterOutputs } from '@acme/api';

View File

@@ -1,15 +1,15 @@
import * as SecureStore from "expo-secure-store";
import { expoClient } from "@better-auth/expo/client";
import { createAuthClient } from "better-auth/react";
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";
import { getBaseUrl } from './base-url';
export const authClient = createAuthClient({
baseURL: getBaseUrl(),
plugins: [
expoClient({
scheme: "expo",
storagePrefix: "expo",
scheme: 'expo',
storagePrefix: 'expo',
storage: SecureStore,
}),
],

View File

@@ -1,4 +1,4 @@
import Constants from "expo-constants";
import Constants from 'expo-constants';
/**
* Extend this function when going to production by
@@ -14,12 +14,12 @@ export const getBaseUrl = () => {
* baseUrl to your production API URL.
*/
const debuggerHost = Constants.expoConfig?.hostUri;
const localhost = debuggerHost?.split(":")[0];
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.",
'Failed to get localhost. Please point to your production server.',
);
}
return `http://${localhost}:3000`;

View File

@@ -1,6 +1,6 @@
import * as SecureStore from "expo-secure-store";
import * as SecureStore from 'expo-secure-store';
const key = "session_token";
const key = 'session_token';
export const getToken = () => SecureStore.getItem(key);
export const deleteToken = () => SecureStore.deleteItemAsync(key);

View File

@@ -0,0 +1 @@
[["1","2","3","4","5","6","7","8","9","10","11"],{"key":"12","value":"13"},{"key":"14","value":"15"},{"key":"16","value":"17"},{"key":"18","value":"19"},{"key":"20","value":"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"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/env.ts",{"size":1676,"mtime":1768155639474,"hash":"34","data":"35"},"/home/gib/Documents/Code/convex-monorepo/apps/next/postcss.config.js",{"size":63,"mtime":1766225879000,"hash":"36","data":"37"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/favicon.ico",{"size":103027,"mtime":1766225172000,"hash":"38"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/layout.tsx",{"size":1885,"mtime":1768155639311,"hash":"39","data":"40"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/t3-icon.svg",{"size":923,"mtime":1766225172000,"hash":"41"},"/home/gib/Documents/Code/convex-monorepo/apps/next/next.config.js",{"size":2062,"mtime":1768155639139,"hash":"42","data":"43"},"/home/gib/Documents/Code/convex-monorepo/apps/next/package.json",{"size":1506,"mtime":1768154671185,"hash":"44","data":"45"},"/home/gib/Documents/Code/convex-monorepo/apps/next/tsconfig.json",{"size":297,"mtime":1766225936000,"hash":"46","data":"47"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/styles.css",{"size":596,"mtime":1768155639429,"hash":"48","data":"49"},"/home/gib/Documents/Code/convex-monorepo/apps/next/eslint.config.ts",{"size":369,"mtime":1768155639048,"hash":"50","data":"51"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/page.tsx",{"size":1169,"mtime":1768155639357,"hash":"52","data":"53"},"08dcc17ca04632a7262728b699450d5d",{"hashOfOptions":"54"},"d9fa362e836306677416d4e30dfb1528",{"hashOfOptions":"55"},"36aa513994f9883f30ff408fe777cec6","f819b8d99aba405d3975e6e5d3adf796",{"hashOfOptions":"56"},"44418c5550aee51cfdf0a321ad70ae47","1f2ba5c5e807955994fd4dfca4c709be",{"hashOfOptions":"57"},"6dba01a7cb29904291cb3c9930fc1ac3",{"hashOfOptions":"58"},"785dd21512a5bdf43635be77f6a483ae",{"hashOfOptions":"59"},"48e377494d0046d38b923af54e109529",{"hashOfOptions":"60"},"a9eecac327a9c09dd26e82c515134edc",{"hashOfOptions":"61"},"3847ea1421b6ea4079a526d7e44c946d",{"hashOfOptions":"62"},"1049592143","533286024","3492548434","1155092478","4045122132","2138361299","3590205716","4261166930","1190248983"]

View File

@@ -1,16 +1,15 @@
import { defineConfig } from "eslint/config";
import { defineConfig } from 'eslint/config';
import { baseConfig, restrictEnvAccess } from "@gib/eslint-config/base";
import { nextjsConfig } from "@gib/eslint-config/nextjs";
import { reactConfig } from "@gib/eslint-config/react";
import { baseConfig, restrictEnvAccess } from '@gib/eslint-config/base';
import { nextjsConfig } from '@gib/eslint-config/nextjs';
import { reactConfig } from '@gib/eslint-config/react';
export default defineConfig(
{
ignores: [".next/**"],
ignores: ['.next/**'],
},
baseConfig,
reactConfig,
nextjsConfig,
restrictEnvAccess,
);

View File

@@ -1,12 +1,11 @@
import { env } from './src/env';
import { withSentryConfig } from '@sentry/nextjs';
import { createJiti } from 'jiti';
import { withPlausibleProxy } from 'next-plausible';
import { createJiti } from "jiti";
const jiti = createJiti(import.meta.url);
// Import env files to validate at build time. Use jiti so we can load .ts files in here.
await jiti.import("./src/env");
await jiti.import('./src/env');
/** @type {import("next").NextConfig} */
const config = withPlausibleProxy({
@@ -28,15 +27,10 @@ const config = withPlausibleProxy({
},
},
/** Enables hot reloading for local packages without a build step */
transpilePackages: [
"@gib/backend",
"@gib/ui",
],
transpilePackages: ['@gib/backend', '@gib/ui'],
typescript: { ignoreBuildErrors: true },
});
const sentryConfig = {
// For all available options, see:
// https://www.npmjs.com/package/@sentry/webpack-plugin#options

View File

@@ -1,5 +1,5 @@
{
"name": "@gib/nextjs",
"name": "@gib/next",
"version": "0.1.0",
"type": "module",
"private": true,

View File

@@ -1,58 +0,0 @@
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { Button } from "@acme/ui/button";
import { auth, getSession } from "~/auth/server";
export async function AuthShowcase() {
const session = await getSession();
if (!session) {
return (
<form>
<Button
size="lg"
formAction={async () => {
"use server";
const res = await auth.api.signInSocial({
body: {
provider: "discord",
callbackURL: "/",
},
});
if (!res.url) {
throw new Error("No URL returned from signInSocial");
}
redirect(res.url);
}}
>
Sign in with Discord
</Button>
</form>
);
}
return (
<div className="flex flex-col items-center justify-center gap-4">
<p className="text-center text-2xl">
<span>Logged in as {session.user.name}</span>
</p>
<form>
<Button
size="lg"
formAction={async () => {
"use server";
await auth.api.signOut({
headers: await headers(),
});
redirect("/");
}}
>
Sign out
</Button>
</form>
</div>
);
}

View File

@@ -1,210 +0,0 @@
"use client";
import { useForm } from "@tanstack/react-form";
import {
useMutation,
useQueryClient,
useSuspenseQuery,
} from "@tanstack/react-query";
import type { RouterOutputs } from "@acme/api";
import { CreatePostSchema } from "@acme/db/schema";
import { cn } from "@acme/ui";
import { Button } from "@acme/ui/button";
import {
Field,
FieldContent,
FieldError,
FieldGroup,
FieldLabel,
} from "@acme/ui/field";
import { Input } from "@acme/ui/input";
import { toast } from "@acme/ui/toast";
import { useTRPC } from "~/trpc/react";
export function CreatePostForm() {
const trpc = useTRPC();
const queryClient = useQueryClient();
const createPost = useMutation(
trpc.post.create.mutationOptions({
onSuccess: async () => {
form.reset();
await queryClient.invalidateQueries(trpc.post.pathFilter());
},
onError: (err) => {
toast.error(
err.data?.code === "UNAUTHORIZED"
? "You must be logged in to post"
: "Failed to create post",
);
},
}),
);
const form = useForm({
defaultValues: {
content: "",
title: "",
},
validators: {
onSubmit: CreatePostSchema,
},
onSubmit: (data) => createPost.mutate(data.value),
});
return (
<form
className="w-full max-w-2xl"
onSubmit={(event) => {
event.preventDefault();
void form.handleSubmit();
}}
>
<FieldGroup>
<form.Field
name="title"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid;
return (
<Field data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
</FieldContent>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="Title"
/>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
);
}}
/>
<form.Field
name="content"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid;
return (
<Field data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor={field.name}>Content</FieldLabel>
</FieldContent>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="Content"
/>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
);
}}
/>
</FieldGroup>
<Button type="submit">Create</Button>
</form>
);
}
export function PostList() {
const trpc = useTRPC();
const { data: posts } = useSuspenseQuery(trpc.post.all.queryOptions());
if (posts.length === 0) {
return (
<div className="relative flex w-full flex-col gap-4">
<PostCardSkeleton pulse={false} />
<PostCardSkeleton pulse={false} />
<PostCardSkeleton pulse={false} />
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/10">
<p className="text-2xl font-bold text-white">No posts yet</p>
</div>
</div>
);
}
return (
<div className="flex w-full flex-col gap-4">
{posts.map((p) => {
return <PostCard key={p.id} post={p} />;
})}
</div>
);
}
export function PostCard(props: {
post: RouterOutputs["post"]["all"][number];
}) {
const trpc = useTRPC();
const queryClient = useQueryClient();
const deletePost = useMutation(
trpc.post.delete.mutationOptions({
onSuccess: async () => {
await queryClient.invalidateQueries(trpc.post.pathFilter());
},
onError: (err) => {
toast.error(
err.data?.code === "UNAUTHORIZED"
? "You must be logged in to delete a post"
: "Failed to delete post",
);
},
}),
);
return (
<div className="bg-muted flex flex-row rounded-lg p-4">
<div className="grow">
<h2 className="text-primary text-2xl font-bold">{props.post.title}</h2>
<p className="mt-2 text-sm">{props.post.content}</p>
</div>
<div>
<Button
variant="ghost"
className="text-primary cursor-pointer text-sm font-bold uppercase hover:bg-transparent hover:text-white"
onClick={() => deletePost.mutate(props.post.id)}
>
Delete
</Button>
</div>
</div>
);
}
export function PostCardSkeleton(props: { pulse?: boolean }) {
const { pulse = true } = props;
return (
<div className="bg-muted flex flex-row rounded-lg p-4">
<div className="grow">
<h2
className={cn(
"bg-primary w-1/4 rounded-sm text-2xl font-bold",
pulse && "animate-pulse",
)}
>
&nbsp;
</h2>
<p
className={cn(
"mt-2 w-1/3 rounded-sm bg-current text-sm",
pulse && "animate-pulse",
)}
>
&nbsp;
</p>
</div>
</div>
);
}

View File

@@ -1,4 +0,0 @@
import { auth } from "~/auth/server";
export const GET = auth.handler;
export const POST = auth.handler;

View File

@@ -1,46 +0,0 @@
import type { NextRequest } from "next/server";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter, createTRPCContext } from "@acme/api";
import { auth } from "~/auth/server";
/**
* Configure basic CORS headers
* You should extend this to match your needs
*/
const setCorsHeaders = (res: Response) => {
res.headers.set("Access-Control-Allow-Origin", "*");
res.headers.set("Access-Control-Request-Method", "*");
res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST");
res.headers.set("Access-Control-Allow-Headers", "*");
};
export const OPTIONS = () => {
const response = new Response(null, {
status: 204,
});
setCorsHeaders(response);
return response;
};
const handler = async (req: NextRequest) => {
const response = await fetchRequestHandler({
endpoint: "/api/trpc",
router: appRouter,
req,
createContext: () =>
createTRPCContext({
auth: auth,
headers: req.headers,
}),
onError({ error, path }) {
console.error(`>>> tRPC Error on '${path}'`, error);
},
});
setCorsHeaders(response);
return response;
};
export { handler as GET, handler as POST };

View File

@@ -1,50 +1,49 @@
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import type { Metadata, Viewport } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import { cn } from '@acme/ui';
import { ThemeProvider, ThemeToggle } from '@acme/ui/theme';
import { Toaster } from '@acme/ui/toast';
import { cn } from "@acme/ui";
import { ThemeProvider, ThemeToggle } from "@acme/ui/theme";
import { Toaster } from "@acme/ui/toast";
import { env } from '~/env';
import { TRPCReactProvider } from '~/trpc/react';
import { env } from "~/env";
import { TRPCReactProvider } from "~/trpc/react";
import "~/app/styles.css";
import '~/app/styles.css';
export const metadata: Metadata = {
metadataBase: new URL(
env.VERCEL_ENV === "production"
? "https://turbo.t3.gg"
: "http://localhost:3000",
env.VERCEL_ENV === 'production'
? 'https://turbo.t3.gg'
: 'http://localhost:3000',
),
title: "Create T3 Turbo",
description: "Simple monorepo with shared backend for web & mobile apps",
title: 'Create T3 Turbo',
description: 'Simple monorepo with shared backend for web & mobile apps',
openGraph: {
title: "Create T3 Turbo",
description: "Simple monorepo with shared backend for web & mobile apps",
url: "https://create-t3-turbo.vercel.app",
siteName: "Create T3 Turbo",
title: 'Create T3 Turbo',
description: 'Simple monorepo with shared backend for web & mobile apps',
url: 'https://create-t3-turbo.vercel.app',
siteName: 'Create T3 Turbo',
},
twitter: {
card: "summary_large_image",
site: "@jullerino",
creator: "@jullerino",
card: 'summary_large_image',
site: '@jullerino',
creator: '@jullerino',
},
};
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
{ media: '(prefers-color-scheme: light)', color: 'white' },
{ media: '(prefers-color-scheme: dark)', color: 'black' },
],
};
const geistSans = Geist({
subsets: ["latin"],
variable: "--font-geist-sans",
subsets: ['latin'],
variable: '--font-geist-sans',
});
const geistMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-geist-mono",
subsets: ['latin'],
variable: '--font-geist-mono',
});
export default function RootLayout(props: { children: React.ReactNode }) {
@@ -52,7 +51,7 @@ export default function RootLayout(props: { children: React.ReactNode }) {
<html lang="en" suppressHydrationWarning>
<body
className={cn(
"bg-background text-foreground min-h-screen font-sans antialiased",
'bg-background text-foreground min-h-screen font-sans antialiased',
geistSans.variable,
geistMono.variable,
)}

View File

@@ -1,12 +1,12 @@
import { Suspense } from "react";
import { Suspense } from 'react';
import { HydrateClient, prefetch, trpc } from "~/trpc/server";
import { AuthShowcase } from "./_components/auth-showcase";
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { AuthShowcase } from './_components/auth-showcase';
import {
CreatePostForm,
PostCardSkeleton,
PostList,
} from "./_components/posts";
} from './_components/posts';
export default function HomePage() {
prefetch(trpc.post.all.queryOptions());

View File

@@ -1,8 +1,8 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "@acme/tailwind-config/theme";
@import 'tailwindcss';
@import 'tw-animate-css';
@import '@acme/tailwind-config/theme';
@source "../../../../packages/ui/src/*.{ts,tsx}";
@source '../../../../packages/ui/src/*.{ts,tsx}';
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant light (&:where(.light, .light *));

View File

@@ -1,13 +1,11 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod/v4";
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod/v4';
export const env = createEnv({
shared: {
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
CI: z.boolean().default(false),
SITE_URL: z.string().default("http://localhost:3000"),
.enum(['development', 'production', 'test'])
.default('development'),
},
/**
* Specify your server-side environment variables schema here.
@@ -22,6 +20,7 @@ export const env = createEnv({
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
*/
client: {
NEXT_PUBLIC_SITE_URL: z.url(),
NEXT_PUBLIC_CONVEX_URL: z.url(),
NEXT_PUBLIC_PLAUSIBLE_URL: z.url(),
NEXT_PUBLIC_SENTRY_DSN: z.string(),
@@ -36,13 +35,15 @@ export const env = createEnv({
NODE_ENV: process.env.NODE_ENV,
CI: process.env.CI,
SITE_URL: process.env.SITE_URL,
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL,
NEXT_PUBLIC_PLAUSIBLE_URL: process.env.NEXT_PUBLIC_PLAUSIBLE_URL,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_SENTRY_URL: process.env.NEXT_PUBLIC_SENTRY_URL,
NEXT_PUBLIC_SENTRY_ORG: process.env.NEXT_PUBLIC_SENTRY_ORG,
NEXT_PUBLIC_SENTRY_PROJECT_NAME: process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
NEXT_PUBLIC_SENTRY_PROJECT_NAME:
process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
},
skipValidation:
!!process.env.CI || process.env.npm_lifecycle_event === "lint",
!!process.env.CI || process.env.npm_lifecycle_event === 'lint',
});