Command Palette

Search for a command to run...

Command Palette

Search for a command to run...

Showcase

BHCJobs App

A React Native job portal connecting overseas job seekers with employers — built with Expo Router and TypeScript.

BHCJobs App screenshot

BHCJobs is a production-connected mobile job portal targeting overseas job seekers — workers looking for opportunities abroad who need a fast, reliable way to browse listings, verify employers, and shortlist roles for later. It pulls real data from the BHCJobs REST API, handles full OTP-based authentication, and persists sessions across app restarts with AsyncStorage.

Overview

Most job portals built as portfolio pieces mock their data. BHCJobs does not. The app connects to a live backend, authenticates users through a phone-number OTP flow, and syncs shortlisted jobs back to the API on every change. The architecture mirrors what you would ship at a small company: typed API responses, a centralized Axios instance with interceptors, file-based navigation via Expo Router, and persistent sessions.

The user journey covers the full cycle: browse industries and companies, view detailed job postings, save favorites, and manage the shortlist from a dedicated tab — all without losing state on restart.

Features

FeatureDetails
Phone OTP authRegister or log in with a phone number; verify with a time-limited OTP code
Forgot passwordOTP-based password reset without email dependency
Industry browserBrowse job categories with live counts, tap through to filtered listings
Company profilesDedicated screens with company info and associated open positions
Job detail screenFull posting view with description, requirements, and application CTA
ShortlistingSave and unsave jobs, synced to the API; persists across sessions
Token sessionBearer token stored in AsyncStorage, refreshed through Axios interceptor

Tech Stack

ToolRole
Expo / React NativeCross-platform iOS and Android from a single codebase
Expo Router 6File-based navigation with typed routes
TypeScriptStrict typing across all API response shapes and component props
AxiosHTTP client with auth interceptor and request/response transformation
AsyncStoragePersistent key-value store for session token and user data

Auth Flow

Enter phone number

The registration or login screen accepts a phone number with country prefix. On submit, the API dispatches an OTP to the number.

Verify OTP

A timed OTP entry screen validates the code. Invalid or expired codes return a typed error that surfaces inline without crashing the screen.

Session established

On success, the API returns a Bearer token. It is written to AsyncStorage and injected into every subsequent Axios request automatically.

Token intercepted on every request

A single Axios instance created at app startup reads the stored token before each request. Expired token responses trigger a logout redirect rather than silent failures.

API Layer

All API calls go through a typed service layer rather than being scattered across screen files. Each endpoint returns a typed response or throws a structured error.

src/services/api.ts

import axios from 'axios'
import AsyncStorage from '@react-native-async-storage/async-storage'
 
export const api = axios.create({
  baseURL: process.env.EXPO_PUBLIC_API_URL,
  timeout: 10_000,
})
 
api.interceptors.request.use(async (config) => {
  const token = await AsyncStorage.getItem('auth_token')
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

src/services/jobs.ts

import { api } from './api'
import type { Job, ShortlistResponse } from '@/types'
 
export const getJobById = (id: string): Promise<Job> =>
  api.get(`/jobs/${id}`).then((r) => r.data)
 
export const shortlistJob = (jobId: string): Promise<ShortlistResponse> =>
  api.post('/shortlist', { job_id: jobId }).then((r) => r.data)
 
export const removeShortlist = (jobId: string): Promise<void> =>
  api.delete(`/shortlist/${jobId}`)

Expo Router maps the app/ directory to routes directly. Protected routes check for a stored token before rendering; unauthenticated visitors are redirected to the auth stack without any manual route guard configuration.

app/
├── (auth)/
│   ├── login.tsx
│   ├── register.tsx
│   ├── verify-otp.tsx
│   └── forgot-password.tsx
├── (tabs)/
│   ├── index.tsx          # Job listings
│   ├── companies.tsx
│   ├── industries.tsx
│   └── shortlist.tsx
└── jobs/
    └── [id].tsx           # Job detail