Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- Upgrade to React 19 when Expo supports it
- Adjust TRPC client for the mobile app
- Figure out how to handle html in translations across frontend and mobile apps
- A/ Tailwind -> NativeWind, use react-native-render
- B/ Use custom syntax for html/native styling
- TS ESM metro.config https://github.com/facebook/metro/issues/916
- Babel config ts is probably possible, but not worth the extra step/s
- Ignore expo-env.d.ts (?)
13 changes: 12 additions & 1 deletion apps/backend/src/api/trpc/root.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { createCallerFactory, createTRPCRouter } from "@backend/api/trpc/trpc"
import {
createCallerFactory,
createTRPCRouter,
publicQueryProcedure,
} from "@backend/api/trpc/trpc"
import { verificationTokensRouter } from "@backend/api/trpc/routers/verification-tokens-router"
import { refreshTokensRouter } from "@backend/api/trpc/routers/refresh-tokens-router"
import { usersRouter } from "@backend/api/trpc/routers/users-router"

const fooRouter = createTRPCRouter({
bar: publicQueryProcedure.query(() => {
return "foo-bar"
}),
})

export const appRouter = createTRPCRouter({
refreshTokens: refreshTokensRouter,
users: usersRouter,
verificationTokens: verificationTokensRouter,
foo: fooRouter,
})

export type AppRouterType = typeof appRouter
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ export const env = createEnv({
MAILSLURP_EMAIL: z.string().email(),
},
runtimeEnv: process.env,
skipValidation: false,
emptyStringAsUndefined: true,
})
8 changes: 4 additions & 4 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"@trpc/client": "^11.0.0-next-beta",
"@trpc/react-query": "^11.0.0-next-beta",
"@trpc/server": "^11.0.0-next-beta",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@types/uuid": "^10.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -34,8 +34,8 @@
"next": "15.1.4",
"nextjs-toploader": "^3.7.15",
"pino": "^9.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.54.0",
"react-select": "^5.8.3",
"sonner": "^1.7.1",
Expand Down
3 changes: 1 addition & 2 deletions apps/frontend/src/app/[locale]/(base)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
t_homeDocs,
t_homeIllustrationAlt,
} from "@shared/i18n/messages/t-home"

import Header from "@frontend/components/layout/header"
import Menu from "@frontend/components/navigation/menu"
import Logo from "@frontend/components/site/logo"
Expand All @@ -17,7 +16,7 @@ import Link from "next/link"
import MyButton from "@frontend/components/user-input/my-button"
import { ExternalLinkIcon } from "lucide-react"

