Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/astrxnomo/tourify/llms.txt

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

The Tourify frontend is a single Expo application that targets iOS, Android, and Web from one codebase. It is structured around three core pillars: a services layer for HTTP and storage, an AuthContext for session state, and a library of custom hooks that encapsulate every data-fetching concern.

Entry point chain

index.js
  └── App.js           (SafeAreaProvider + StatusBar)
        └── Router.js  (NavigationContainer + Auth gate + AuthContext.Provider)
App.js wraps the entire tree in SafeAreaProvider (from react-native-safe-area-context) and renders a dark StatusBar. It delegates all navigation and auth logic to Router.js.
// App.js
import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider } from "react-native-safe-area-context";
import Router from "./Router";

export default function App() {
  return (
    <SafeAreaProvider>
      <Router />
      <StatusBar style="dark" />
    </SafeAreaProvider>
  );
}

Auth flow and token validation

On every app start, Router.js validates any persisted token before rendering screens:
1

Read token from SecureStore

storage.getItem('auth_token') is called inside a useEffect. If nothing is stored, loading is set to false and the unauthenticated stack renders.
2

Validate with GET /api/auth/me

If a token exists, get('/auth/me') is called. A successful response means the token is still valid and setToken(stored) switches the app to the authenticated stack.
3

Handle expired or invalid token

If the request throws (401 or network error), the stored token is deleted via storage.deleteItem('auth_token') and the user is sent to the Login screen.
4

Show loading spinner

