diff --git a/.env.example b/.env.example index 690faf4..7e94b2e 100644 --- a/.env.example +++ b/.env.example @@ -2,13 +2,25 @@ # Keep this file up-to-date when you add new variables to \`.env\`. # This file will be committed to version control, so make sure not to have any secrets in it. # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets. -NODE_ENV= -CI= -SKIP_ENV_VALIDATION= +## Next.js ## +NODE_ENV=development +CI=false +SITE_URL= +SENTRY_AUTH_TOKEN= +NEXT_PUBLIC_SITE_URL= +NEXT_PUBLIC_CONVEX_URL= +NEXT_PUBLIC_PLAUSIBLE_URL= +NEXT_PUBLIC_SENTRY_DSN= +NEXT_PUBLIC_SENTRY_URL= +NEXT_PUBLIC_SENTRY_ORG= +NEXT_PUBLIC_SENTRY_PROJECT_NAME= + +## Convex ## CONVEX_SELF_HOSTED_URL= CONVEX_SELF_HOSTED_ADMIN_KEY= -CONVEX_AUTH_URL= -SITE_URL= +SETUP_SCRIPT_RAN= +# Convex Auth +AUTH_URL=convex.example.com USESEND_API_KEY= AUTH_AUTHENTIK_ID= AUTH_AUTHENTIK_SECRET= @@ -16,8 +28,4 @@ AUTH_AUTHENTIK_ISSUER= AUTH_MICROSOFT_ENTRA_ID_ID= AUTH_MICROSOFT_ENTRA_ID_SECRET= AUTH_MICROSOFT_ENTRA_ID_ISSUER= -AUTH_MICROSOFT_ENTRA_ID_AUTH_UR= -SENTRY_AUTH_TOKEN= -SENTRY_DSN= -SENTRY_ORG= -SENTRY_PROJECT_NAME= +AUTH_MICROSOFT_ENTRA_ID_AUTH_URL= diff --git a/apps/expo/tsconfig.json b/apps/expo/tsconfig.json index d4fe979..562b919 100644 --- a/apps/expo/tsconfig.json +++ b/apps/expo/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": ["@acme/tsconfig/base.json"], + "extends": ["@gib/tsconfig/base.json"], "compilerOptions": { "jsx": "react-native", "checkJs": false, diff --git a/apps/next/.gitignore b/apps/next/.gitignore deleted file mode 100644 index f886745..0000000 --- a/apps/next/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# env files (can opt-in for commiting if needed) -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/apps/next/README.md b/apps/next/README.md deleted file mode 100644 index a98bfa8..0000000 --- a/apps/next/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/apps/next/app/favicon.ico b/apps/next/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/apps/next/app/favicon.ico and /dev/null differ diff --git a/apps/next/app/fonts/GeistMonoVF.woff b/apps/next/app/fonts/GeistMonoVF.woff deleted file mode 100644 index f2ae185..0000000 Binary files a/apps/next/app/fonts/GeistMonoVF.woff and /dev/null differ diff --git a/apps/next/app/fonts/GeistVF.woff b/apps/next/app/fonts/GeistVF.woff deleted file mode 100644 index 1b62daa..0000000 Binary files a/apps/next/app/fonts/GeistVF.woff and /dev/null differ diff --git a/apps/next/app/globals.css b/apps/next/app/globals.css deleted file mode 100644 index 6af7ecb..0000000 --- a/apps/next/app/globals.css +++ /dev/null @@ -1,50 +0,0 @@ -:root { - --background: #ffffff; - --foreground: #171717; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - color: var(--foreground); - background: var(--background); -} - -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - -a { - color: inherit; - text-decoration: none; -} - -.imgDark { - display: none; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } - - .imgLight { - display: none; - } - .imgDark { - display: unset; - } -} diff --git a/apps/next/app/layout.tsx b/apps/next/app/layout.tsx deleted file mode 100644 index 8469537..0000000 --- a/apps/next/app/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Metadata } from "next"; -import localFont from "next/font/local"; -import "./globals.css"; - -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/apps/next/app/page.module.css b/apps/next/app/page.module.css deleted file mode 100644 index 3630662..0000000 --- a/apps/next/app/page.module.css +++ /dev/null @@ -1,188 +0,0 @@ -.page { - --gray-rgb: 0, 0, 0; - --gray-alpha-200: rgba(var(--gray-rgb), 0.08); - --gray-alpha-100: rgba(var(--gray-rgb), 0.05); - - --button-primary-hover: #383838; - --button-secondary-hover: #f2f2f2; - - display: grid; - grid-template-rows: 20px 1fr 20px; - align-items: center; - justify-items: center; - min-height: 100svh; - padding: 80px; - gap: 64px; - font-synthesis: none; -} - -@media (prefers-color-scheme: dark) { - .page { - --gray-rgb: 255, 255, 255; - --gray-alpha-200: rgba(var(--gray-rgb), 0.145); - --gray-alpha-100: rgba(var(--gray-rgb), 0.06); - - --button-primary-hover: #ccc; - --button-secondary-hover: #1a1a1a; - } -} - -.main { - display: flex; - flex-direction: column; - gap: 32px; - grid-row-start: 2; -} - -.main ol { - font-family: var(--font-geist-mono); - padding-left: 0; - margin: 0; - font-size: 14px; - line-height: 24px; - letter-spacing: -0.01em; - list-style-position: inside; -} - -.main li:not(:last-of-type) { - margin-bottom: 8px; -} - -.main code { - font-family: inherit; - background: var(--gray-alpha-100); - padding: 2px 4px; - border-radius: 4px; - font-weight: 600; -} - -.ctas { - display: flex; - gap: 16px; -} - -.ctas a { - appearance: none; - border-radius: 128px; - height: 48px; - padding: 0 20px; - border: none; - font-family: var(--font-geist-sans); - border: 1px solid transparent; - transition: background 0.2s, color 0.2s, border-color 0.2s; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - line-height: 20px; - font-weight: 500; -} - -a.primary { - background: var(--foreground); - color: var(--background); - gap: 8px; -} - -a.secondary { - border-color: var(--gray-alpha-200); - min-width: 180px; -} - -button.secondary { - appearance: none; - border-radius: 128px; - height: 48px; - padding: 0 20px; - border: none; - font-family: var(--font-geist-sans); - border: 1px solid transparent; - transition: background 0.2s, color 0.2s, border-color 0.2s; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - line-height: 20px; - font-weight: 500; - background: transparent; - border-color: var(--gray-alpha-200); - min-width: 180px; -} - -.footer { - font-family: var(--font-geist-sans); - grid-row-start: 3; - display: flex; - gap: 24px; -} - -.footer a { - display: flex; - align-items: center; - gap: 8px; -} - -.footer img { - flex-shrink: 0; -} - -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - a.primary:hover { - background: var(--button-primary-hover); - border-color: transparent; - } - - a.secondary:hover { - background: var(--button-secondary-hover); - border-color: transparent; - } - - .footer a:hover { - text-decoration: underline; - text-underline-offset: 4px; - } -} - -@media (max-width: 600px) { - .page { - padding: 32px; - padding-bottom: 80px; - } - - .main { - align-items: center; - } - - .main ol { - text-align: center; - } - - .ctas { - flex-direction: column; - } - - .ctas a { - font-size: 14px; - height: 40px; - padding: 0 16px; - } - - a.secondary { - min-width: auto; - } - - .footer { - flex-wrap: wrap; - align-items: center; - justify-content: center; - } -} - -@media (prefers-color-scheme: dark) { - .logo { - filter: invert(); - } -} diff --git a/apps/next/app/page.tsx b/apps/next/app/page.tsx deleted file mode 100644 index 1fee7e2..0000000 --- a/apps/next/app/page.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import Image, { type ImageProps } from "next/image"; -import { Button } from "@repo/ui/button"; -import styles from "./page.module.css"; - -type Props = Omit & { - srcLight: string; - srcDark: string; -}; - -const ThemeImage = (props: Props) => { - const { srcLight, srcDark, ...rest } = props; - - return ( - <> - - - - ); -}; - -export default function Home() { - return ( -
-
- -
    -
  1. - Get started by editing apps/web/app/page.tsx -
  2. -
  3. Save and see your changes instantly.
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - -
- -
- -
- ); -} diff --git a/apps/next/eslint.config.js b/apps/next/eslint.config.js deleted file mode 100644 index 47b0670..0000000 --- a/apps/next/eslint.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import { nextJsConfig } from "@repo/eslint-config/next-js"; - -/** @type {import("eslint").Linter.Config[]} */ -export default nextJsConfig; diff --git a/apps/next/eslint.config.ts b/apps/next/eslint.config.ts new file mode 100644 index 0000000..e0e2cbe --- /dev/null +++ b/apps/next/eslint.config.ts @@ -0,0 +1,16 @@ +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"; + +export default defineConfig( + { + ignores: [".next/**"], + }, + baseConfig, + reactConfig, + nextjsConfig, + restrictEnvAccess, +); + diff --git a/apps/next/next.config.js b/apps/next/next.config.js index 4678774..0beb120 100644 --- a/apps/next/next.config.js +++ b/apps/next/next.config.js @@ -1,4 +1,65 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = {}; +import { env } from './src/env'; +import { withSentryConfig } from '@sentry/nextjs'; +import { withPlausibleProxy } from 'next-plausible'; +import { createJiti } from "jiti"; -export default nextConfig; +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"); + +/** @type {import("next").NextConfig} */ +const config = withPlausibleProxy({ + customDomain: env.NEXT_PUBLIC_PLAUSIBLE_URL, +})({ + output: 'standalone', + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '*.gbrown.org', + }, + ], + }, + serverExternalPackages: ['require-in-the-middle'], + experimental: { + serverActions: { + bodySizeLimit: '10mb', + }, + }, + /** Enables hot reloading for local packages without a build step */ + transpilePackages: [ + "@gib/backend", + "@gib/ui", + ], + typescript: { ignoreBuildErrors: true }, +}); + + + +const sentryConfig = { + // For all available options, see: + // https://www.npmjs.com/package/@sentry/webpack-plugin#options + org: env.NEXT_PUBLIC_SENTRY_ORG, + project: env.NEXT_PUBLIC_SENTRY_PROJECT_NAME, + sentryUrl: env.NEXT_PUBLIC_SENTRY_URL, + authToken: env.SENTRY_AUTH_TOKEN, + // Only print logs for uploading source maps in CI + silent: !env.CI, + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: '/monitoring', + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, + // Capture React Component Names + reactComponentAnnotation: { + enabled: true, + }, +}; +export default withSentryConfig(config, sentryConfig); diff --git a/apps/next/package.json b/apps/next/package.json index badd115..fa4d61b 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -6,11 +6,11 @@ "scripts": { "build": "bun with-env next build", "clean": "git clean -xdf .cache .next .turbo node_modules", - "dev": "pnpm with-env next dev --turbo", - "dev:tunnel": "pnpm with-env next dev --turbo", + "dev": "bun with-env next dev --turbo", + "dev:tunnel": "bun with-env next dev --turbo", "format": "prettier --check . --ignore-path ../../.gitignore", "lint": "eslint --flag unstable_native_nodejs_ts_config", - "start": "pnpm with-env next start", + "start": "bun with-env next start", "typecheck": "tsc --noEmit", "with-env": "dotenv -e ../../.env --" }, diff --git a/apps/next/postcss.config.js b/apps/next/postcss.config.js new file mode 100644 index 0000000..499d250 --- /dev/null +++ b/apps/next/postcss.config.js @@ -0,0 +1 @@ +export { default } from '@gib/tailwind-config/postcss-config'; diff --git a/apps/next/public/favicon.ico b/apps/next/public/favicon.ico new file mode 100644 index 0000000..f0058b4 Binary files /dev/null and b/apps/next/public/favicon.ico differ diff --git a/apps/next/public/file-text.svg b/apps/next/public/file-text.svg deleted file mode 100644 index 9cfb3c9..0000000 --- a/apps/next/public/file-text.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/next/public/globe.svg b/apps/next/public/globe.svg deleted file mode 100644 index 4230a3d..0000000 --- a/apps/next/public/globe.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/next/public/next.svg b/apps/next/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/apps/next/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/next/public/t3-icon.svg b/apps/next/public/t3-icon.svg new file mode 100644 index 0000000..e377165 --- /dev/null +++ b/apps/next/public/t3-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/apps/next/public/turborepo-dark.svg b/apps/next/public/turborepo-dark.svg deleted file mode 100644 index dae38fe..0000000 --- a/apps/next/public/turborepo-dark.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/apps/next/public/turborepo-light.svg b/apps/next/public/turborepo-light.svg deleted file mode 100644 index ddea915..0000000 --- a/apps/next/public/turborepo-light.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/apps/next/public/vercel.svg b/apps/next/public/vercel.svg deleted file mode 100644 index 0164ddc..0000000 --- a/apps/next/public/vercel.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/next/public/window.svg b/apps/next/public/window.svg deleted file mode 100644 index bbc7800..0000000 --- a/apps/next/public/window.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/next/src/app/_components/auth-showcase.tsx b/apps/next/src/app/_components/auth-showcase.tsx new file mode 100644 index 0000000..685a3f2 --- /dev/null +++ b/apps/next/src/app/_components/auth-showcase.tsx @@ -0,0 +1,58 @@ +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 ( +
+ +
+ ); + } + + return ( +
+

+ Logged in as {session.user.name} +

+ +
+ +
+
+ ); +} diff --git a/apps/next/src/app/_components/posts.tsx b/apps/next/src/app/_components/posts.tsx new file mode 100644 index 0000000..5a4bbeb --- /dev/null +++ b/apps/next/src/app/_components/posts.tsx @@ -0,0 +1,210 @@ +"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 ( +
{ + event.preventDefault(); + void form.handleSubmit(); + }} + > + + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + + Bug Title + + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + placeholder="Title" + /> + {isInvalid && } + + ); + }} + /> + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + + Content + + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + placeholder="Content" + /> + {isInvalid && } + + ); + }} + /> + + +
+ ); +} + +export function PostList() { + const trpc = useTRPC(); + const { data: posts } = useSuspenseQuery(trpc.post.all.queryOptions()); + + if (posts.length === 0) { + return ( +
+ + + + +
+

No posts yet

+
+
+ ); + } + + return ( +
+ {posts.map((p) => { + return ; + })} +
+ ); +} + +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 ( +
+
+

{props.post.title}

+

{props.post.content}

+
+
+ +
+
+ ); +} + +export function PostCardSkeleton(props: { pulse?: boolean }) { + const { pulse = true } = props; + return ( +
+
+

+   +

+

+   +

+
+
+ ); +} diff --git a/apps/next/src/app/api/auth/[...all]/route.ts b/apps/next/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..71c7ff7 --- /dev/null +++ b/apps/next/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "~/auth/server"; + +export const GET = auth.handler; +export const POST = auth.handler; diff --git a/apps/next/src/app/api/trpc/[trpc]/route.ts b/apps/next/src/app/api/trpc/[trpc]/route.ts new file mode 100644 index 0000000..c5548f9 --- /dev/null +++ b/apps/next/src/app/api/trpc/[trpc]/route.ts @@ -0,0 +1,46 @@ +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 }; diff --git a/apps/next/src/app/layout.tsx b/apps/next/src/app/layout.tsx new file mode 100644 index 0000000..8985f5d --- /dev/null +++ b/apps/next/src/app/layout.tsx @@ -0,0 +1,70 @@ +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 { env } from "~/env"; +import { TRPCReactProvider } from "~/trpc/react"; + +import "~/app/styles.css"; + +export const metadata: Metadata = { + metadataBase: new URL( + 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", + 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", + }, + twitter: { + 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" }, + ], +}; + +const geistSans = Geist({ + subsets: ["latin"], + variable: "--font-geist-sans", +}); +const geistMono = Geist_Mono({ + subsets: ["latin"], + variable: "--font-geist-mono", +}); + +export default function RootLayout(props: { children: React.ReactNode }) { + return ( + + + + {props.children} +
+ +
+ +
+ + + ); +} diff --git a/apps/next/src/app/page.tsx b/apps/next/src/app/page.tsx new file mode 100644 index 0000000..28a5d24 --- /dev/null +++ b/apps/next/src/app/page.tsx @@ -0,0 +1,41 @@ +import { Suspense } from "react"; + +import { HydrateClient, prefetch, trpc } from "~/trpc/server"; +import { AuthShowcase } from "./_components/auth-showcase"; +import { + CreatePostForm, + PostCardSkeleton, + PostList, +} from "./_components/posts"; + +export default function HomePage() { + prefetch(trpc.post.all.queryOptions()); + + return ( + +
+
+

+ Create T3 Turbo +

+ + + +
+ + + + +
+ } + > + + +
+ +
+
+ ); +} diff --git a/apps/next/src/app/styles.css b/apps/next/src/app/styles.css new file mode 100644 index 0000000..25d002f --- /dev/null +++ b/apps/next/src/app/styles.css @@ -0,0 +1,29 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "@acme/tailwind-config/theme"; + +@source "../../../../packages/ui/src/*.{ts,tsx}"; + +@custom-variant dark (&:where(.dark, .dark *)); +@custom-variant light (&:where(.light, .light *)); +@custom-variant auto (&:where(.auto, .auto *)); + +@utility container { + margin-inline: auto; + padding-inline: 2rem; + @media (width >= --theme(--breakpoint-sm)) { + max-width: none; + } + @media (width >= 1400px) { + max-width: 1400px; + } +} + +@layer base { + * { + @apply border-border; + } + body { + letter-spacing: var(--tracking-normal); + } +} diff --git a/apps/next/src/env.ts b/apps/next/src/env.ts new file mode 100644 index 0000000..67dfede --- /dev/null +++ b/apps/next/src/env.ts @@ -0,0 +1,48 @@ +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"), + }, + /** + * Specify your server-side environment variables schema here. + * This way you can ensure the app isn't built with invalid env vars. + */ + server: { + SENTRY_AUTH_TOKEN: z.string(), + }, + + /** + * Specify your client-side environment variables schema here. + * For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`. + */ + client: { + NEXT_PUBLIC_CONVEX_URL: z.url(), + NEXT_PUBLIC_PLAUSIBLE_URL: z.url(), + NEXT_PUBLIC_SENTRY_DSN: z.string(), + NEXT_PUBLIC_SENTRY_URL: z.string(), + NEXT_PUBLIC_SENTRY_ORG: z.string(), + NEXT_PUBLIC_SENTRY_PROJECT_NAME: z.string(), + }, + /** + * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. + */ + experimental__runtimeEnv: { + NODE_ENV: process.env.NODE_ENV, + CI: process.env.CI, + SITE_URL: process.env.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, + }, + skipValidation: + !!process.env.CI || process.env.npm_lifecycle_event === "lint", +}); diff --git a/apps/next/tsconfig.json b/apps/next/tsconfig.json index 7aef056..ad25c8b 100644 --- a/apps/next/tsconfig.json +++ b/apps/next/tsconfig.json @@ -1,20 +1,13 @@ { - "extends": "@repo/typescript-config/nextjs.json", + "extends": "@gib/tsconfig/base.json", "compilerOptions": { - "plugins": [ - { - "name": "next" - } - ] + "lib": ["ES2022", "dom", "dom.iterable"], + "jsx": "preserve", + "paths": { + "@/*": ["./src/*"] + }, + "plugins": [{ "name": "next" }] }, - "include": [ - "**/*.ts", - "**/*.tsx", - "next-env.d.ts", - "next.config.js", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] + "include": [".", ".next/types/**/*.ts"], + "exclude": ["node_modules"] } diff --git a/bun.lock b/bun.lock index d9aa4aa..70598ab 100644 --- a/bun.lock +++ b/bun.lock @@ -105,7 +105,6 @@ "@oslojs/crypto": "^1.0.1", "@react-email/components": "0.5.4", "@react-email/render": "^1.4.0", - "@t3-oss/env-core": "^0.13.8", "convex": "catalog:convex", "react": "catalog:react19", "react-dom": "catalog:react19", diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..b5b7a86 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,38 @@ +# Dependencies +node_modules +bun.lockb +*.lock + +# Next.js +apps/next/.next +apps/next/out +apps/next/.turbo + +# Development +.git +.gitignore +*.log +*.md +README.md +.env*.local +.vscode +.idea + +# Tests +**/__tests__ +**/*.test.ts +**/*.test.tsx +**/*.spec.ts + +# Build artifacts +dist +build +.turbo + +# Convex local +packages/backend/.convex +convex/_generated + +# OS +.DS_Store +Thumbs.db diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..819cc41 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,35 @@ +# Next Envrionment Variables +NETWORK=nginx-bridge +NEXT_CONTAINER_NAME=next-app +NEXT_DOMAIN_NAME=gbrown.org +# Port is disabled by default as suggested +# config is to have reverse proxy on the same +# network so you can just forward to the +# port on the internal network. +# NEXT_PORT=3000 + +# Convex Environment Variables +BACKEND_TAG=latest +BACKEND_CONTAINER_NAME=convex-backend +BACKEND_DOMAIN_NAME=convex.gbrown.org +#BACKEND_PORT= +#SITE_PROXY_PORT= +DASHBOARD_TAG=latest +DASHBOARD_CONTAINER_NAME=convex-dashboard +DASHBOARD_DOMAIN=dashboard.convex.gbrown.org +#DASHBOARD_PORT +INSTANCE_NAME=convex +#INSTANCE_SECRET= +CONVEX_CLOUD_ORIGIN=https://api.convex.gbrown.org +CONVEX_SITE_ORIGIN=https://convex.gbrown.org +DISABLE_BEACON=true +REDACT_LOGS_TO_CLIENT=true +DO_NOT_REQUIRE_SSL=true +NEXT_PUBLIC_DEPLOYMENT_URL=https://api.convex.gbrown.org +#POSTGRES_URL= +#DATABASE_URL= +#CONVEX_RELEASE_VERSION_DEV= +#ACTIONS_USER_TIMEOUT_SECS= +#MYSQL_URL= +#RUST_LOG= +#RUST_BACKTRACE= diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..ea818bd --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,57 @@ +# syntax=docker/dockerfile:1 +FROM oven/bun:alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Copy root package files +COPY package.json bun.lockb turbo.json ./ +COPY apps/next/package.json ./apps/next/package.json +COPY packages/backend/package.json ./packages/backend/package.json +COPY tools/*/package.json ./tools/ + +# Install dependencies +RUN bun install --frozen-lockfile + +# Builder stage +FROM base AS builder +WORKDIR /app + +# Copy dependencies +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/apps/next/node_modules ./apps/next/node_modules +COPY --from=deps /app/packages/backend/node_modules ./packages/backend/node_modules + +# Copy source code +COPY . . + +# Build +ENV NEXT_TELEMETRY_DISABLED=1 +ENV SKIP_ENV_VALIDATION=1 +RUN bun run build --filter=@gib/next + +# Runner stage +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copy built application +COPY --from=builder --chown=nextjs:nodejs /app/apps/next/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/apps/next/.next/static ./apps/next/.next/static +COPY --from=builder --chown=nextjs:nodejs /app/apps/next/public ./apps/next/public + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "apps/next/server.js"] diff --git a/docker/compose.yml b/docker/compose.yml new file mode 100644 index 0000000..5bcfecd --- /dev/null +++ b/docker/compose.yml @@ -0,0 +1,71 @@ +networks: + nginx-bridge: # You need to change this line to your defined network is as well + external: true + +services: + next-app: + build: + context: ../ + dockerfile: ./docker/Dockerfile + image: ${NEXT_CONTAINER_NAME}:alpine + container_name: ${NEXT_CONTAINER_NAME} + env_file: [.env] + hostname: ${NEXT_CONTAINER_NAME} + domainname: ${NEXT_DOMAIN_NAME} + networks: ['${NETWORK:-nginx-bridge}'] + #ports: ['${NEXT_PORT}:3000'] + depends_on: ['convex-backend'] + tty: true + stdin_open: true + restart: unless-stopped + + convex-backend: + image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest} + container_name: ${BACKEND_CONTAINER_NAME:-convex-backend} + hostname: ${BACKEND_CONTAINER_NAME:-convex-backend} + domainname: ${BACKEND_DOMAIN_NAME:-convex.gbrown.org} + networks: ['${NETWORK:-nginx-bridge}'] + #user: '1000:1000' + #ports: ['${BACKEND_PORT:-3210}:3210','${SITE_PROXY_PORT:-3211}:3211'] + volumes: [./data:/convex/data] + labels: ['com.centurylinklabs.watchtower.enable=true'] + env_file: ['.env'] + environment: + - INSTANCE_NAME + - INSTANCE_SECRET + - CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${BACKEND_PORT:-3210}} + - CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${SITE_PROXY_PORT:-3211}} + - DISABLE_BEACON + - REDACT_LOGS_TO_CLIENT + - DO_NOT_REQUIRE_SSL + #- DATABASE_URL=${DATABASE_URL:-} + stdin_open: true + tty: true + restart: unless-stopped + healthcheck: + test: curl -f http://localhost:3210/version + interval: 5s + start_period: 10s + stop_grace_period: 10s + stop_signal: SIGINT + + convex-dashboard: + image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest} + container_name: ${DASHBOARD_CONTAINER_NAME:-convex-dashboard} + hostname: ${DASHBOARD_CONTAINER_NAME:-convex-dashboard} + domainname: ${DASHBOARD_DOMAIN_NAME:-dashboard.${BACKEND_DOMAIN_NAME:-convex.gbrown.org}} + networks: ['${NETWORK:-nginx-bridge}'] + #user: 1000:1000 + #ports: ['${DASHBOARD_PORT:-6791}:6791'] + labels: ['com.centurylinklabs.watchtower.enable=true'] + env_file: [.env] + environment: + - NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${PORT:-3210}} + depends_on: + convex-backend: + condition: service_healthy + stdin_open: true + tty: true + restart: unless-stopped + stop_grace_period: 10s + stop_signal: SIGINT diff --git a/package.json b/package.json index 94622d7..6fc370f 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,10 @@ "clean:ws": "turbo run clean", "dev": "turbo run dev", "dev:tunnel": "turbo run dev:tunnel", - "dev:next": "turbo run dev -F @gib/next", - "dev:expo": "turbo run dev -F @gib/expo", - "dev:expo:tunnel": "turbo run dev -F @gib/expo --tunnel", + "dev:next": "turbo run dev -F @gib/next -F @gib/backend", + "dev:expo": "turbo run dev -F @gib/expo -F @gib/backend", + "dev:backend": "turbo run dev -F @gib/backend", + "dev:expo:tunnel": "turbo run dev:tunnel -F @gib/expo -F @gib/backend", "format": "turbo run format --continue -- --cache --cache-location .cache/.prettiercache", "format:fix": "turbo run format --continue -- --write --cache --cache-location .cache/.prettiercache", "lint": "turbo run lint --continue -- --cache --cache-location .cache/.eslintcache", diff --git a/packages/backend/env.ts b/packages/backend/env.ts deleted file mode 100644 index 2e6e1e4..0000000 --- a/packages/backend/env.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createEnv } from "@t3-oss/env-core"; -import { z } from "zod/v4"; - -export const convexEnv = () => { - return createEnv({ - server: {}, - runtimeEnv: process.env, - skipValidation: !!process.env.CI, - }); -}; diff --git a/packages/backend/package.json b/packages/backend/package.json index 3f140e2..6cca95d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -20,7 +20,6 @@ "@oslojs/crypto": "^1.0.1", "@react-email/components": "0.5.4", "@react-email/render": "^1.4.0", - "@t3-oss/env-core": "^0.13.8", "convex": "catalog:convex", "react": "catalog:react19", "react-dom": "catalog:react19", diff --git a/tools/eslint/base.ts b/tools/eslint/base.ts index e126739..30354b1 100644 --- a/tools/eslint/base.ts +++ b/tools/eslint/base.ts @@ -20,7 +20,7 @@ export const restrictEnvAccess = defineConfig( object: "process", property: "env", message: - "Use `import { env } from '~/env'` instead to ensure validated types.", + "Use `import { env } from '@/env'` instead to ensure validated types.", }, ], "no-restricted-imports": [ @@ -29,7 +29,7 @@ export const restrictEnvAccess = defineConfig( name: "process", importNames: ["env"], message: - "Use `import { env } from '~/env'` instead to ensure validated types.", + "Use `import { env } from '@/env'` instead to ensure validated types.", }, ], }, diff --git a/turbo.json b/turbo.json index 2a92398..c954103 100644 --- a/turbo.json +++ b/turbo.json @@ -2,12 +2,30 @@ "$schema": "https://turborepo.com/schema.json", "globalDependencies": ["**/.env.*local"], "globalEnv": [ - "CONVEX_SELF_HOSTED_URL", - "CONVEX_SELF_HOSTED_ADMIN_KEY", - "CONVEX_AUTH_URL", + "NODE_ENV", + "CI", + "SKIP_ENV_VALIDATION", "SITE_URL", "SENTRY_AUTH_TOKEN", - "SENTRY_ORG" + "NEXT_PUBLIC_SITE_URL", + "NEXT_PUBLIC_CONVEX_URL", + "NEXT_PUBLIC_SENTRY_DSN", + "NEXT_PUBLIC_SENTRY_URL", + "NEXT_PUBLIC_SENTRY_ORG", + "NEXT_PUBLIC_SENTRY_PROJECT_NAME", + "CONVEX_SELF_HOSTED_URL", + "CONVEX_SELF_HOSTED_ADMIN_KEY", + "SETUP_SCRIPT_RAN", + "SITE_URL", + "AUTH_URL", + "USESEND_API_KEY", + "AUTH_AUTHENTIK_ID", + "AUTH_AUTHENTIK_SECRET", + "AUTH_AUTHENTIK_ISSUER", + "AUTH_MICROSOFT_ENTRA_ID_ID", + "AUTH_MICROSOFT_ENTRA_ID_SECRET", + "AUTH_MICROSOFT_ENTRA_ID_ISSUER", + "AUTH_MICROSOFT_ENTRA_ID_AUTH_URL" ], "globalPassThroughEnv": [ "NODE_ENV",