Skip to content

Introduction

This assumes steps in Getting started and Project structure are done and understood.

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

Federation precision

Although Karr is 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.

However, the UI will show which instance a user/trip comes from.

Structure

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

Conventions/Practices

Imports

Only imports from the same directory are relative, otherwise, all are @/*. This alias’ root is at src/, so @/components/QueryProvider resolves to src/components/QueryProvider.

Components

Components that are meant to be used within only one scope should be defined in a local _components/ directory.

Components that are meant to be used in multiples pages should be defined in the root components/ directory. To avoid a huge component dump, they can be sorted by use.

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

Adding shadcn/ui components

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.

Images

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

Fetching

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

From the API

To fetch data from the API, use @/util/apifetch. This predefines the behaviour and API base route for data fetching.

Trips

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

The web frontend needs to use EventSource.

Although won’t work for the moment, the API check auth with Authorization header, which is not supported by SSE/EventSource. Need to remove auth check on this route at least until mock login is built, then use cookies instead.

Working example in Svelte
<script lang="ts">
import { onMount } from "svelte";
let dataItems: Array<Record<string, any>> = [];
let eventSource: EventSource | null = null;
let loading = true;
onMount(() => {
eventSource = new EventSource("http://localhost:3000/v1/trip/search");
// Listen for the custom "new-trips" event
eventSource.addEventListener("new-trip", (event) => {
const data = JSON.parse(event.data);
dataItems = [...dataItems, data];
console.log("New data received:", data);
console.log("All data:", dataItems);
});
eventSource.onerror = (error) => {
if (eventSource?.readyState === 0) {
console.warn("SSE connection closed by the server.");
eventSource.close();
loading = false;
} else {
console.error("EventSource failed:", error);
}
};
return () => {
// Cleanup when the component is destroyed
eventSource?.close();
};
});
</script>
<p>
PoC won't work anymore, auth has been added to the API through Authorization
header for the moment. Will move to cookie auth in the future to support
EventSource.
</p>
{#if dataItems.length === 0}
<p>No data received yet.</p>
{/if}
<div>
{#each dataItems.sort((a, b) => parseInt(a.id) - parseInt(b.id)) as item}
<div>{JSON.stringify(item)}</div>
{/each}
{#if loading}
<p>Loading...</p>
{/if}
</div>

External

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

import { ofetch } from "ofetch"