While validation is in progress, a full-screen ActivityIndicator is displayed using colors.primary (#0d9d8e) against the colors.background (#e4edee) surface.
// Router.js — token validation (simplified)
useEffect(() => {
  const validateToken = async () => {
    const stored = await storage.getItem("auth_token");
    if (!stored) { setLoading(false); return; }
    try {
      await get("/auth/me");
      setToken(stored);
    } catch {
      await storage.deleteItem("auth_token");
    } finally {
      setLoading(false);
    }
  };
  validateToken();
}, []);
Router.js uses React Navigation 7 with a NativeStackNavigator as the root. The authenticated branch nests a BottomTabNavigator (MainTabs) as the first screen.

Unauthenticated stack

Rendered when token is null:
ScreenComponentNotes
LoginLoginScreenNo header shown
RegisterRegisterScreenBack title: “Atrás”
ForgotPasswordForgotPasswordScreenBack title: “Atrás”

Authenticated stack

Rendered when a valid token exists. Main hosts the bottom tabs; the remaining screens are full-stack screens pushed over the tabs:
ScreenComponentTab icon
MainHome (tab)HomeScreenhome (MaterialIcons)
MainExplore (tab)ExploreScreenexplore
MainFavorites (tab)FavoritesScreenfavorite
MainProfile (tab)ProfileScreenperson
CityDetailCityDetailScreen
PlaceDetailPlaceDetailScreen
EventDetailEventDetailScreen
CategoryDetailCategoryDetailScreen
NotificationsNotificationsScreen
MyRegistrationsMyRegistrationsScreen
The bottom tab bar uses colors.primary (#0d9d8e) for the active tint and colors.textTertiary (#55777b) for inactive icons. It has a fixed height of 90 with 8 px padding top and bottom.

AuthContext

context/AuthContext.js creates a single React context that exposes login and logout functions defined in Router.js:
// context/AuthContext.js
import { createContext, useContext } from "react";

export const AuthContext = createContext(null);
export const useAuth = () => useContext(AuthContext);
The provider is mounted at the root of Router.js:
<AuthContext.Provider value={{ login, logout }}>
  <NavigationContainer>
    {/* ... stack navigator */}
  </NavigationContainer>
</AuthContext.Provider>
Any screen or hook that needs to trigger a login or logout imports useAuth():
const { login, logout } = useAuth();

// After a successful POST /api/auth/login:
await storage.setItem("auth_token", token);
login(token); // updates Router state → switches to authenticated stack

// On logout:
logout(); // calls POST /api/auth/logout, clears storage, sets token null

Services layer

services/post.js — HTTP client

Wraps the native fetch API with a base URL of http://localhost:8000/api. Every request reads the current Bearer token from storage before executing.
const API_URL = "http://localhost:8000/api";

async function getAuthHeaders() {
  const token = await storage.getItem("auth_token");
  return {
    "Content-Type": "application/json",
    Accept: "application/json",
    ...(token ? { Authorization: `Bearer ${token}` } : {}),
  };
}
Four named exports cover all HTTP verbs used in the app:
ExportMethodUsage
get(endpoint)GETFetching lists and detail pages
post(endpoint, data)POSTAuth, creating favorites, registering for events
patch(endpoint, data)PATCHMarking notifications as read
del(endpoint, data)DELETERemoving favorites, cancelling registrations
On a non-OK response the service throws an Error with the first human-readable message it can find: json.message → first validation error string → fallback "Error en la solicitud".

services/storage.js — Secure storage adapter

Abstracts platform differences: on native (iOS/Android) it delegates to expo-secure-store; on web it falls back to localStorage. The API is intentionally identical to SecureStore so callers never branch on platform.
import * as SecureStore from "expo-secure-store";
import { Platform } from "react-native";

const isWeb = Platform.OS === "web";

export const storage = {
  getItem:    (key) => isWeb ? Promise.resolve(localStorage.getItem(key))
                             : SecureStore.getItemAsync(key),
  setItem:    (key, value) => isWeb ? Promise.resolve(localStorage.setItem(key, value))
                                    : SecureStore.setItemAsync(key, value),
  deleteItem: (key) => isWeb ? Promise.resolve(localStorage.removeItem(key))
                              : SecureStore.deleteItemAsync(key),
};

Custom hooks

Every data-fetching and form-submission concern lives in a dedicated hook under hooks/. Hooks call the services layer and manage their own loading and error state, keeping screen components purely presentational.
This pattern makes it trivial to add new API-backed features: create a useXxx.js hook, call get or post from services/post.js, expose { data, loading, error }, and drop it into any screen. No screen component ever calls fetch directly.
hooks/
├── useApi.js               # Generic helper used internally
├── useCategories.js        # GET /api/categories
├── useCategoryDetail.js    # GET /api/categories/:id
├── useCities.js            # GET /api/cities
├── useCityDetail.js        # GET /api/cities/:id
├── useEventDetail.js       # GET /api/events/:id
├── useEventRegistration.js # POST/DELETE /api/events/:id/register
├── useEvents.js            # GET /api/events
├── useFavorites.js         # GET/POST/DELETE /api/favorites
├── useForgotPassword.js    # POST /api/auth/forgot-password
├── useLogin.js             # POST /api/auth/login
├── useMyRegistrations.js   # GET /api/my/registrations
├── useNotifications.js     # GET /api/notifications + PATCH read
├── usePlaceDetail.js       # GET /api/places/:id
├── usePlaces.js            # GET /api/places
├── usePushNotifications.js # Expo push token registration
├── useRegister.js          # POST /api/auth/register
├── useReviewForm.js        # POST /api/places/:id/reviews (or events)
└── useUser.js              # GET /api/auth/me (current user)

Screen components

All screen components live in views/public/ and map 1-to-1 with the navigation routes defined in Router.js:
views/public/
├── LoginScreen.js
├── RegisterScreen.js
├── ForgotPasswordScreen.js
├── HomeScreen.js
├── ExploreScreen.js
├── FavoritesScreen.js
├── ProfileScreen.js
├── CityDetailScreen.js
├── PlaceDetailScreen.js
├── EventDetailScreen.js
├── CategoryDetailScreen.js
├── NotificationsScreen.js
└── MyRegistrationsScreen.js

Reusable component library

Components are grouped by concern under views/components/:

auth/

AuthCheckbox, AuthDivider, AuthInput — form primitives for login, register, and password-recovery screens.

common/

Button, Card, Chip, Divider, MediaListItem, Pill — generic layout and interaction elements used across all screens.

detail/

DetailCard, DetailHero, DetailItem, ReviewCard, ReviewForm, StatCard — building blocks for city, place, and event detail pages.

explore/ & home/

explore/: CategoryCard, CityCard, EventCard
home/: CategoryList, FilterChips, HomeHeader, PlaceCard, PromoCard, SearchBar, SectionHeader

Theming and design tokens

All colors, shadows, spacing, and border radii are exported from views/constants/colors.js and imported by every component — no hard-coded hex values appear in screen files.
// views/constants/colors.js
export const colors = {
  primary:       "#0d9d8e",
  primaryLight:  "#49bfb0",
  primaryDark:   "#087a6e",
  background:    "#e4edee",
  surfaceLight:  "#e8f4f6",
  surface:       "#ffffff",
  textPrimary:   "#12343b",
  textSecondary: "#2f5b60",
  textTertiary:  "#55777b",
  textLight:     "#f7fffe",
  success:       "#15b3a7",
  warning:       "#ffa500",
  error:         "#ff6b6b",
  info:          "#0d9d8e",
  overlay:       "rgba(8, 53, 61, 0.45)",
  overlayLight:  "rgba(13, 157, 142, 0.1)",
};

export const spacing = { xs: 4, sm: 8, md: 12, lg: 16, xl: 20, "2xl": 24, "3xl": 32 };
export const radius  = { sm: 8, md: 14, lg: 16, xl: 20, full: 999 };
The shadows export provides cross-platform shadow styles (CSS box-shadow for web, elevation integers for Android):
export const shadows = {
  sm: { boxShadow: "0 2px 8px rgba(18, 52, 59, 0.08)" },
  md: { boxShadow: "0 8px 16px rgba(18, 52, 59, 0.12)" },
  lg: { boxShadow: "0 12px 34px rgba(30, 68, 74, 0.12)" },
  xl: { boxShadow: "0 24px 36px rgba(12, 53, 58, 0.2)" },
};

export const shadowStyles = { sm: { elevation: 2 }, md: { elevation: 4 },
                              lg: { elevation: 8 }, xl: { elevation: 12 } };
When building for Android, import shadowStyles (elevation) instead of shadows (box-shadow). For iOS and Web, use shadows. The separation keeps styles correct on all three platforms without runtime branching inside components.

Build docs developers (and LLMs) love