Skip to content

Introduction

This assumes all previous articles are read and understood.

This also assumes you are familiar with Next.js and Tailwind CSS.

Although Karr can be a federated service, the frontend does not handle federation. All federation is done on server side, the client only communicates with its instance’s API.

The UI will however show which instance a user/trip comes from.

All application code lives in the src/ directory.

  • Directorysrc/
    • Directoryapp/
      • Directory(index)/ Contains the home page files
        • apage.tsx
      • Directorysearch/
        • Directory_components/ Local use components
          • SearchBar.tsx
          • SearchResults.tsx
        • page.tsx
    • Directoryassets/ All static assets to be imported
    • Directorycomponents/ Global use components
      • QueryProvider.tsx
    • Directoryutil/
      • apifetch.ts Helper to fetch data from the API
  • package.json
  • next.config.js

Only imports from the same directory or subdirectories are relative. Otherwise, use either:

  • @/*: This alias’ root is at src/, so @/i18n/routing resolves to src/i18n/routing.
  • ~/*: This alias’ root is at src/app/, so ~/auth/actions resolves to src/app/auth/actions.

Components should live as low in the file tree as possible.

  • A component that is useful only in one page should live next to that page, or in a _components directory next to that page.
  • A component that is used across multiple pages should live in the lowest common ancestor’s directory, or its _components directory.

Highly reusable components such as buttons, tabs, avatar, stats graph, etc. should be defined in the @karr/ui package. More information on the package’s page.

All shadcn/ui components are small and for targeted uses, so they should be added to the @karr/ui package. Please refer to this package’s documentation.

The only exception is for the pre-build Blocks and Charts — not the Chart component. These are bigger pieces of UI that are composed of multiple different components, so they should be directly put in apps/web. Refer to the previous section for details.

Always use next/image’s <Image>, importing the image directly into the tsx component when possible, along with placeholder="blur" for a nice Blurhash while the image loads.

Minimise as much as possible any dependence on external providers (Google Fonts, image cdn, etc.). Always load files and content from API or assets/.

The API is built with Hono, which offers RPC for end-to-end type safety between the API and web front-end.

The RPC client is ready to use from @/util/apifetch.

import { client } from "@/util/apifetch"

Then use it inside a query. This way, the data key will be properly typed with the return type from the API route.

For example:

const { data, isError, isLoading, error } = useQuery({
queryKey: ["user"],
queryFn: async () => {
const res = await client.user.info.$get()
if (res.status !== 200) {
throw new Error("Failed to fetch user data", {
cause: await res.json()
})
}
return res.json()
}
})

This works the same for other request methods. The body fields will be properly typed.

For example with post:

const res = await tryCatch(
client.trips.add.$post({
json: {
...data
}
})
)
if (!res.success) {
console.error(res.error)
toast.error("Something went wrong")
} else {
toast.success(t("added"))
router.push("/trips/search")
}

The trip fetch route gives back an SSE. This means trips are progressively returned.

This isn’t yet supported by Hono RPC, so use:

import { apiFetch } from "@/util/apifetch"

You shouldn’t need to fetch from external urls, but if you do, use ofetch.

import { ofetch } from "ofetch"