export default function PageX() {
export default function HomePage() {
const locale = useLocale()

return (
Expand Down
24 changes: 6 additions & 18 deletions apps/frontend/src/components/site/logo.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
import { cn } from "@frontend/lib/utils"
import NavLink from "@frontend/components/navigation/nav-link"
import { Link } from "expo-router"
import { Text } from "react-native"

export default function Logo({ className }: { className?: string }) {
export default function Logo() {
return (
<NavLink
data-testid="logo"
href="/"
className={cn(
"flex h-[25px] items-center justify-center hover:no-underline",
className,
)}
>
<div className="mr-2 text-xl">ENT Stack</div>
<img
src="/static/logo.png"
alt="Logo"
className="max-h-full max-w-full"
/>
</NavLink>
<Link href="/" className="flex-row items-center">
<Text>ENT Stack</Text>
</Link>
)
}
188 changes: 76 additions & 112 deletions apps/frontend/src/trpc/query-client.ts
Original file line number Diff line number Diff line change
@@ -1,134 +1,98 @@
import {
defaultShouldDehydrateQuery,
MutationCache,
QueryCache,
QueryClient,
} from "@tanstack/react-query"
import SuperJSON from "superjson"
import { TRPCClientError } from "@trpc/client"
import { createQueryClient as createBaseQueryClient } from "@shared/trpc/create-query-client"
import {
t_clientError,
t_serverError,
} from "@shared/i18n/messages/common/t-error"
import { type LocaleType } from "@shared/i18n/t"
import { useToast } from "@frontend/hooks/use-toast"
import { handleTRPCError } from "@frontend/trpc/trpc-error-handler"
import { TRPCClientError } from "@trpc/client"
import { TRPCErrorEnum } from "@shared/enums/trpc-error-enum"
import { useRouter } from "next/navigation"
import { toastQueryParams } from "@frontend/lib/utils"
import { ToastEnum } from "@frontend/enums/toast-enum"
import { EnvironmentEnum } from "@shared/enums/environment-enum"
import { clientEnv } from "@frontend/env/client-env"
import { getLocalizedPathname } from "@frontend/lib/navigation"
import { handleTRPCError } from "@frontend/trpc/trpc-error-handler"

export default function createQueryClient(
locale: LocaleType,
toast: ReturnType<typeof useToast>,
router: ReturnType<typeof useRouter>,
) {
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error: unknown) => {
if (error instanceof TRPCClientError) {
handleTRPCError(error, locale, {
onTRPCServerError: (message) => {
// Should be logged by the server
if (
clientEnv.NEXT_PUBLIC_NODE_ENV !== EnvironmentEnum.DEVELOPMENT
) {
toast.error(t_serverError(locale))
} else {
toast.error(message)
}
},
onClientError: (message) => {
// Should be logged by the server
if (
clientEnv.NEXT_PUBLIC_NODE_ENV !== EnvironmentEnum.DEVELOPMENT
) {
toast.error(t_clientError(locale))
} else {
toast.error(message)
}
},
onUserError: async (message, code) => {
// Might be logged by the server (as a warning)
if (code === TRPCErrorEnum.UNAUTHORIZED) {
const localizedPathname = getLocalizedPathname(
locale,
"/login",
toastQueryParams(message, ToastEnum.ERROR),
)

router.push(localizedPathname)
}
},
})
}
},
}),
mutationCache: new MutationCache({
onSuccess: (data, variables, context, mutation) => {
if (!mutation.options.meta?.skipInvalidation) {
queryClient.invalidateQueries()
}
},
onError: (error: unknown) => {
if (error instanceof TRPCClientError) {
handleTRPCError(error, locale, {
onTRPCServerError: (message) => {
// Should be logged by the server
if (
clientEnv.NEXT_PUBLIC_NODE_ENV !== EnvironmentEnum.DEVELOPMENT
) {
toast.error(t_serverError(locale))
} else {
toast.error(message)
}
},
onClientError: (message) => {
// Should be logged by the server
if (
clientEnv.NEXT_PUBLIC_NODE_ENV !== EnvironmentEnum.DEVELOPMENT
) {
toast.error(t_clientError(locale))
} else {
toast.error(message)
}
},
onUserError: async (message, code) => {
// Might be logged by the server (as a warning)
if (code === TRPCErrorEnum.UNAUTHORIZED) {
const localizedPathname = getLocalizedPathname(
locale,
"/login",
toastQueryParams(message, ToastEnum.ERROR),
)

router.push(localizedPathname)
}
},
})
}
},
}),
defaultOptions: {
queries: {
staleTime: 30 * 1000,
retry: false,
},
mutations: {
retry: false,
},
dehydrate: {
serializeData: SuperJSON.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === "pending",
},
hydrate: {
deserializeData: SuperJSON.deserialize,
},
const queryClient = createBaseQueryClient({
onQueryError(error) {
if (error instanceof TRPCClientError) {
handleTRPCError(error, locale, {
onTRPCServerError: (message) => {
if (
clientEnv.NEXT_PUBLIC_NODE_ENV !== EnvironmentEnum.DEVELOPMENT
) {
toast.error(t_serverError(locale))
} else {
toast.error(message)
}
},
onClientError: (message) => {
if (
clientEnv.NEXT_PUBLIC_NODE_ENV !== EnvironmentEnum.DEVELOPMENT
) {
toast.error(t_clientError(locale))
} else {
toast.error(message)
}
},
onUserError: async (message, code) => {
if (code === TRPCErrorEnum.UNAUTHORIZED) {
const localizedPathname = getLocalizedPathname(
locale,
"/login",
toastQueryParams(message, ToastEnum.ERROR),
)
router.push(localizedPathname)
}
},
})
}
},
onMutationError(error) {
if (error instanceof TRPCClientError) {
handleTRPCError(error, locale, {
onTRPCServerError: (message) => {
if (
clientEnv.NEXT_PUBLIC_NODE_ENV !== EnvironmentEnum.DEVELOPMENT
) {
toast.error(t_serverError(locale))
} else {
toast.error(message)
}
},
onClientError: (message) => {
if (
clientEnv.NEXT_PUBLIC_NODE_ENV !== EnvironmentEnum.DEVELOPMENT
) {
toast.error(t_clientError(locale))
} else {
toast.error(message)
}
},
onUserError: async (message, code) => {
if (code === TRPCErrorEnum.UNAUTHORIZED) {
const localizedPathname = getLocalizedPathname(
locale,
"/login",
toastQueryParams(message, ToastEnum.ERROR),
)
router.push(localizedPathname)
}
},
})
}
},
onMutationSuccess(data, variables, context, mutation) {
if (!mutation.options.meta?.skipInvalidation) {
queryClient.invalidateQueries()
}
},
})

Expand Down
16 changes: 7 additions & 9 deletions apps/frontend/src/trpc/trpc-client-react.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
"use client"

import { QueryClientProvider, type QueryClient } from "@tanstack/react-query"
import { httpBatchLink } from "@trpc/client"
import { createTRPCReact } from "@trpc/react-query"
import { ReactNode, useState } from "react"
import SuperJSON from "superjson"
import type { AppRouterType } from "@backend/api/trpc/root"
import { useToast } from "@frontend/hooks/use-toast"
import createQueryClient from "@frontend/trpc/query-client"
import { type LocaleType } from "@shared/i18n/t"
import { useRouter } from "next/navigation"
import useLocale from "@frontend/hooks/use-locale"
import { RequestHeaderEnum } from "@shared/enums/request-header-enum"
import { v4 as uuidv4 } from "uuid"
import { clientEnv } from "@frontend/env/client-env"
import createQueryClient from "@frontend/trpc/query-client"

let cachedQueryClient: QueryClient | null = null

Expand All @@ -23,6 +21,7 @@ function getQueryClient(
router: ReturnType<typeof useRouter>,
) {
if (typeof window === "undefined") {
// SSR or SSG: always create a fresh instance
return createQueryClient(locale, toast, router)
}
// Use cached query client to keep the same query client when in browser
Expand All @@ -36,6 +35,7 @@ export function TRPCReactProvider(props: { children: ReactNode }) {
const locale = useLocale()
const toast = useToast({ locale })
const router = useRouter()

const queryClient = getQueryClient(locale, toast, router)

const [trpcClient] = useState(() =>
Expand All @@ -44,21 +44,19 @@ export function TRPCReactProvider(props: { children: ReactNode }) {
// There is an issue when setting server-side cookies with unstable_httpBatchStreamLink
// https://github.com/trpc/trpc/discussions/4800
httpBatchLink({
transformer: SuperJSON,
url: `${clientEnv.NEXT_PUBLIC_BACKEND_URL}/trpc`,
transformer: SuperJSON,
headers: () => {
const headers = new Headers()
headers.set(RequestHeaderEnum.ACCEPT_LANGUAGE, locale)
headers.set(RequestHeaderEnum.REQUEST_ID, uuidv4())

return headers
},
fetch: async (url, options): Promise<Response> => {
return await fetch(url, {
fetch: async (url, options) =>
fetch(url, {
...options,
credentials: "include",
})
},
}),
}),
],
}),
Expand Down
Loading