Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/viet2811/uk-travel-recommendation/llms.txt

Use this file to discover all available pages before exploring further.

The frontend communicates with the Django REST backend exclusively through a thin set of typed functions in the api/ directory and a custom React hook in hooks/. All calls go through a shared axiosInstance that is pre-configured with the backend base URL, a JSON content-type header, and two interceptors — one that attaches the JWT access token to every outgoing request, and one that silently refreshes the token and retries the original call on any 401 Unauthorized response. This architecture means that feature-level code never deals with token management or retry logic directly.

Configuration

The Axios instance is created once in api/axios.ts and imported by every API module:
// api/axios.ts
import axios from 'axios';

export const axiosInstance = axios.create({
  baseURL: process.env.EXPO_PUBLIC_BACKEND_URL,
  headers: { 'Content-Type': 'application/json' },
});

Base URL

Set via the EXPO_PUBLIC_BACKEND_URL environment variable. Prefix the variable name with EXPO_PUBLIC_ to make it available in the JavaScript bundle at build time via Expo’s env-var injection.

Auth Interceptor

A request interceptor reads accessToken from expo-secure-store before every call and injects Authorization: Bearer <token>. A response interceptor handles 401 errors by refreshing the token and retrying — see the Authentication page for full details.
Never hard-code a backend URL. Use .env (development) and Expo’s EAS Secrets (production) to keep the base URL configurable across environments.

Attraction Functions

All attraction-related API calls are defined in api/attraction.ts and imported by screens and hooks as needed.
Fetches the next batch of personalised attraction recommendations for the current user.
export async function getRecommendations() {
  const geoFilter = (await AsyncStorage.getItem('geoFilter')) ?? '';
  const response = await axiosInstance.get(`/recommendations${geoFilter}`);
  return response.data;
}
DetailValue
MethodGET
Path/recommendations (optionally with a geo query string, e.g. /recommendations?area=Scotland)
Auth requiredYes
Geo filterRead from AsyncStorage key geoFilter and appended verbatim to the URL path
Returnsresponse.data — the raw backend payload
The geo filter value is a pre-formed query string (e.g. ?area=London) written by the UpdateGeoFilter screen and read here without further processing.

User Functions

User account and preference management functions are defined in api/user.ts.
Creates a new user account on the backend.
export async function registerUser({
  username,
  password,
}: {
  username: string;
  password: string;
}) {
  await axiosInstance.post('user/register/', { username, password });
}
DetailValue
MethodPOST
Pathuser/register/
Auth requiredNo
Body{ username: string, password: string }
After a successful registration, the RegisterScreen immediately calls login(username, password) from AuthContext to obtain tokens and transition the user into the onboarding flow without a separate sign-in step.

useRecommendations Hook

The useRecommendations hook in hooks/useRecommendation.tsx is the primary data layer for the DiscoveryScreen. It wraps a TanStack Query useInfiniteQuery and co-locates all swipe interaction logic, so the screen component itself stays thin.

Parameters

The hook takes no parameters. It reads the geo filter from AsyncStorage internally via getRecommendations().

Return Values

const {
  allRecommendations,  // Attraction[]              — deduplicated flat list from all fetched pages
  currentIndex,        // number                    — index of the card currently on top of the deck
  setCurrentIndex,     // (index: number) => void   — update the current index
  isLoading,           // boolean                   — true while the first page is loading
  onSwipeLeft,         // (id: string) => void      — call on left swipe (dislike)
  onSwipeRight,        // (id: string) => void      — call on right swipe (like)
} = useRecommendations();
Return valueTypeDescription
allRecommendationsAttraction[]Flat, deduplicated array of all recommendations fetched so far across all pages. Safe to index directly with currentIndex.
currentIndexnumberZero-based index of the card currently displayed on top of the swipe deck.
setCurrentIndex(index: number) => voidSetter for currentIndex. Increment this after each swipe to advance the deck.
isLoadingbooleantrue during the initial fetch. Show a loading indicator while this is true.
onSwipeLeft(id: string) => voidCall with the swiped attraction’s ID. Calls dislikeAttraction(id) directly (not via a mutation).
onSwipeRight(id: string) => voidCall with the swiped attraction’s ID. Calls likeMutation.mutate(id), which invokes likeAttraction(id) and on success invalidates the likedHistory query.

Auto-Pagination

The hook monitors currentIndex against the length of allRecommendations. When five or fewer cards remain (i.e. allRecommendations.length - currentIndex <= 5), the TanStack Query infinite query automatically fetches the next page from getRecommendations() and appends the new items. Duplicates are removed via a Set-based dedup step before the combined array is returned, ensuring the deck never shows the same attraction twice.

Swipe Behaviour

The two swipe handlers have different internal implementations:
  • onSwipeLeft(id) — calls dislikeAttraction(id) directly. The call is fire-and-forget; there is no mutation wrapper and no cache invalidation.
  • onSwipeRight(id) — calls likeMutation.mutate(id). The underlying useMutation calls likeAttraction(id) and, on success, runs queryClient.invalidateQueries({ queryKey: ['likedHistory'] }) so the Liked tab updates immediately.

Usage Example

import { useRecommendations } from 'hooks/useRecommendation';

export function DiscoveryScreen() {
  const {
    allRecommendations,
    currentIndex,
    setCurrentIndex,
    isLoading,
    onSwipeLeft,
    onSwipeRight,
  } = useRecommendations();

  if (isLoading) {
    return <LoadingSpinner />;
  }

  return (
    <SwiperList
      data={allRecommendations}
      currentIndex={currentIndex}
      onSwipeLeft={(item) => {
        onSwipeLeft(item.id);
        setCurrentIndex((i) => i + 1);
      }}
      onSwipeRight={(item) => {
        onSwipeRight(item.id);
        setCurrentIndex((i) => i + 1);
      }}
    />
  );
}
Always call both the hook’s interaction handler (onSwipeLeft / onSwipeRight) and setCurrentIndex inside the swipe callback. The interaction handler fires the network request; setCurrentIndex advances the deck and triggers the auto-pagination check.

Build docs developers (and LLMs) love